1. 程式人生 > 其它 >Elasticsearch 效能調優:段合併(Segment merge)

Elasticsearch 效能調優:段合併(Segment merge)

Elasticsearch索引(elasticsearch index)由一個或者若干分片(shard)組成,分片(shard)通過副本(replica)來實現高可用。一個分片(share)其實就是一個Lucene索引(lucene index),一個Lucene索引(lucene index)又由一個或者若干段(segment)組成。所以,當我們查詢一個Elasticsearch索引時,查詢會在所有分片上執行,既而到段(segment),然後合併所有結果。
此文將從segment的視角,分析如何對Elasticsearch進行索引效能的優化。
倒排索引
Elasticsearch可以對全文進行檢索主要歸功於倒排索引,倒排索引被寫入磁碟後是不可改變的,永遠不能被修改。倒排索引的不變性有幾個好處:

  • 因為索引不能更新,不需要鎖
  • 檔案系統快取親和性,由於索引不會改變,只要系統記憶體足夠,大部分讀請求直接命中記憶體,可以極大提高效能
  • 其他快取,如filter快取,在索引的生命週期內始終有效
  • 寫入單個大的倒排索引允許資料被壓縮,減少磁碟I/O和需要被快取到記憶體的索引的使用量

但倒排索引的不變性,同樣意味著當需要新增文件時,需要對整個索引進行重建,當資料更新頻繁時,這個問題將會變成災難。那Elasticsearch索引近似實時性,是如何解決這個問題的呢?
段(segment)
Elasticsearch是基於Lucene來生成索引的,Lucene引入了“按段搜尋”的概念。用更多的倒排索引來反映最新的修改,這樣就不需要重建整個倒排索引而實現索引的更新,查詢時就輪詢所有的倒排索引,然後對結果進行合併。
除了上面提到的”段(segment)”的概念,Lucene還增加了一個”提交點(commit point)”的概念,”提交點(commit point)”用於列出了所有已知的”段”。
索引更新過程(段的不斷生成)
索引的更新過程可以通過refresh api和flush API來說明。
refresh API


從記憶體索引緩衝區把資料寫入新段(segment)中,並開啟,可供檢索,但這部分資料仍在快取中,未寫入磁碟。預設間隔是1s,這個時間會影響段的大小,對段的合併策略有影響,後面會分析。可以進行手動重新整理:

# 重新整理所有索引
POST /_refresh

# 指定重新整理索引
POST /index_name/_refresh

flush API
執行一個提交併且截斷translog的行為在Elasticsearch被稱作一次flush。每30分鐘或者translog太大時會進行flush,所以可以通過translog的設定來調節flush的行為。完成一次flush會有以下過程:

  • 所有在記憶體緩衝區的文件都被寫入一個新的段。
  • 緩衝區被清空。
  • 一個提交點被寫入硬碟。
  • 檔案系統快取通過fsync被重新整理(flush)。
  • 老的translog被刪除。

段合併(segment merge)
每次refresh都產生一個新段(segment),頻繁的refresh會導致段數量的暴增。段數量過多會導致過多的消耗檔案控制代碼、記憶體和CPU時間,影響查詢速度。基於這個原因,Lucene會通過合併段來解決這個問題。但是段的合併會消耗掉大量系統資源,尤其是磁碟I/O,所以在Elasticsearch 6.0版本之前對段合併都有“限流(throttling)”功能,主要是為了防止“段爆炸”問題帶來的負面影響,這種影響會拖累Elasticsearch的寫入速率。當出現”限流(throttling)”時,Elasticsearch日誌裡會出現類似如下日誌:

now throttling indexing: numMergesInFlight=7, maxNumMerges=6
stop throttling indexing: numMergesInFlight=5, maxNumMerges=6

但有時我們更在意索引批量匯入的速度,這時我們就不希望Elasticsearch對段合併進行限流,可以通過indices.store.throttle.max_bytes_per_sec提高限流閾值,預設是20MB/s:

PUT /_cluster/settings
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "200mb"
    }
}

當然也可以關掉段合併限流,”indices.store.throttle.type”設定為none即可:

PUT /_cluster/settings
{
    "transient" : {
        "indices.store.throttle.type" : "none" 
    }
}

