今天来学习下 es 的写入原理。
Elasticsearch底层使用Lucene来实现doc的读写操作:
没有并发设计
lucene
只是一个搜索引擎库,并没有涉及到分布式相关的设计,因此要想使用Lucene来处理海量数据,并利用分布式的能力,就必须在其之上进行分布式的相关设计。
非实时
将文件写入lucence
后并不能立即被检索,需要等待lucene
生成一个完整的segment
才能被检索
数据存储不可靠
写入lucene
的数据不会立即被持久化到磁盘,如果服务器宕机,那存储在内存中的数据将会丢失
不支持部分更新
lucene
中提供仅支持对文档的全量更新,对部分更新不支持。例如:对文档进行部分更新,只新增一个字段或者修改某一字段的值,Lucene
是不支持的。
针对Lucene
的问题,ES
做了如下设计
为了支持对海量数据的存储和查询,需要用到分布式系统,通过大规模集群来提高系统水平扩展能力,因此Elasticsearch
引入分片的概念,一个索引被分成多个分片(shard
)。
除了将index
分片以提高水平扩展能力,Elasticsearch
还会将shard
复制成多个副本,放置到不同的机器上,提高系统可用性,并且副分片还提供读服务,分担集群压力。
每个shard
都是一个lucene
段,是可以独立执行搜索任务最小单位。
但是多副本也会带来一致性问题。部分副本写成功,部分副本写失败。
例如:下面的集群由三个节点组成。 存在一个索引,有两个主分片,每个主分片有两个副本分片。相同分片的副本不会放在同一节点。
Elasticsearch
采用多Shard方式,通过路由规则将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能。当写入文档的时候,根据routing
规则,将文档发送给特定Shard
中建立索引。这样就能实现分布式了。
如何确定一条数据属于哪个shard?
ES会根据公式:
shard_num = hash(_routing) % num_primary_shards
_routing的默认值是文档的_id
通过计算得出文档要分配到的分片,在从集群元数据中找出对应主分片的位置,将请求路由到该分片进行文档写操作。
当一个文档写入Lucene
后是不能被立即查询到的,Elasticsearch
提供了一个refresh
操作,为内存中新写入的数据生成一个新的segment
,此时被处理的文档均可以被检索到。refresh
操作的时间间隔由refresh_interval
参数控制,默认为1s
, 当然还可以在写入请求中带上refresh表示写入后立即refresh
,另外还可以调用refresh API
显式refresh
。
lucene
支持对文档的整体更新,ES
为了支持局部更新,在Lucene
的Store
索引中存储了一个_source
字段,该字段的key
值是文档ID
, 内容是文档的原文。当进行更新操作时先从_source
中获取原文,与更新部分合并后,再调用lucene API
进行全量更新, 对于写入了ES
但是还没有refresh
的文档,可以从translog
中获取。另外为了防止读取文档过程后执行更新前有其他线程修改了文档,ES
增加了版本机制,当执行更新操作时发现当前文档的版本与预期不符,则会重新获取文档再更新。
分别从集群角度和 shard
自身角度来介绍数据如何写入。
我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。
NODE1
发送写请求。Active
的Shard
数。NODE1
使用文档ID来确定文档属于的分片(图例是:分片0),通过集群状态中的信息获知分片0的主分片位于NODE3
,因此请求被转发到NODE3
上。NODE3
上的主分片执行写操作。NODE1
和NODE2
的副分片上。Client
。(1)为什么要检查Active
的Shard
数?
ES中有一个参数,叫做wait_for_active_shards
。这个参数的含义是,在每次写入前,该shard
至少具有的active
副本数。假设我们有一个Index
,其每个Shard
有3个Replica,加上Primary
则总共有4个副本。如果配置wait_for_active_shards
为3,那么允许最多有一个Replica
挂掉,如果有两个Replica
挂掉,则Active
的副本数不足3,此时不允许写入。
这个参数默认是1,即只要Primary
在就可以写入。如果配置大于1,可以起到一种保护的作用,保证写入的数据具有更高的可靠性。但是这个参数只在写入前检查,并不保证数据一定在至少这些个副本上写入成功,所以并不是严格保证了最少写入了多少个副本。
(2)写入Primary
完成后,为何要等待所有同步Replica
响应(或连接失败)后返回?
早期ES版本,Primary
和Replica
之间是允许异步复制的,即写入Primary
成功即可返回。但是这种模式下,如果Primary挂掉,就有丢数据的风险,而且从Replica
读数据也很难保证能读到最新的数据。所以后来ES就取消异步模式了,改成Primary
等同步Replica返回后再返回给客户端。
https://github.com/elastic/elasticsearch/blob/master/docs/reference/docs/data-replication.asciidoc
Once all in-sync replicas have successfully performed the operation and responded to the primary, the primary acknowledges the successful completion of the request to the client.
{
"_shards" : {
"total" : 2,
"failed" : 0,
"successful" : 2
}
}
(3) 如果某个Replica
持续写失败,用户是否会经常查到旧数据?
假如一个Replica
持续写入失败,那么这个Replica
上的数据可能落后Primary
很多。Primary会将这个信息报告给Master
,然后Master
会在Meta
中更新这个Index
的InSyncAllocations
配置,将这个Replica
从中移除,移除后它就不再承担读请求。在Meta
更新到各个Node
之前,用户可能还会读到这个Replica
的数据,但是更新了Meta
之后就不会了。所以这个方案并不是非常的严格,考虑到ES
本身就是一个近实时系统,数据写入后需要refresh
才可见,所以一般情况下,在短期内读到旧数据应该也是可接受的。
在每一个Shard
中,写入流程分为两部分, 先写入Lucene
,再写入TransLog
。
Shard
后,先写Lucene
文件。TransLog
。TransLog
后,刷新TransLog
数据到磁盘上,并且保留一定的translog
中的数据。例如:在进行数据恢复,可以通过translog
来进行数据回放,而不是基于数据副本的恢复。提高磁盘的利用率。(1)为什么引入translog
?
当一个文档写入Lucence
后是存储在内存中的,即使执行了refresh
操作仍然是在文件系统缓存中,如果此时服务器宕机,那么这部分数据将会丢失。为此ES增加了translog
, 当进行文档写操作时会先将文档写入Lucene
,然后写入一份到translog
,写入translog
是落盘的,这样就可以防止服务器宕机后数据的丢失。由于translog
是追加写入,因此性能比较好。
而且key value的形式写Translog
, Key
是Id
, Value
是Doc
的内容。当查询的时候,如果请求的是GetDocById
则可以直接根据_id
从translog
中获取。满足nosql
场景的实时性。
(2)为什么es要先写入lucene
,后写入translog
?
Lucene
的内存写入会有很复杂的逻辑,很容易失败,比如分词,字段长度超过限制等,比较重,为了避免TransLog
中有大量无效记录,为了减少写入失败回滚的复杂度和提高速度,所以就把写Lucene
放在了最前面。
当一个文档写入Lucene后是不能被立即查询到的,Elasticsearch提供了一个refresh操作,会定时地为内存中新写入的数据生成一个新的segment,此时被处理的文档均可以被检索到。refresh操作的时间间隔由refresh_interval参数控制,默认为1s。
另外每30分钟或当translog
达到一定大小(由index.translog.flush_threshold_size
控制,默认512mb), ES
会触发一次flush
操作,此时ES
会先执行refresh
操作将buffer
中的数据生成segment
,然后调用lucene
的commit
方法将所有内存中的segment fsync
到磁盘。此时lucene
中的数据就完成了持久化。
由于refresh默认间隔为1s中,因此会产生大量的小segment,为此ES会运行一个任务检测当前磁盘中的segment,对符合条件的segment进行合并操作,减少lucene中的segment个数,提高查询速度,降低负载。
不仅如此,merge过程也是文档删除和更新操作后,旧的doc真正被删除的时候。用户还可以手动调用_forcemerge API来主动触发merge,以减少集群的segment个数和清理已删除或更新的文档。
更新流程:
_version
版本信息, 假设此时_version=1
。Doc
合并为一个完整的Doc
。Update
请求就变成了Index请求。Elasticsearch
在写入索引时, 检查客户端A提交的文档的版本信息(这里仍然是1) 和 现存的文档的版本信息(这里也是1), 发现相同后, 执行写入操作, 并修改版本号_version=2
。update
将失败。为了对比学习,也对比了一下 ES 和我之前学习过的组件一些大方向上的原理做了个比对。
为什么Kafka不支持读写分离,而ES支持读分离?
Elasticsearch
中每个Shard
都会有多个Replica
,主要是为了保证数据可靠性,除此之外,还可以增加读能力,因为写的时候虽然要写大部分Replica Shard
,但是查询的时候只需要查询Primary和Replica中的任何一个就可以了。
所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID
,第二阶段再查询DocID对应的完整文档,这种在Elasticsearch中称为query_then_fetch
。
Node 3
。Node 3
将查询请求转发到索引的每个主分片或副本分片中。ID
和排序值给协调节点,也就是 Node 3
查询阶段标识哪些文档满足搜索请求,然后需要取回这些文档。
{"from":90, "size": 10}
,则只有从第91个开始的10个结果需要被取回。