文章目录
- 核心概念
- 系统架构
- 分布式集群
- 单节点集群
- 故障转移
- 水平扩容
- 应对故障
- 路由计算(确定哪个主分片)
- 分片控制(确定哪个节点)
- 创建个集群
- 如何查看数据呢?
- 写流程
- 读流程
- 更新流程
- 分片原理
- 倒序索引
- 文档搜索
- 动态更新索引
- 持久化变更
- 文档分析
- 内置分析器
- 分析器使用场景
- 测试分析器
- IK分词器
- 自定义分析器
- 文档处理
- 文档冲突(为上述的更新流程案例)
- 乐观并发控制
核心概念
-
索引
一个索引就是一个拥有几分相似特征的文档的集合。
Eg:一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。
能搜索的数据必须索引,这样的好处是可以提高查询速度
-
类型
一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。
ES6.x之后一个索引只能有一个type
-
文档
一个文档是一个可被索引的基础信息单元,也就是一条数据
文档以 JSON(Javascript Object Notation)格式来表示,而 JSON 是一个到处存在的互联网数据交互格式。
-
字段
相当于是数据表的字段,对文档数据根据不同属性进行的分类标识。
-
映射
mapping 是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、分析器、是否被索引等等。
-
分片
应用场景:一个索引可以存储超出单个节点硬件限制的大量数据。
分片很重要,主要有两方面的原因:
1)允许你水平分割 / 扩展你的内容容量。
2)允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。
-
副本(复制分片)
Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。
复制分片主要原因:
- 在分片/节点失败的情况下,提供了高可用;so不能把复制分片与原分片置于用一个节点上
- 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。
默认情况下,Elasticsearch 中的每个索引被分片 1 个主分片和 1 个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有 1 个主分片和另外 1 个复制分片(1 个完全拷贝),这样的话每个索引总共就有 2 个分片,我们需要根据索引需要确定分片个数。
-
分配
将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由 master 节点完成的。
系统架构
说明
- 这三个节点拥有相同的cluster.name配置,所以他们组成一个集群共同承担数据和负载的压力。
- 如图所示:node1节点被选举成为主节点,将负责管理集群范围内的所有的变更,文档的增删、节点的增删等
- 当一个主节点确定后,会将主节点的分片进行备份,也即是保存副本
分布式集群
单节点集群
我们在包含一个空节点的集群内创建名为users的索引,为了演示目的,我们将分配3个主分片和一份副本(每个主分片拥有一个副本分片)
-
请求方式
向 ES 服务器发 GET 请求 :http://127.0.0.1:1001/users
-
请求体
{ "settings": { "number_of_shards": 3, "number_of_replicas": 1 // 这个number_of_replicas参数控制着一个主节点下有几份副本 } }
-
通过 elasticsearch-head 插件查看集群情况
- node-1(绿色的):表示3个主分片正常
- unassigned:表示三个副本分片没有被分配到任何节点,原因是:同一个节点既保存原始数据又保存副本毫无意义
故障转移
启动第二台节点机器(拥有同样cluster.name配置的机器)
- 同一台机器上启动第二个节点,会自动发现集群并加入其中(同一台机器上会自动加入)
- 不同的机器上启动节点时,为了加入到同一个集群,需要配置一个可连接到的单播主机列表,之所以配置是为了使用单播发现,以防止节点无意中加入集群。
-
通过 elasticsearch-head 插件查看集群情况
- node-1节点:表示三个主分片(P1、P2、P3)
- node-2节点:当第二个节点加入到集群后,3 个副本分片将会分配到这个节点上——每个主分片对应一个副本分片。这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。
水平扩容
启动第三台节点机器:为了分散负载而对分片进行重新分配(原分片不全丢在同一个篮子里了)
-
通过 elasticsearch-head 插件查看集群情况
- 可以看到node-1、node-2上各有一个分片被迁移到新的node-3节点上,现在每个节点上由开始的三个分片下降到两个分片,即硬件资源被更少的分片所共享,提升性能
-
副本扩容到6个分片
即一个原分片被复制2份,更安全,搜索的时候在任一节点上都能找到所有的分片信息,按照下面的节点配置,我们可以在失去 2 个节点的情况下不丢失任何数据。
当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。
在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。
-
请求头
向 ES 服务器发 PUT 请求 :http://127.0.0.1:1001/users/_settings
-
请求体
``` { "number_of_replicas": 2 } ```
-
通过 elasticsearch-head 插件查看集群情况
-
-
应对故障
Node-1(主节点)异常关闭
前置条件:集群必须拥有一个主节点来保证正常工作
-
ES内部处理机制
- 选举一个新的主节点: Node-3 。
- 将Node-3身上的副本提升至主分片。
-
通过 elasticsearch-head 插件查看集群情况
-
关于集群状态说明
按理说,当前node-3拥有所有的主分片,梦回之前两个节点的情况,相差无二
但是,之前我们设置了number_of_replicas为2,这时插件就会查询到每个主分片应当对应两份副本,而此时只有一份,so,报yellow状态
-
重启原主节点
- 与当前主分片对比,复制发生修改的数据文件到自己这
- master节点不会动,原master节点回来也只能当从节点,与redis类似
路由计算(确定哪个主分片)
都知道,文档要被储存到主分片中,但是主分片有很多啊,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?
首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:
shard = hash(routing) % 每个主分片的副本分片数量(number_of_primary_shards) -> [0,1,2] -> [P0, P1, P2]
- routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。
- 从公式中不难看出,创建索引的时候,每个主分片的副本分片数量(number_of_primary_shards)这个值一旦确定,就不能改变,一旦改变,之前的路由的值全都无效了,文档就找不到了。
- 概念澄清:
number_of_primary_shards
定义了每个主分片的副本数量number_of_replicas
定义了每个主分片的复制数量。
分片控制(确定哪个节点)
创建个集群
创建一个索引:emps,集群由三个节点组成,有两个主分片,每个主分片拥有两个副本
向 ES 服务器发 PUT 请求 :http://127.0.0.1:1001/emps
-
请求体
{ "settings": { "number_of_shards": 2, "number_of_replicas": 2 } }
-
通过插件查看集群情况
如何查看数据呢?
这时候我们对集群的任一节点发送查询请求,每个节点都有能力处理这些请求。
每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。
当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。
写流程
-
客户端向 Node 2 发送新建、索引或者删除请求。
-
节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 1,因为分片 0 的主分片目前被分配在 Node 1 上。
-
Node 1 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 1 将向协调节点报告成功,协调节点向客户端报告成功。
关于第三步,提升性能的方式:牺牲数据安全这个代价,即不需要等所有的副本分片都OK,协调节点才向客户端报告OK
参数 含义 consistency consistency,即一致性。在默认设置下,即使仅仅是在试图执行一个写操作之前,主分片都会要求必须要有规定数量(quorum)(或者换种说法,也即必须要有大多数)的分片副本处于活跃可用状态,才会去执行 写操作(其中分片副本可以是主分片或者副本分片)。这是为了避免在发生网络分区故障(networkpartition)的时候进行写操作,进而导致数据不一致。
规定数量 即:int( (primary + number of replicas) / 2 ) + 1
consistency 参数的值可以设为 one (只要主分片状态 ok 就允许执行 写 操作),all (必须要主分片和所有副本分片的状态没问题才允许执行 写 操作),或quorum 。默认值为 quorum,即大多数的分片副本状态没问题就允许执行_写操作。注意,规定数量 的计算公式中 number of replicas 指的是在索引设置中的设定副本分片数,而不是指当前处理活动状态的副本分片数。如果你的索引设置中指定了当前索引拥有三个副本分片,那规定数量的计算结果即:
int( (primary + 3 replicas) / 2 ) + 1 = 3
如果此时你只启动两个节点,那么处于活跃状态的分片副本数量就达不到规定数量,也因此您将无法索引和删除任何文档。timeout 如果没有足够的副本分片会发生什么? Elasticsearch 会等待,希望更多的分片出现。默认情况下,它最多等待 1 分钟。 如果你需要,你可以使用 timeout 参数使它更早终止: 100表示100 毫秒,30s 是 30 秒。
读流程
-
客户端向 Node 1 发送获取请求。
-
节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2 。
-
Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。
更新流程
高并发的情况下,容易出现问题
-
客户端向 Node 2 发送更新请求。
-
它将请求转发到主分片所在的 Node 1 。
-
Node 1 从主分片检索文档,修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 retry_on_conflict 次后放弃。
-
如果 Node 1 成功地更新文档,它将新版本的文档并行转发到 Node 2 的副本分片,重新建立索引。一旦所有副本分片都返回成功, Node 1 向协调节点也返回成功,协调节点向客户端返回成功。
分片原理
分片是 Elasticsearch 最小的工作单元。
倒序索引
传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。
所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件 ID,搜索时将这个ID 和搜索关键字进行对应,形成 K-V 对,然后对关键字进行统计计数
-
举个例子
建两个文档
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
搜索quick、brown
分析:
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
-
引入新问题
- Quick 和 quick 以独立的词条出现,然而用户可能认为它们是相同的词。
- fox 和 foxes 非常相似, 就像 dog 和 dogs ;他们有相同的词根。
- jumped 和 leap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。
文档搜索
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。
倒排索引被写入磁盘后是 不可改变的,它永远不会修改。
-
不变的好处
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像 filter 缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
-
不变的坏处
主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档 可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
动态更新索引
保留不变性的前提下,实现倒排索引的更新
- 用更多的索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。
持久化变更
文档分析
分析包含下面的过程:
- 将一块文本分成适合于倒排索引的独立的词条
- 字符过滤器:他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉 HTML,或者将 & 转化成 and。
- 分词器:一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
- 将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall
- Token 过滤器:这个过程可能会改变词条(例如,小写化Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。
内置分析器
下面几种分析其都会针对"Set the shape to semi-transparent by calling set_trans(5)"进行分词
-
标准分析器
-
原理
它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。
-
结果
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
-
-
简单分析器
-
原理
在任何不是字母的地方分隔文本,将词条小写。
-
结果
set, the, shape, to, semi, transparent, by, calling, set, trans
-
-
空格分析器
-
原理
在空格的地方划分文本。
-
结果
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
-
-
语言分析器
-
原理
特定语言分析器可用于 很多语言。它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干 。
-
结果
set, shape, semi, transpar, call, set_tran, 5
- transparent->transpar(词根)
- calling->call(词根)
- set_trans->set_tran(词根)
-
分析器使用场景
当我们 索引 一个文档,它的全文域被分析成词条以用来创建倒排索引。
但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致。
- 当你查询一个全文域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表
- 当你查询一个精确值域时,不会分析查询字符串,而是搜索你指定的精确值。
测试分析器
向 ES 服务器发 GET 请求 :http://localhost:9200/_analyze
-
请求
{ "analyzer": "standard", "text": "Text to analyze" }
-
响应
{ "tokens": [ { "token": "text", "start_offset": 0, "end_offset": 4, "type": "<ALPHANUM>", "position": 1 }, { "token": "to", "start_offset": 5, "end_offset": 7, "type": "<ALPHANUM>", "position": 2 }, { "token": "analyze", "start_offset": 8, "end_offset": 15, "type": "<ALPHANUM>", "position": 3 } ] }
- token 是实际存储到索引中的词条。
- position 指明词条在原始文本中出现的位置。
- start_offset 和 end_offset 指明字符在原始字符串中的位置。
IK分词器
ES 的默认分词器无法识别中文中测试、单词这样的词汇,而是简单的将每个字拆完分为一个词。这样的结果显然不符合我们的使用要求,所以我们需要下载 ES 对应版本的中文分词器。
-
下载
github下载地址:https://github.com/medcl/elasticsearch-analysis-ik
-
使用
将解压后的后的文件夹放入 ES 根目录下的 plugins 目录下,重启 ES 即可使用。
-
验证
GET http://localhost:9200/_analyze
-
请求
{ "text":"测试单词", "analyzer":"ik_max_word" }
- ik_max_word:会将文本做最细粒度的拆分
- ik_smart:会将文本做最粗粒度的拆分
-
响应
{ "tokens": [ { "token": "测试", "start_offset": 0, "end_offset": 2, "type": "CN_WORD", "position": 0 }, { "token": "单词", "start_offset": 2, "end_offset": 4, "type": "CN_WORD", "position": 1 } ] }
-
-
自定义拓展词汇
-
进入 ES 根目录中的 plugins 文件夹下的 ik 文件夹,进入 config 目录,创建 custom.dic文件,写入{自定义拓展词汇}。
-
打开 IKAnalyzer.cfg.xml 文件,将新建的 custom.dic 配置其中,重启 ES 服务器。
-
自定义分析器
一个分析器就是在一个包里面组合了三种函数的一个包装器, 三种函数按照顺序被执行:
-
字符过滤器
字符过滤器 用来 整理 一个尚未被分词的字符串。例如,如果我们的文本是 HTML 格式的,它会包含像
<p>
或者<div>
这样的 HTML 标签,这些标签是我们不想索引的。我们可以使用 html 清除 字符过滤器 来移除掉所有的 HTML 标签,并且像把 Á 转换为相对应的 Unicode 字符 Á 这样,转换 HTML 实体。一个分析器可能有 0 个或者多个字符过滤器。 -
分词器
一个分析器 必须有一个唯一的分词器。 分词器把字符串分解成单个词条或者词汇单元。 标准 分析器里使用的 标准 分词器 把一个字符串根据单词边界分解成单个词条,并且移除掉大部分的标点符号,然而还有其他不同行为的分词器存在。 例如,关键词分词器完整地输出 接收到的同样的字符串,并不做任何分词。 空格分词器只根据空格分割文本。正则分词器根据匹配正则表达式来分割文本。
-
词单元过滤器
经过分词,作为结果的词单元流 会按照指定的顺序通过指定的词单元过滤器。词单元过滤器可以修改、添加或者移除词单元。我们已经提到过lowercase和stop词过滤器,但是在Elasticsearch里面还有很多可供选择的词单元过滤器。词干过滤器 把单词遏制为词干。ascii_folding 过滤器移除变音符,把一个像"très"这样的词转换为"tres"。ngram和edge_ngram 词单元过滤器 可以产生适合用于部分匹配或者自动补全的词单元。
-
自定义的分析器
创建索引 PUT http://localhost:9200/my_index
-
请求
{ "settings": { "analysis": { "char_filter": { "&_to_and": { "type": "mapping", "mappings": [ "&=> and " ] } }, "filter": { "my_stopwords": { "type": "stop", "stopwords": [ "the", "a" ] } }, "analyzer": { "my_analyzer": { "type": "custom", "char_filter": [ "html_strip", "&_to_and" ], "tokenizer": "standard", "filter": [ "lowercase", "my_stopwords" ] } } } } }
使用analyze API来测试这个新的分析器 GET http://127.0.0.1:9200/my_index/_analyze
-
请求
{ "text":"The quick & brown fox", "analyzer": "my_analyzer" }
-
部分响应
{ "tokens": [ { "token": "quick", "start_offset": 4, "end_offset": 9, "type": "<ALPHANUM>", "position": 1 }, { "token": "and", "start_offset": 10, "end_offset": 11, "type": "<ALPHANUM>", "position": 2 }, ... ] }
-
文档处理
文档冲突(为上述的更新流程案例)
当我们使用 index API更新文档,可以一次性读取原始文档,做我们的修改,然后重新索引整个文档。最近的索引请求将获胜:无论最后哪一个文档被索引,都将被唯一存储在Elasticsearch中。如果其他人同时更改这个文档,他们的更改将丢失。
解决方式:加锁
-
悲观并发控制
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
-
乐观并发控制
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
乐观并发控制
当我们之前讨论 index ,GET 和 delete 请求时,我们指出每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。 Elasticsearch 使用这个 version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达,它可以被简单的忽略。
我们可以利用 version 号来确保 应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version 号来达到这个目的。 如果该版本不是当前版本号,我们的请求将会失败。
老的版本 es 使用 version,但是新版本不支持了,会报下面的错误,提示我们用 if_seq_no 和 if_primary_term