Elasticsearch架构原理
问题:
- 写入的数据是如何变成elasticsearch里可以被检索和聚合的索引内容的?
- lucene如何实现准实时索引?
- 什么是segment?
- 什么是commit?
- segment的数据来自哪里?
- segment在写入磁盘前就可以被检索,是因为利用了什么?
- elasticsearch中的refresh操作是什么?配置项是哪个?设置的命令是什么?
- refresh只是写到了文件系统缓存,那么实际写入磁盘是由什么控制呢?,如果这期间发生错误和故障,数据会不会丢失?
- 什么是translog日志?什么时候会被清空?什么是flush操作?配置项是什么?怎么配置?
什么是段合并?为什么要段合并?段合并线程配置项?段合并策略?怎么forcemerge(optimize)? - routing的规则是什么样的?replica读写过程?wait_for_active_shards参数timeout参数 ?
- reroute 接口?
- 两种 自动发现方式?
上行数据写入过程
数据的写入不依赖master节点,读取也是一样的,每一个节点都可以作为协调节点处理请求,并将数据路由到数据该有的节点,每个节点都可以查询到集群和文档的详细信息,master的作用是什么呢?es集群master节点作用是维护元信息和管理集群状态,master节点只是维护元信息并不是所有元信息的存放点,负责删除和创建索引等系列操作,但数据的写入和数据的查询都不需要经过master节点的。

数据写入的整个过程如下:数据写入请求——>协调节点接收后数据路由处理——>存入对应数据节点的index buffer并记录translog日志——>经过refresh刷新为segment存入文件缓存并变为可搜索——>数据永久刷新到磁盘并清空translog日志。到此一次数据就写完,同时后台根据merge策略进行段的合并操作,在一个索引中,segment越少,搜索效率越高,一个shard最小可以merge合并成一个segment,segment就是倒序索引。
3. 下行数据写入搜索过程
当客户端节点收到search请求后,计算出牵涉到的shard并将请求分发出去,收到请求的节点进行第一步的汇聚,然后将汇聚结果返回到client节点,client节点再次处理后,将结果发给客户
动态更新的 Lucene 索引
Lucene 把每次生成的倒排索引,叫做一个段(segment)。然后另外使用一个 commit 文件,记录索引内所有的 segment。而生成 segment 的数据来源,则是内存中的 buffer。
新接收的数据进入内存 buffer。索引状态如图

内存 buffer 刷到磁盘,生成一个新的 segment,commit 文件同步更新

利用磁盘缓存实现的准实时检索
磁盘太慢了!对要求实时性很高的服务来说,这种处理还不够,在第 3 步的处理中,还有一个中间状态:内存 buffer 生成一个新的 segment,刷到文件系统缓存中,Lucene 即可检索这个新 segment。

文件系统缓存真正同步到磁盘上,commit 文件更新。在 Elasticsearch 中,是默认设置为 1 秒间隔的,对于大多数应用来说,几乎就相当于是实时可搜索了。Elasticsearch 也提供了单独的 /_refresh 接口,用户如果对 1 秒间隔还不满意的,可以主动调用该接口来保证搜索可见。
translog 提供的磁盘同步控制
refresh 只是写到文件系统缓存,写到实际磁盘是什么来控制的?如果这期间发生主机错误、硬件故障等异常情况,数据会不会丢失?Elasticsearch 在把数据写入到内存 buffer 的同时,其实还另外记录了一个 translog 日志。

refresh 发生的时候,translog 日志文件依然保持原样

