這兩周主要看了下 Elasticsearch(其實是Lucene)的 segments 的 merge 流程焕盟。事情起因是哼勇,線上的ES有些大索引,其中的segments 個數(shù)幾十個土砂,每個大小100M+乏悄,小 segments 若干龙亲,而遇到問題就是這些大的 segments 不再做 merge 了陕凹,除非強制進行forceMerge 操作震鹉,由于我們第一次ES上線,其實也不清楚這究竟是個問題還是本來 Lucene 就是這樣捆姜,網上找了一些關于ES 或者 Lucene 的 merge 的策略介紹传趾,除了說道大家都了解的一些常規(guī)的參數(shù),如最大size泥技,最大doc 下不會再做merge云云浆兰,就是沒提到到了多少個 segments 之后就不 merge ,接著問了Elasticsearch 圈子的一些人珊豹,也沒有找到非常確定的答案簸呈。查找?guī)滋熨Y料無果,順帶就看看源碼店茶,最終在昨天又瞄了幾眼終于發(fā)現(xiàn)一段算法蜕便,雖無驗證,但應八九不離十贩幻,故記錄分享之轿腺。
Segments
首先還是先重溫一下 Lucene 下的 segments丛楚,對這個比較陌生的可以閱讀三斗大神的這一節(jié)
我只引用最下面那張圖介紹一下,綠色的就是已經固化的一個個的 segments 文件趣些,不會再更新仿荆,左下角就是當前在內存的 Lucene 維護的查詢可見的仍為持久化的segment,當Elasticsearch 配置的refresh_invterval (默認是1s坏平,可調)到時拢操,這些in in-memory buffer就會推送到OS的文件系統(tǒng)緩存中去,注意這里只是到緩存舶替,很可能OS仍未持久化到文件系統(tǒng)令境,成為一個單獨的 segment 文件,而啥時commit 到文件系統(tǒng)永不丟失坎穿,則由Lucene 的flush 機制保證展父,當Lucene 做完flush 則表明該 segment 真正推送到文件系統(tǒng),此時才會在translog做標記并可以刪除commit之前的translog 了玲昧。
注意這里只是一個簡化描述,據三斗和賴總介紹篮绿,Lucene 仍有很多因素會促使產生一個segment 而不是百分百由Elasticsearch的refresh_interval 決定孵延。這里就不繼續(xù)討論究竟在哪些情況會立即生成一個segment了。
Segment 的merge
詳細信息可看三斗這一節(jié) 這里只引用兩個圖介紹一下
從圖上看亲配,Lucene每次會選取一些小的segments 進而merge到一個大的segment尘应,我這里不再贅述流程和策略惶凝,這里只補充一句就是,如果你之前用的scroll查詢犬钢,之前的scroll還是會指向老的segments苍鲜,也就是說老的segments 的引用會知道scroll失效后才會被回收。
Elasticsearch 5.x merge 參數(shù)的變化
在老的Elasticsearch 中玷犹,merge 被認為是一個非常消耗資源的操作混滔,甚至只有一個線程來做這事,并且會影響indexing的request歹颓。在之前的版本里坯屿,merge 操作用的是一類 merge throttle limit
這樣的配置來限制各種峰值數(shù)據,如下面這些參數(shù)巍扛,注意這些參數(shù)都已經在5.x 中移除掉了领跛。
- indices.store.throttle.type
- indices.store.throttle.max_bytes_per_sec
因為在Elasticsearch 5.x 想采用多線程和動態(tài)調整這種方式來更加智能地去執(zhí)行merge操作。如檢測是否使用SSD硬盤撤奸,應該啟動多少個merge線程等吠昭。
在Elasticsearch 5.x 下,tired merge policy
成為了唯一的merge策略胧瓜。因此下面的參數(shù)同樣也在5.x 下被移除了怎诫。
- index.merge.policy.type
- index.merge.policy.min_merge_size
- index.merge.policy.max_merge_size
- index.merge.policy.merge_factor
- index.merge.policy.max_merge_docs
- index.merge.policy.calibrate_size_by_deletes
- index.merge.policy.min_merge_docs
- index.merge.policy.max_merge_docs
如上面說的,ES希望采用一種更智能的方式去調整這些參數(shù)贷痪,達到一個性能的折中幻妓。在5下我們可以配置這些參數(shù):
- index.merge.policy.expunge_deletes_allowed: 指刪除了的文檔數(shù)在一個segment里占的百分比,默認是10劫拢,大于這個值時肉津,在執(zhí)行expungeDeletes 操作時將會merge這些segments.
- index.merge.policy.floor_segment: 官網的解釋我沒大看懂,我的個人理解是ES會避免產生很小size的segment舱沧,小于這個閾值的所有的非常小的segment都會做merge直到達到這個floor 的size妹沙,默認是2MB.
- index.merge.policy.max_merge_at_once: 一次最多只操作多少個segments,默認是10.
- index.merge.policy.max_merge_at_once_explicit: 顯示調用optimize 操作或者 expungeDeletes時可以操作多少個segments熟吏,默認是30.
- index.merge.policy.max_merged_segment: 超過多大size的segment不會再做merge距糖,默認是5g.
- index.merge.policy.segments_per_tier: 每個tier允許的segement 數(shù),注意這個數(shù)要大于上面的at_once數(shù)牵寺,否則這個值會先于最大可操作數(shù)到達悍引,就會立刻做merge,這樣會造成頻繁
- index.reclaim_deletes_weight: 考慮merge的segment 時刪除文檔數(shù)量多少的權重帽氓,默認即可.
- index.compund_format: 還不知道干啥用的趣斤,默認即可.
merge 線程調整
Elasticsearch 5 采用了多線程去執(zhí)行merge,可以通過修改index.merge.scheduler.max_thread_count
來動態(tài)調整這個線程數(shù)黎休,默認的話是通過下面公式去計算:
Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2))
要注意的是如果你是用HDD而非SSD的磁盤的話浓领,最好是用單線程為妙玉凯。
force merge API
這里有3個參數(shù)可以用
- max_num_segments 期望merge到多少個segments,1的意思是強行merge到1個segment
- only_expunge_deletes 只做清理有deleted的segments联贩,即瘦身
- flush 清理完執(zhí)行一下flush漫仆,默認是true
你可以用下面的URL來執(zhí)行強行的merge
curl -XPOST "http://localhost:9200/library/_forcemerge?max_num_segments=1
代碼介紹
merge的代碼相對比較少,因為基本上ES并沒有做什么東西泪幌,只是采用了多線程盲厌,和直接調用Lucene的API而已,主要看幾個類:
-
MergePolicyConfig.java
專門管理我們輸入的那些參數(shù) -
MergeScheduler.java
這個類其實就是封裝了一下Lucene的幾個調用 -
ConcurrentMergeScheduler.java
繼承了MergeScheduler.java
在上面實現(xiàn)了多線程分工并加上了多線程下的一些限制座菠,如上面配置的那些最大XXX狸眼,最多XXX 之類的。 -
ElasticsearchConcurrentMergeScheduler.java
繼承了ConcurrentMergeScheduler.java
通過配置去控制整個ES實例的merge 流程的運作,還有打印日志等浴滴。 -
TieredMergePolicy.java
merge的策略控制類拓萌,之前提到了ES5后只剩下這個唯一的默認的策略控制,所有的選擇升略,打分微王,觸發(fā)merge的策略都在這里定義。
最后發(fā)現(xiàn)問題就出在TieredMergePolicy.java
上品嚣,再次回顧我遇到的問題
事情起因是炕倘,線上的ES有些大索引,其中的segments 個數(shù)幾十個翰撑,每個大小100M+罩旋,小 segments 若干,而遇到問題就是這些大的 segments 不再做 merge 了眶诈,除非強制進行forceMerge 操作涨醋。
然后我們跳到下面這個方法:
public MergeSpecification findMerges(MergeTrigger mergeTrigger, SegmentInfos infos, IndexWriter writer) throws IOException {
<snip> //這里主要拿到總bytes數(shù),segments數(shù)逝撬,踢掉超過閾值的segments
minSegmentBytes = floorSize(minSegmentBytes);
// Compute max allowed segs in the index
long levelSize = minSegmentBytes;
long bytesLeft = totIndexBytes;
double allowedSegCount = 0;
while(true) {
final double segCountLevel = bytesLeft / (double) levelSize;
if (segCountLevel < segsPerTier) {
allowedSegCount += Math.ceil(segCountLevel);
break;
}
allowedSegCount += segsPerTier;
bytesLeft -= segsPerTier * levelSize;
levelSize *= maxMergeAtOnce;
}
int allowedSegCountInt = (int) allowedSegCount;
<snip>
問題就出在這段算法里了浴骂,tieredPolicy,顧名思義就是梯隊宪潮,階級溯警,等級,就是把所有的segments 分層一個個的階級狡相,ES的設想就是 每個tier 都應該至少包含 segsPerTier
個segments梯轻,這樣從上至下就可以分批的一次每個tier都可以做一輪merge操作,舉個例子谣光,如果我們按默認值floor的size是2MB檩淋,maxMergeAtOnce
使用默認10 的話,那么最低層就應該有 10 個 2MB的segments萄金,做完merge 應該就會產生一個20MB的 segment蟀悦,那么這個20MB應該就是下一個tier, 在這個tier里也應該有至少10個20MB的segments 來等待做merge氧敢,如此類推日戈。
那我就用我遇到的例子來演繹一遍上面的算法,假設我有5個100MB的大segments孙乖,下面就只有少數(shù)幾個的segments浙炼,segsPerTier
和maxMergeAtOnce
都采用默認值10。
- 第一次while唯袄,
segCountLevel = 500/2 = 250
顯然大于10弯屈,所以allowedSegCount = 10
,bytesLeft = 500 - 2*10 = 480
恋拷,levelSize = 2*10 = 20
- 第二次while资厉,
segCountLevel = 480/20 = 24
還是大于10,所以allowedSegCount = 10 + 10 = 20
蔬顾,bytesLeft = 480 - 20*10 = 280
宴偿,levelSize = 20*10 = 200
- 第三次while,
segCountLevel = 280/200 = 1.4
小于10 了诀豁,所以allowedSegCount = 20 + 2 = 22
窄刘; 退出
也就是說這次的merge 操作,根據當前segments總的字節(jié)數(shù)推算舷胜,ES應該是被允許最多merge 22 個segments娩践;接著就是去找實際可以merge的總的eligible的segments數(shù)量
// Gather eligible segments for merging, ie segments
// not already being merged and not already picked (by
// prior iteration of this loop) for merging:
final List<SegmentCommitInfo> eligible = new ArrayList<>();
for(int idx = tooBigCount; idx<infosSorted.size(); idx++) {
final SegmentCommitInfo info = infosSorted.get(idx);
if (merging.contains(info)) {
mergingBytes += size(info, writer);
} else if (!toBeMerged.contains(info)) {
eligible.add(info);
}
}
final boolean maxMergeIsRunning = mergingBytes >= maxMergedSegmentBytes;
if (verbose(writer)) {
message(" allowedSegmentCount=" + allowedSegCountInt + " vs count=" + infosSorted.size() + " (eligible count=" + eligible.size() + ") tooBigCount=" + tooBigCount, writer);
}
if (eligible.size() == 0) {
return spec;
}
if (eligible.size() > allowedSegCountInt) {
//可以作為預備merge的segments大于允許的數(shù),這輪merge可以做了
//剩下就是為segments打分烹骨,選出一定數(shù)量的segments來merge
} else {
//達不到預期數(shù)量翻伺,不做了
}
從上面看到,確實如果segments 不夠ES被期望的達到那么多可被merge的segments 數(shù)量的時候展氓,其實ES是不做merge的穆趴。那么就會在一種場景里面出現(xiàn):
當索引達到了比較大時,這時經過了一定時間的merge 完成后遇汞,segments都會比較大未妹,這時如果indexing的頻率相對比較低時,則每輪merge 選擇階段就會得出ES期望這次可以merge的segments數(shù)就會比較大空入,而如果eligible的segments并沒有那么大時络它,則ES就不會進行merge
這就是我得到的結論,還望各ES大神指點是否準確歪赢。