ElasticSearch Merge机制和写放大问题研究
背景:
ES在做Segment合并的时候,根据写入模式和数据量,通常会有几倍到十几倍的写放大。此处写放大的定义是:磁盘写入数据总量/最终生成索引的大小。ES通常有两种类型的合并:NatureMerge和ForceMerge。NatureMerge是ES为了提升查询性能、回收删除Doc,在后台定期调度的merge操作。对索引的任何增删改操作都有可能触发NatureMerge。而ForceMerge是指在一次导入大批量数据后,由运维人员手动触发的merge操作,目的是减少segment数量,提升查询性能。ForceMerge由于是人工触发,通常预设当前无查询流量,所以过程比较激进,例如默认会merge成一个segment。
ES Merge过程解析
ES Merge过程可以视为一个典型的生产者-消费者模式。首先由MergePolicy根据一系列算法生成一个MergeSpecification对象。其次由MergeScheduler 执行这个MergeSpecification。
MergePolicy类图如上所示,当前ES/Lucene的默认Merge策略是TieredMergePolicy。具体算法流程解析稍后给出。在基类中的findMerges/findForcedMerges分别对应前述的NatureMerge和ForceMerge过程。
MergeScheduler的类图如上所示,ES/Lucene默认实现为ConcurrentMergeScheduler。在Lucene层面,如果用户不想触发Merge,可以把默认MergePolicy和MergeScheduler分别指定为NoMergePolicy和NoMergeScheduler。但是ES目前并不开放这个配置。
分层Nature合并:
在前文中提到ES/Lucene的默认merge策略是TieredMergePolicy,即分层Merge。注意此处层只是一个按Segment大小划分的逻辑概念,在文件系统和ES架构中不同层的索引并无本质区别。分层Merge主流程主要分三步:
1. 根据分层算法和deletesPctAllowed配置推导出本次Merge完成后AllowedSegCount和AllowedDelCount。这两个值是后续循环中止条件。分层算法的逻辑非常简单:比如当前索引总量为20M,AllowedSegCount为10个(10*2M)。当前索引总量为220M,AllowedSegCount为20个(20M*10 + 2M*10)。依此类推。
2. 滑动窗口算法寻找OneMerge对象。如下图所示:对候选Segments按大小排序,通过一个滑动窗口从左往右滑动。窗口包含的Segment数从1开始,最大不超过maxMergeAtOnce(默认10)。同时窗口内的SegmentSize总和不超过maxMergedSegmentByte(默认5G)。
3. 对第二步选出来的OneMerge对象进行打分(分数越低越优)。打分考虑如下三个因素,考虑权重依次降低:
a) 选中Segment的大小平均度,越平均越好
b) 选中Segment中可回收Doc的比率,越高越好。
c) 选中SegmentByte总和,越小越好
分层Force合并
由于Force合并是手工触发,并不考虑当前服务吞吐和延迟。所以策略比Nature合并简单粗暴很多。
1. 首先看当前Segment总数<maxMergeAtOnceExplicit(30),并且输出的maxSegmentCount=1,如果两个条件均满足则生成一个All-In-One MergeSpecification对象并返回。
2. 如果上述条件不满足,则依旧采用滑动窗口算法。但是和Nature合并相反,Force合并是从右向左滑动。窗口的初始值为2,不超过maxMergeAtOnceExplicit,并且窗口内SegmentSize总和不超过maxMergedSegmentByte。
ConcurrentMergeScheduler执行过程
ConcurrentMergeScheduler实际上是对一个后台线程池的封装。当设备硬件为传统磁盘时,启动1个线程。当年设备硬件为SSD固态硬盘时,启动的线程数为max(1, min(4, core/2))。同时工作的MergeCount = ThreadCount + 5。ConcurrentMergeScheduler执行流程如下图所示:
由上图可知,整个流程其实就是对线程池的调用,虚线部分表示这是一个线程池Push的异步操作,并不需要等待merge工作实际完成。整个Merge过程实际上分四步,分别是mergeInit,mergeMiddle,mergeSuccess和mergeFinish。实际干活的事情都在mergeMiddle中实现。MergeMiddle借助SegmentMerger封装,对FieldInfo、倒排、正排等索引结构做依次Merge。Merge过程如下:
Lucene借助Codec的抽象,将索引处理流程和索引数据结构解耦开。右边是Merge流程,左边是每个数据结构对应的Codec。Codec中包含输入数据的Consumer/Producer。分别负责生成索引和读取索引。
小结
由上文可知,整个Lucene索引Merge的流程并不复杂。通过Policy/Scheduler将索引合并的描述MergeSpecification的生成和执行解耦。开发者可以根据自己的业务场景需要,自由灵活的组装。而Merge的实际工作MergeMiddle主要依赖SegmentMerger类实现。
附录1:ES/Lucene对Merge写放大有影响的参数
参数名 |
含义 |
默认值 |
备注 |
max_merge_at_once
|
一次普通merge可以参与的segment数量 |
10 |
|
max_merge_at_once_explicit |
一次forcemerge可以参与的segment数量 |
30 |
推荐适度调大,可降低写放大。 |
max_merged_segment_bytes |
OneMerge产出的segment最大值 |
5G |
对于小规模索引够用,对于海量索引数据推荐调大。如调为0则不触发Nature Merge |
附录2:参考文献
Lucene-8.6源码:https://github.com/apache/lucene-solr
Lucene官方API文档:https://lucene.apache.org/core/8_6_2/core/index.html
Solar官方文档: https://lucene.apache.org/solr/guide/8_6/
http://blog.mikemccandless.com/2011/02/visualizing-lucenes-segment-merges.html
- 点赞
- 收藏
- 关注作者
评论(0)