需要注意的是,這裡的”限流(throttling)”是對流量(注意單位是Byte)進行限流,而不是限制程序(index.merge.scheduler.max_thread_count)。
indices.store.throttle.type和indices.store.throttle.max_bytes_per_sec在版本6.x已被移除,在使用中經常會發現”限速(throttling)”是併發數(index.merge.scheduler.max_thread_count),這兩個引數感覺很雞肋。但即使上面的限流關掉(none),我們在Elasticsearch日誌裡仍然能看到”throttling”日誌,這主要是因為**merge**的執行緒數達到了最大,這個最大值通過引數index.merge.scheduler.max_thread_count來設定,這個配置不能動態更新,需要設定在配置檔案elasticsearch.yml裡:

index.merge.scheduler.max_thread_count: 3

這個設定允許 max_thread_count + 2 個執行緒同時進行磁碟操作,也就是設定為 3 允許5個執行緒。預設值是 Math.min(3, Runtime.getRuntime().availableProcessors() / 2)。
段合併策略(Merge Policy)
這裡討論的Elasticsearch版本是1.6.x(目前使用的版本,有點老),這個版本里用的搜尋引擎版本是Lucene4,Lucene4中段的合併策略預設使用的是TieredMergePolicy,所以在Elasticsearch 1.6中,舊的LogMergePolicy合併策略引數已經被棄用,在Elasticsearch 2.x裡這些引數直接就被移除了。所以這節主要是討論跟TieredMergePolicy有關的調優(在版本6.x裡,merge相關的引數都被移除)。
TieredMergePolicy的特點是找出大小接近且最優的段集。首先,這個策略會計算在當前索引中可分配的段(segment)數量預算(budget,程式碼中變數allowedSegCount,通過index總大小totIndexBytes和最小段大小minSegmentBytes進行一系列計算獲得),如果超預算(budget)了,策略會對段(segment)安裝大小進行降序排序,找到*最小成本(least-cost)的段進行合併。最小成本(least-cost)*由合併的段的”傾斜度(skew,最大段除以最小段的值)”、總的合併段的大小和回收的刪除文件的百分比(percent deletes reclaimed)來衡量。”傾斜度(skew)”越小、段(segment)總大小越小、可回收的刪除文件越大,合併將會獲得更高的分數。
這個策略涉及到幾個重要的引數

  • max_merged_segment:預設5G,合併的段的總大小不能超過這個值。
  • floor_segment:當段的大小小於這個值,把段設定為這個值參與計算。預設值為2m。
  • max_merge_at_once:合併時一次允許的最大段數量,預設值是10。
  • segments_per_tier:每層允許的段數量大小,預設值是10。一般 >= max_merge_at_once。

當增大floor_segment或者index.refresh_interval的值時,minSegmentBytes(所有段中最小段的大小,最小值為floor_segment)也會變大,從而使allowedSegCount變小,最終導致合併頻繁。當減小segments_per_tier的值時,意味著更頻繁的合併和更少的段。floor_segment需要設定多大,這個跟具體業務有很大關係。
需要了解更多細節,可以閱讀這篇文章:Elasticsearch: How to avoid index throttling, deep dive in segments merging
再談限流(throttling)
前文講到Elasticsearch在進行段合併時,如果合併併發執行緒超過index.merge.scheduler.max_thread_count時,就會出現限流(throttling),這時也會拖累索引的速度。那如何避免throttling呢?
Elasticsearch 1.6中,限速發生在MergeSchedulerListener.beforeMerge,當TieredMergePolicy.findMerges策略返回的段數量超過了”maxNumMerges”值時,會啟用限速。”maxNumMerges”可以通過index.merge.scheduler.max_merge_count來進行設定ConcurrentMergeSchedulerProvider,預設設定為index.merge.scheduler.max_thread_count + 2。這個引數在官方文件中找不到,不過可以動態更新:

PUT /index_name/_settings 
{
  "index.merge.scheduler.max_merge_count": 100
}

不過這裡有待進一步測試。當然,也可以通過提高index.merge.scheduler.max_thread_count引數來增加限流的閾值,尤其當使用SSD時:

index.merge.scheduler.max_thread_count: 10

在**段合併策略**裡有提到,當增加index.refresh_interval的值時,生成大段(large segment)有可能使allowedSegCount變小,導致合併更頻繁,這樣出現併發限流的機率更高。可以通過增加index.translog.flush_threshold_size(預設512 MB)的設定,提高每次清空觸發(flush)時積累出更多的大段(larger segment)。重新整理(flush)頻率更低,大段(larger segment)合併的頻率也就更低,對磁碟的影響更小,索引的速度更快,但要求更高的heap記憶體。