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,這個時間會影響段的大小,對段的合並策略有影響,后面會分析。可以進行手動刷新:
1 |
# 刷新所有索引 |
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日志里會出現類似如下日志:
1 |
now throttling indexing: numMergesInFlight=7, maxNumMerges=6 |
但有時我們更在意索引批量導入的速度,這時我們就不希望Elasticsearch對段合並進行限流,可以通過indices.store.throttle.max_bytes_per_sec提高限流閾值,默認是20MB/s:
1 |
PUT /_cluster/settings |
當然也可以關掉段合並限流,”indices.store.throttle.type”設置為none即可:
1 |
PUT /_cluster/settings |
需要注意的是,這里的”限流(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里:
1 |
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。這個參數在官方文檔中找不到,不過可以動態更新:
1 |
PUT /index_name/_settings |
不過這里有待進一步測試。
當然,也可以通過提高index.merge.scheduler.max_thread_count參數來增加限流的閾值,尤其當使用SSD時:
1 |
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內存。
原文:https://xiaoz.co/2020/02/22/elasticsearch-segment-merge/