TieredMergePolicy 作为 Elasticserach 默认的策略,和 LogMergePolicy 合并相邻的段不同,其合并大小相近的段。
作为 ES 使用的段策略,它的核心思想是将索引段分成多个层次(tier),每个层次的段大小会有一个预设的上限。
当某一层的段数量超过阈值或者某个段的大小达到阈值时,就会触发合并操作,将多个小段合并成一个较大的段。
通俗理解分层
为更好地辅助大家理解“分层”,这里给个通俗类比描述如下:
我们可以将书比作文档,桌面上的堆积的书表示一个索引中的段,不同大小的书堆代表不同大小的段。当桌子上的书太多时,为了节省空间和使工作台更整洁,我们可能会决定合并一些小堆书,组成一个大堆。但大堆书因为合并的代价大,所以不会频繁地进行整理或合并。当我们确实需要更多空间时,例如想在桌子上放一个大物件,这时我们可能会考虑整理大堆书。
这样的比喻可以更生动地描述TieredMergePolicy的工作原理。
桌比喻 | TieredMergePolicy | 说明 |
---|---|---|
小堆书(<5本) | 小的索引段 | 这些小段经常被合并,就像我们经常整理桌上的小堆书来为更大的物件腾地方。 |
中等堆书(~15本) | 中等大小的索引段 | 较小的段可能会被合并成这种中等大小的段,它们之间的合并频率较小堆要低一些。 |
大堆书(>20本) | 大的索引段 | 这些大段不经常被合并,就像我们不常重新整理大堆书。只有在空间不足时才会考虑整理它们。 |
合并几个小堆书 | 合并小段 | 为了节省空间和保持工作台整洁,我们会优先合并小堆书。 |
将几个中等堆书合并成大堆 | 将几个中等大小的段合并 | 当有过多的中等大小的段时,它们可能会被合并为一个大段,以减少段的数量和提高性能。 |
只在必要时整理大堆书 | 只在必要时合并大段 | 大段的合并是代价高昂的,所以不经常发生。只有在空间紧张或需要优化性能时,我们才考虑整理大堆书。 |
TieredMergePolicy 的特点包括:
TiredMergePolicy 控制着 Lucene 的索引在增删改查过程中自然发生的merge以及forcemerge的OneMerge(单个原始合并)生成策略。
接下来,我们详细分析一下 Lucene 8.11.2 版本中的 org.apache.lucene.index.TieredMergePolicy 实现并思考有哪些调优思路。
💡 注:
这个用于判断生成新段的时候,是否使用复合文件, 复合文件(Compound File)是将多个索引文件合并为一个单一的文件组合,以减少文件数量和提高性能。
在 Lucene 中,复合文件主要由两个部分组成:
DEFAULT_NO_CFS_RATIO 默认为0.1, 如果合并段的大小小于或者等于 DEFAULT_NO_CFS_RATIO * 所有段的总大小
,那么就使用复合文件,否则就不使用。
具体实现见:org.apache.lucene.index.MergePolicy#useCompoundFile
其分三种 TYPE, 但是实际上只有 2 种 TYPE:
private enum MERGE_TYPE {
NATURAL, FORCE_MERGE, FORCE_MERGE_DELETES
}
后文会对不同的类型做详细分析。
接下来,我们从具体的find流程去分析:
这个流程实际上是在org.apache.lucene.index.TieredMergePolicy#findMerges 中实现的。
他主要分为以下几个步骤:
看起来floorSegmentBytes和segsPerTier都影响着allowedSegCount ,那么他们到底是什么样的关系呢?
总的来说,就是为了得到可以合并的段列表、每次合并的最大段数、索引允许的段数、允许删除的文档数、是否有超出大的合并(合并的字节总数大于maxMergedSegmentBytes),而这些作为参数进而调用doFindMerges函数。
关键流程图如下图 1所示:
图1
其中“分层”的核心代码如下:
@Override
public MergeSpecification findMerges(...) throws IOException {
// ...
// 计算mergeFactor, 这个描述了执行一次merge,最多能包含多少个段
// 从我们提到的两个变量中求一个最小值,默认都是10
final int mergeFactor = (int) Math.min(maxMergeAtOnce, segsPerTier);
// 注意这里的分层只是逻辑概念,并没有真的对各个段的大小做范围分层,只是为了求出allowedSegCount
// 计算index中允许的segment数
// Compute max allowed segments in the index
// levelSize 每一层中允许最小段的大小, TieredMergePolicy合并相似的段,从最小段开始找
long levelSize = Math.max(minSegmentBytes, floorSegmentBytes);
// 待合并的大小
long bytesLeft = totIndexBytes;
double allowedSegCount = 0;
// 不断分层计算
while (true) {
// 计算当前层的允许的段数,剩下的字节除以每一层中允许最小段的大小
final double segCountLevel = bytesLeft / (double) levelSize;
// 如果当前允许的段数小于等于segsPerTier, 或者当前层级最小段都等于设置的最大段大小
// 直接退出循环,因为这个时候不需要再向下合并的了
if (segCountLevel < segsPerTier || levelSize == maxMergedSegmentBytes) {
allowedSegCount += Math.ceil(segCountLevel);
break;
}
allowedSegCount += segsPerTier;
bytesLeft -= segsPerTier * levelSize;
// 确保每层合并的段大小不会超过最大合并段大小
levelSize = Math.min(maxMergedSegmentBytes, levelSize * mergeFactor);
}
// 取 allowedSegCount和segsPerTier 中的较大值,确保至少可以有segsPerTier个段, 因为可能上面的逻辑只分了一个层,而且这个层得到的段数还小于segsPerTier
// 单测方法org.apache.lucene.index.TestTieredMergePolicy#testManyMaxSizeSegments 第一个findMerges 就是这种情况
allowedSegCount = Math.max(allowedSegCount, segsPerTier);
return doFindMerges(sortedInfos, maxMergedSegmentBytes, mergeFactor, (int) allowedSegCount, allowedDelCount, MERGE_TYPE.NATURAL,
mergeContext, mergingBytes >= maxMergedSegmentBytes);
}
接下来再看 doFindMerges 的逻辑, findMerges 和findForcedDeletesMerges 都调用这个方法,我们详细解释一下各个变量的含义:
这个函数不断循环,每次选择一组最佳的合并段,构建合并规格,记录已选中的段,直到不能找到更多合并或不满足特定条件为止。这个过程中,函数将合并候选段进行组合,计算合并分数,并根据一定条件选择最佳合并。以下是关键的三个循环:
其关键流程如下图2所示:
图2
核心代码分析见:org.apache.lucene.index.TieredMergePolicy#doFindMerges
doFindMerges 中有一个很重要的合并段算分逻辑,这是为了找到最优的 OneMerge 以尽量减少后续的重复合并。
protected MergeScore score(...) throws IOException {
final double skew;
if (hitTooLarge) {
final int mergeFactor = (int) Math.min(maxMergeAtOnce, segsPerTier);
skew = 1.0/mergeFactor;
} else {
skew = ((double) floorSize(segmentsSizes.get(candidate.get(0)).sizeInBytes)) / totAfterMergeBytesFloored;
}
double mergeScore = skew;
mergeScore *= Math.pow(totAfterMergeBytes, 0.05);
final double nonDelRatio = ((double) totAfterMergeBytes)/totBeforeMergeBytes;
mergeScore *= Math.pow(nonDelRatio, 2);
final double finalMergeScore = mergeScore;
总的计算公式为:
skew = Max(floorSegmentBytes, 候选列表第一个segment的Size) / totAfterMergeBytesFloored
详细说明一下,为什么不平衡的合并,会随着时间的推移,有 O(N^2) 合并的成本。
这个实现实际上在:org.apache.lucene.index.TieredMergePolicy#findForcedDeletesMerges,当执行forcemerge并带上only_expunge_deletes选项时,就会调用这个函数。
关键流程为:
关键流程图3为:
图3
注意到, findForcedDeletesMerges 传递了maxMergedSegmentBytes,也就是说它会遵守maxMergedSegmentBytes的大小限制,即大于这个大小的段,不会被合并,哪怕这个段的删除文档数超出限制了,也不会合并,因为在doFindMerges中,不会再去判断删除文档的限制了,只会判断maxMergedSegmentBytes。
这个流程实际上在:org.apache.lucene.index.TieredMergePolicy#findForcedMerges,当forceMerge没有带上only_expunge_deletes选项时,就会调用这个。
为啥他不复用doFindMerges,而findForcedDeleteesMerges复用?有以下几个原因:
因为forcemerge目标是合并成maxSegmentCount 个,和findForcedDeleteesMerges是不一样的。
其关键流程如下:
其关键流程图如下:
图4
注意到, 当maxSegmentCount = 1 的时候, 段的最大字节数为无限, 如果不是1的时候,并不能保证会生成的段≤maxSegmentCount,这是一个尽力而为的操作。
经过对以上的各个流程的分析,我们反思一下在索引正常的增删改查以及调用forcemerge时需要注意的细节和可能调优的思路。
Elasticsearch默认使用 TieredMergePolicy, 我们看下有哪些选项(org.elasticsearch.index.MergePolicyConfig):
Elasticsearch配置(都是索引级别动态) | ES 默认值 | 对应Lucene配置(变量) | Lucene 默认值 | 最佳实践&说明 |
---|---|---|---|---|
index.compound_format | 0.1 | DEFAULT_NO_CFS_RATIO | 0.1 | |
index.merge.policy.expunge_deletes_allowed | 10 | forceMergeDeletesPctAllowed | 10 | force_merge情况下,当携带only_expunge_deletes=true的forcemerge调用,文档删除的比例大于这个的并且小于max_merged_segment的段会参与合并 |
index.merge.policy.floor_segment | 2M | floorSegmentBytes | 2M | 自然合并中,期望的最小段的大小。1.floorSegmentBytes 太小,会导致分片允许存在的段很多,这可能会导致存在大量的小段没有合并;2.floorSegmentBytes 太大,会导致分片允许存的段少,这会有效降低段的个数,但是则也会导致合并越频繁,不过大小方面有index.merge.policy.max_merged_segment控制。如果发现集群中很多小段没有合并,可以尝试提高这个参数 |
index.merge.policy.max_merge_at_once | 10 | maxMergeAtOnce | 10 | 自然合并中,一个Lucene索引一次merge最多包含多少段 |
index.merge.policy.max_merge_at_once_explicit | 30 | maxMergeAtOnceExplicit | 无限 Integer.MAX_VALUE; | forc_merge情况下,一个Lucene索引一次merge最多包含多少段。 |
index.merge.policy.max_merged_segment | 5G | maxMergedSegmentBytes | 5G | 自然合并中, 一个段最大的大小,这个是一个预估值,并不一定保证合并之后的大小就小于max_merged_segment, 大于这个大小的段后续不会参与合并,除非删除的文档超出deletes_pct_allowed阈值。值越小,会更有很多分段,但是能减少merge次数。 |
index.merge.policy.segments_per_tier | 10 | segsPerTier | 10 | 自然合并中,一个分片允许的最少分片数,少了会带来更多的合并,但是多了,可能会导致一个分片最多有segments_per_tier-1个小段。如果发现集群中很多小段没有合并,可以尝试减少这个参数;相反,增加大可以减少merge次数,会有更多分段。 |
index.merge.policy.deletes_pct_allowed | 33 | deletesPctAllowed | 33 | 自然合并中, 如果一个段或者一个Lucene索引的删除的文档比例大于这个值,则一定会合,无视其他限制。如果设置得太小,会导致频繁合并。 |
有2个地方可以使用:
关键参数为:
注:
(1)forcemerge的线程池大小为:max(1, (# of allocated processors) / 8), 其队列是无限的,需要注意不能一次性提交太多索引的frocemerge。
(2)官方推荐对只读索引进行force merge ,如果我force merge之后又有新的写入,会怎么样?
(3)本文基于 Elasticsearch 7.10.2,Lucene 8.11.2 源码进行剖析!
本文分享自 铭毅天下Elasticsearch 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有