理解Elasticsearch写入过程原理

1. lucene的写操作及其问题

Elasticsearch底层使用Lucene来实现doc的读写操作,Lucene通过

public long addDocument(...);
public long deleteDocuments(...);
public long updateDocument(...);

三个方法来实现文档的写入,更新和删除操作。但是存在如下问题

  1. 没有并发设计
    lucene只是一个搜索引擎库,并没有涉及到分布式相关的设计,因此要想使用Lucene来处理海量数据,并利用分布式的能力,就必须在其之上进行分布式的相关设计。
  2. 非实时
    将文件写入lucence后并不能立即被检索,需要等待lucene生成一个完整的segment才能被检索
  3. 数据存储不可靠
    写入lucene的数据不会立即被持久化到磁盘,如果服务器宕机,那存储在内存中的数据将会丢失
  4. 不支持部分更新
    lucene中提供的updateDocuments仅支持对文档的全量更新,对部分更新不支持

2. Elasticsearch的写入方案

完整elasticsearch的写入数据流程如下:

添加document的流程

① 将数据写入buffer(内存缓冲区);

② 执行commit操作: buffer空间被占满, 其中的数据将作为新的 index segment 被commit到文件系统的cache(缓存)中;

③ cache中的index segment通过fsync强制flush到系统的磁盘上;

④ 写入磁盘的所有segment将被记录到commit point(提交点)中, 并写入磁盘;

④ 新的index segment被打开, 以备外部检索使用;

⑤ 清空当前buffer缓冲区, 等待接收新的文档.

indexing buffer优化说明如下:

(a) fsync是一个Unix系统调用函数, 用来将内存缓冲区buffer中的数据存储到文件系统. 这里作了优化, 是指将文件缓存cache中的所有segment刷新到磁盘的操作.

(b)  修改index_buffer_size 的设置,可以设置成百分数,也可设置成具体的大小,大小可根据集群的规模做不同的设置测试。indices.memory.index_buffer_size:10%(默认,可优化30%写入配置文件)

(c) 每个Shard都有一个提交点(commit point), 其中保存了当前Shard成功写入磁盘的所有segment.

针对Lucene的问题,ES做了如下设计

2.1 分布式设计:

为了支持对海量数据的存储和查询,Elasticsearch引入分片的概念,一个索引被分成多个分片,每个分片可以有一个主分片和多个副本分片,每个分片副本都是一个具有完整功能的lucene实例。分片可以分配在不同的服务器上,同一个分片的不同副本不能分配在相同的服务器上。
在进行写操作时,ES会根据传入的_routing参数(或mapping中设置的_routing, 如果参数和设置中都没有则默认使用_id), 按照公式

shard_num = hash(\routing) % num_primary_shards

,计算出文档要分配到的分片,在从集群元数据中找出对应主分片的位置,将请求路由到该分片进行文档写操作。

ES一旦创建好索引后,就无法调整分片的设置,而在ES中,一个分片实际上对应一个lucene 索引,而lucene索引的读写会占用很多的系统资源,因此,分片数不能设置过大;所以,在创建索引时,合理配置分片数是非常重要的。一般来说,我们遵循一些原则:

1. 控制每个分片占用的硬盘容量不超过ES的最大JVM的堆空间设置(一般设置不超过32G,参考上文的JVM设置原则),因此,如果索引的总容量在500G左右,那分片大小在16个左右即可;当然,最好同时考虑原则2。
2. 考虑一下node数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了1个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以, 一般都设置分片数不超过节点数的3倍。

 

2.2 近实时性-refresh操作

es接收数据请求时先存入内存中,默认每隔一秒会从内存buffer中将数据写入filesystem cache,这个过程叫做refresh;

当一个文档写入Lucene后是不能被立即查询到的,Elasticsearch提供了一个refresh操作,会定时地调用lucene的reopen(新版本为openIfChanged)为内存中新写入的数据生成一个新的segment,此时被处理的文档均可以被检索到。refresh操作的时间间隔由refresh_interval参数控制,默认为1s, 当然还可以在写入请求中带上refresh表示写入后立即refresh,另外还可以调用refresh API显式refresh。

可以看出现有流程的问题:

插入的新文档必须等待fsync操作将segment强制写入磁盘后, 才可以提供搜索.而 fsync操作的代价很大, 使得搜索不够实时,所以出现refresh操作。