如果在这期间发生异常,Elasticsearch 会从 commit 位置开始,恢复整个 translog 文件中的记录,保证数据一致性。等到真正把 segment 刷到磁盘,且 commit 文件进行更新的时候, translog 文件才清空。这一步,叫做 flush。同样,Elasticsearch 也提供了 /_flush 接口。
对于 flush 操作,Elasticsearch 默认设置为:每 30 分钟主动进行一次 flush,或者当 translog 文件大小大于 512MB (老版本是 200MB)时,主动进行一次 flush。可以分别通过 index.translog.flush_threshold_period 和 index.translog.flush_threshold_size 参数修改。
如果对这两种控制方式都不满意,Elasticsearch 还可以通过 index.translog.flush_threshold_ops 参数,控制每收到多少条数据后 flush 一次。
translog 一致性
默认情况下,Elasticsearch 每 5 秒,或每次请求操作结束前,会强制刷新 translog 日志到磁盘上。后者是 Elasticsearch 2.0 新加入的特性。为了保证不丢数据,每次 index、bulk、delete、update 完成的时候,一定触发刷新 translog 到磁盘上,才给请求返回 200 OK。这个改变在提高数据安全性的同时当然也降低了一点性能。
不在意这点可能性,还是希望性能优先,可以在 index template 里设置如下参数:
{ "index.translog.durability": "async" }
segment merge对写入性能的影响
知道了数据怎么进入 ES 并且如何才能让数据更快的被检索使用。其中用一句话概括了 Lucene 的设计思路就是"开新文件"。从另一个方面看,开新文件也会给服务器带来负载压力。因为默认每 1 秒,都会有一个新文件产生,每个文件都需要有文件句柄,内存,CPU 使用等各种资源。一天有 86400 秒,设想一下,每次请求要扫描一遍 86400 个文件,这个响应性能绝对好不了!
当归并完成,较大的这个 segment 刷到磁盘后,commit 文件做出相应变更,删除之前几个小 segment,改成新的大 segment。等检索请求都从小 segment 转到大 segment 上以后,删除没用的小 segment。这时候,索引里 segment 数量就下降了,状态如图
归并线程配置
segment 归并的过程,需要先读取 segment,归并计算,再写一遍 segment,最后还要保证刷到磁盘。可以说,这是一个非常消耗磁盘 IO 和 CPU 的任务。所以,ES 提供了对归并线程的限速机制,确保这个任务不会过分影响到其他任务。
归并线程的数目,ES 也是有所控制的。默认数目的计算公式是: Math.min(3, Runtime.getRuntime().availableProcessors() / 2)。即服务器 CPU 核数的一半大于 3 时,启动 3 个归并线程;否则启动跟 CPU 核数的一半相等的线程数。相信一般做 Elastic Stack 的服务器 CPU 合数都会在 6 个以上。所以一般来说就是 3 个归并线程。
归并线程是按照一定的运行策略来挑选 segment 进行归并的。主要有以下几条:
- index.merge.policy.floor_segment
默认 2MB,小于这个大小的 segment,优先被归并。 - index.merge.policy.max_merge_at_once
默认一次最多归并 10 个 segment - index.merge.policy.max_merge_at_once_explicit
默认 forcemerge 时一次最多归并 30 个 segment。 - index.merge.policy.max_merged_segment
默认 5 GB,大于这个大小的 segment,不用参与归并。forcemerge 除外。
routing和replica的读写过程
作为一个没有额外依赖的简单的分布式方案,ES 在这个问题上同样选择了一个非常简洁的处理方式,对任一条数据计算其对应分片的方式如下:
shard = hash(routing) % number_of_primary_shards
每个数据都有一个 routing 参数,默认情况下,就使用其 _id 值。将其 _id 值计算哈希后,对索引的主分片数取余,就是数据实际应该存储到的分片 ID。
由于取余这个计算,完全依赖于分母,所以导致 ES 索引有一个限制,索引的主分片数,不可以随意修改。因为一旦主分片数不一样,所以数据的存储位置计算结果都会发生改变,索引数据就完全不可读了。
副本一致性
ES 数据写入流程,自然也涉及到副本。在有副本配置的情况下,数据从发向 ES 节点,到接到 ES 节点响应返回,流向如下:
- 客户端请求发送给 Node 1 节点,注意图中 Node 1 是 Master 节点,实际完全可以不是。
- Node 1 用数据的
_id取余计算得到应该将数据存储到 shard 0 上。通过 cluster state 信息发现 shard 0 的主分片已经分配到了 Node 3 上。Node 1 转发请求数据给 Node 3。 - Node 3 完成请求数据的索引过程,存入主分片 0。然后并行转发数据给分配有 shard 0 的副本分片的 Node 1 和 Node 2。当收到任一节点汇报副本分片数据写入成功,Node 3 即返回给初始的接收节点 Node 1,宣布数据写入成功。Node 1 返回成功响应给客户端。
有几个参数可以用来控制或变更其行为:
wait_for_active_shards:2 个副本分片只要有 1 个成功,就可以返回给客户端了。这点也是有配置项的。其默认值的计算来源如下:
int( (primary + number_of_replicas) / 2 ) + 1
也可以将参数设置为 one,表示仅写完主分片就返回,等同于 async;还可以设置为 all,表示等所有副本分片都写完才能返回。
timeout:如果集群出现异常,有些分片当前不可用,ES 默认会等待 1 分钟看分片能否恢复。可以使用 ?timeout=30s 参数来缩短这个等待时间。
shard 的 allocate 控制
某个 shard 分配在哪个节点上,一般来说,是由 ES 自动决定的。以下几种情况会触发分配动作:
- 新索引生成
- 索引的删除
- 新增副本分片
- 节点增减引发的数据均衡
all。可选项还包括 primaries 和 new_primaries。none 则彻底拒绝分片。该参数的作用。indices_all_active,即要求所有分片都正常启动成功以后,才可以进行数据均衡操作,否则的话,在集群重启阶段,会浪费太多流量。 indices.recovery.concurrent_streams
该参数用来控制节点从网络复制恢复副本分片时的数据流个数。默认是 3 个。可以配合上一条配置一起加大。
indices.recovery.max_bytes_per_sec
该参数用来控制节点恢复时的速率。默认是 40MB。
在高并发下如何保证读写一致性?
对于更新操作:可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖
每个文档都有一个_version 版本号,版本号在文档被改变时加一。Elasticsearch使用这个 _version 保证所有修改都被正确排序,当一个旧版本出现在新版本之后,会被简单的忽略。
利用_version的这一优点确保数据不会因为修改冲突而丢失,比如指定文档的version来做更改,如果那个版本号不是现在的,请求就失败了。
对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,副本将会在一个不同的节点上重建。
one:写操作只要有一个primary shard是active活跃可用的,就可以执行
all:写操作必须所有的primary shard和replica shard都是活跃可用的,才可以执行
quorum:默认值,要求ES中大部分的shard是活跃可用的,才可以执行写操作
对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。
倒排索引内部结构
数据生成时需要对文章进行分析,将文本拆解成一个个单词,索引结构
word documentId life 1,2 word 1
上百万或上亿文档,分词后的 word非常多,查找一个词要全局遍历,单内存就放不下,对单词进行排序,像 B+ 树一样,在页里实现二分查找。只排序还不行,单词放在磁盘,磁盘 IO 比较慢,Mysql 把索引缓存到了内存。elasticsearch 就是 Lucene 底层存储如下图所示:


Term Dictionary
快速找到某个term,将所有的term排个序,二分法查找term,logN查找效率,就像通过字典查找一样,就是Term Dictionary。和mysql方式类似为什么说查询更快呢?
Term Index
有 term dictionary 之后,用 logN 次磁盘查找得到目标。但是磁盘的随机读操作是非常昂贵的(一次 random access 大概需要 10ms 的时间)。尽量少的读磁盘,把一些数据缓存到内存里。但是整个 term dictionary 本身又太大,无法完整地放到内存里。于是就有 term index。
Lucene倒排索,增加最左边的一层「字典树」term index,不存储所有的单词,只存储单词前缀,通过字典树找可以很快速的定位到 term dictionary 的某个 offset,就是单词的大概位置,再块里二分查找,找到对应单词,再找到单词对应的文档列表。
为什么 Elasticsearch/Lucene 检索可以比 MySQL 快?
Mysql 只有 term dictionary 这一层,以 B+树 的方式存储在磁盘上的。检索一个term需若干次的 random access 磁盘操作。而 Lucene 在 term dictionary 的基础上添加term index 来加速检索,term index 以树的形式缓存在内存中。从 term index 查到对应的 term dictionary 的 block 位置之后,再去磁盘上找 term,大大减少磁盘的 random access 次数。term index 在内存中是以 FST(Finite State Transducers)对它进一步压缩来存储的。
FST
FST(Finite State Transducer)有两个优点:
1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩存储空间。使 Term Index 小到可以放进内存,也会占用更多的cpu资源。
2)查询速度快。O(len(str))的查询时间复杂度。
对“cat”、 “deep”、 “do”、 “dog” 、“dogs”这5个单词进行插入构建FST(注:必须已排序)
1)插入“cat”
插入cat,每个字母形成一条边,其中t边指向终点。

2)插入“deep”
与前一个单词“cat”进行最大前缀匹配,发现没有匹配则直接插入,P边指向终点。

3)插入“do”
发现是d,则在d边后增加新边o,o边指向终点。

4)插入“dog”
与前一个单词“do”进行最大前缀匹配,发现是do,则在o边后增加新边g,g边指向终点。

5)插入“dogs”
与前一个单词“dog”进行最大前缀匹配,发现是dog,则在g后增加新边s,s边指向终点。

得到一个有向无环图。利用该结构很方便的进行查询,如给定一个term “dog”,可以通过上述结构很方便的查询存不存在,甚至在构建过程中可以将单词与某一数字、单词进行关联,从而实现key-value的映射。
参考:
https://www.jianshu.com/p/5b88e95a9e80
浙公网安备 33010602011771号