① 将数据写入buffer(内存缓冲区);
② 不等buffer空间被占满, 而是每隔一定时间(默认1s), 其中的数据就作为新的index segment被commit到文件系统的cache(缓存)中;
③ index segment 一旦被写入cache(缓存), 就立即打开该segment供搜索使用;
④ 清空当前buffer缓冲区, 等待接收新的文档.

优化的地方: 过程②和过程③:
segment进入操作系统的缓存中就可以提供搜索, 这个写入和打开新segment的轻量过程被称为refresh.

优化refresh的间隔:
Elasticsearch中, 每个Shard每秒都会自动refresh一次, 所以ES是近实时的, 数据插入到可以被搜索的间隔默认是1秒
(1) 手动refresh —— 测试时使用, 正式生产中请减少使用:
        # 刷新所有索引:
        POST _refresh
        # 刷新某一个索引: 
        POST index/_refresh
(2) 手动设置refresh间隔 —— 若要优化索引速度, 而不注重实时性, 可以降低刷新频率:
        # 在已有索引中设置, 间隔10秒: 
        PUT /_all/_settings
        {
          "index":{
            "refresh_interval":"120s"
          }
        }
(3) 当你在生产环境中建立一个大的新索引时, 可以先关闭自动刷新, 要开始使用该索引时再改回来:
        # 关闭自动刷新: 
        PUT /_all/_settings
        {
            "refresh_interval": -1 
        } 

2.3 数据存储可靠性

  1. 引入translog
    当一个文档写入Lucence后是存储在内存中的,即使执行了refresh操作仍然是在文件系统缓存中,如果此时服务器宕机,那么这部分数据将会丢失。为此ES增加了translog, 当进行文档写操作时会先将文档写入Lucene,然后写入一份到translog,写入translog是落盘的(如果对可靠性要求不是很高,也可以设置异步落盘,可以提高性能,由配置index.translog.durabilityindex.translog.sync_interval控制),这样就可以防止服务器宕机后数据的丢失。由于translog是追加写入,因此性能比较好。与传统的分布式系统不同,这里是先写入Lucene再写入translog,原因是写入Lucene可能会失败,为了减少写入失败回滚的复杂度,因此先写入Lucene。
  2. flush操作
    另外每30分钟或当translog达到一定大小(由index.translog.flush_threshold_size控制,默认512mb), ES会触发一次flush操作,此时ES会先执行refresh操作将buffer中的数据生成segment,然后调用lucene的commit方法将所有内存中的segment fsync到磁盘。此时lucene中的数据就完成了持久化,会清空translog中的数据(6.x版本为了实现sequenceIDs,不删除translog) 

  3. merge操作
    由于refresh默认间隔为1s中,因此会产生大量的小segment,为此ES会运行一个任务检测当前磁盘中的segment,对符合条件的segment进行合并操作,减少lucene中的segment个数,提高查询速度,降低负载。不仅如此,merge过程也是文档删除和更新操作后,旧的doc真正被删除的时候。用户还可以手动调用_forcemerge API来主动触发merge,以减少集群的segment个数和清理已删除或更新的文档。
  4. 多副本机制
    另外ES有多副本机制,一个分片的主副分片不能分片在同一个节点上,进一步保证数据的可靠性。
Elasticsearch通过事务日志(translog)来防止数据的丢失 —— durability持久化.

(1)文档持久化到磁盘的流程
    ① 索引数据在写入内存buffer(缓冲区)的同时, 也写入到translog日志文件中;
    ② 每隔refresh_interval的时间就执行一次refresh:
        (a) 将buffer中的数据作为新的 index segment, 刷到文件系统的cache(缓存)中;
        (b) index segment一旦被写入文件cache(缓存), 就立即打开该segment供搜索使用;
    ③ 清空当前内存buffer(缓冲区), 等待接收新的文档;
    ④ 重复①~③, translog文件中的数据不断增加;
    ⑤ 每隔一定时间(默认30分钟), 或者当translog文件达到一定大小时, 发生flush操作, 并执行一次全量提交:
        (a) 将此时内存buffer(缓冲区)中的所有数据写入一个新的segment, 并commit到文件系统的cache中;
        (b) 打开这个新的segment, 供搜索使用;
        (c) 清空当前的内存buffer(缓冲区);
        (d) 将translog文件中的所有segment通过fsync强制刷到磁盘上;
        (e) 将此次写入磁盘的所有segment记录到commit point中, 并写入磁盘;
        (f) 删除当前translog, 创建新的translog接收下一波创建请求.

基于translog和commit point的数据恢复

(1) 关于translog的配置:

flush操作 = 将translog中的记录刷到磁盘上 + 更新commit point信息 + 清空translog文件.

Elasticsearch默认: 每隔30分钟就flush一次;
或者: 当translog文件的大小达到上限(默认为512MB)时主动触发flush.

相关配置为:

# 发生多少次操作(累计多少条数据)后进行一次flush, 默认是unlimited: 
index.translog.flush_threshold_ops

# 当translog的大小达到此预设值时, 执行一次flush操作, 默认是512MB: 
index.translog.flush_threshold_size

# 每隔多长时间执行一次flush操作, 默认是30min:
index.translog.flush_threshold_period

# 检查translog、并执行一次flush操作的间隔. 默认是5s: ES会在5-10s之间进行一次操作: 
index.translog.sync_interval

(2) 数据的故障恢复:

① 增删改操作成功的标志: segment被成功刷新到Primary Shard和其对应的Replica Shard的磁盘上, 对应的操作才算成功.
② translog文件中存储了上一次flush(即上一个commit point)到当前时间的所有数据的变更记录. —— 即translog中存储的是还没有被刷到磁盘的所有最新变更记录.
③ ES发生故障, 或重启ES时, 将根据磁盘中的commit point去加载已经写入磁盘的segment, 并重做translog文件中的所有操作, 从而保证数据的一致性.

(3) 异步刷新translog:

为了保证不丢失数据, 就要保护translog文件的安全:

Elasticsearch 2.0之后, 每次写请求(如index、delete、update、bulk等)完成时, 都会触发fsync将translog中的segment刷到磁盘, 然后才会返回200 OK的响应;

或者: 默认每隔5s就将translog中的数据通过fsync强制刷新到磁盘.

提高数据安全性的同时, 降低了一点性能.频繁地执行fsync操作, 可能会产生阻塞导致部分操作耗时较久. 

如果允许部分数据丢失, 可设置异步刷新translog来提高效率.优化如下:

PUT /_all/_settings
{
    "index.translog.durability": "async",
    "index.translog.flush_threshold_size":"1024mb",
    "index.translog.sync_interval": "120s"
}

优化写入流程 - 实现海量segment文件的归并-merge

由上述近实时性搜索的描述, 可知ES默认每秒都会产生一个新的segment文件, 而每次搜索时都要遍历所有的segment, 这非常影响搜索性能.

为解决这一问题, ES会对这些零散的segment进行merge(归并)操作, 尽量让索引中只保有少量的、体积较大的segment文件.这个过程由独立的merge线程负责, 不会影响新segment的产生.

同时, 在merge段文件(segment)的过程中, 被标记为deleted的document也会被彻底物理删除.

1,merge操作的流程

① 选择一些有相似大小的segment, merge成一个大的segment;
② 将新的segment刷新到磁盘上;
③ 更新commit文件: 写一个新的commit point, 包括了新的segment, 并删除旧的segment;
④ 打开新的segment, 完成搜索请求的转移;
⑤ 删除旧的小segment.

2,优化merge的配置项

segment的归并是一个非常消耗系统CPU和磁盘IO资源的任务, 所以ES对归并线程提供了限速机制, 确保这个任务不会过分影响到其他任务.

segment合并;索引节点粒度配置,segment默认最小值2M,不过有时候合并会拖累写入速率

PUT /_all/_settings 
{
  "index.merge.policy.floor_segment":"10mb"
}

(1) 归并线程的数目:

推荐设置为CPU核心数的一半, 如果磁盘性能较差, 可以适当降低配置, 避免发生磁盘IO堵塞,所以我们需要降低每个索引并发访问磁盘的线程数。这个设置允许 max_thread_count + 2 个线程同时进行磁盘操作,也就是设置为 1 允许三个线程。

PUT /_all/_settings
{
    "index.merge.scheduler.max_thread_count" : "1"
}

(2) 其他策略:

# 优先归并小于此值的segment, 默认是2MB:
index.merge.policy.floor_segment

# 一次最多归并多少个segment, 默认是10个: 
index.merge.policy.max_merge_at_once

#如果堆栈经常有很多merge,则可以调整配置,默认是10个,其应该大于等于index.merge.policy.max_merge_at_once。
index.merge.policy.segments_per_tier

# 一次直接归并多少个segment, 默认是30个 
index.merge.policy.max_merge_at_once_explicit 

# 大于此值的segment不参与归并, 默认是5GB. optimize操作不受影响,可以考虑适当降低此值 
index.merge.policy.max_merged_segment

 

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值