將 ELASTICSEARCH 寫入速度優化到極限
基於版本: 2.x – 5.x
在 es 的預設設定,是綜合考慮資料可靠性,搜尋實時性,寫入速度等因素的,當你離開預設設定,追求極致的寫入速度時,很多是以犧牲可靠性和搜尋實時性為代價的.有時候,業務上對兩者要求並不高,反而對寫入速度要求很高,例如在我的場景中,要求每秒200w 條的平均寫入速度,每條500位元組左右
接下來的優化基於叢集正常執行的前提下,如果是叢集首次灌入資料,可以將副本數設定為0,寫入完畢再調整回去,這樣副本分片只需要拷貝,節省了索引過程.
綜合來說,提升寫入速度從以下幾方面入手:
- 加大 translog flush ,目的是降低 iops,writeblock
- 加大 index refresh間隔, 目的除了降低 io, 更重要的降低了 segment merge 頻率
- 調整 bulk 執行緒池和佇列
- 優化磁碟間的任務均勻情況,將 shard 儘量均勻分佈到物理主機的各磁碟
- 優化節點間的任務分佈,將任務儘量均勻的發到各節點
- 優化 lucene 層建立索引的過程,目的是降低 CPU 佔用率及 IO
translog flush 間隔調整
從 es 2.x 開始, 預設設定下,translog 的持久化策略為:每個請求都flush.對應配置項為:
123 | index.translog.durability:request |
這是影響 es 寫入速度的最大因素.但是隻有這樣,寫操作才有可能是可靠的,原因參考寫入流程.
如果系統可以接受一定機率的資料丟失,調整 translog 持久化策略為週期性和一定大小的時候 flush:
123456 | index.translog.durability:asyncindex.translog.sync_interval:120sindex.translog.flush_threshold_size:1024mbindex.translog.flush_threshold_period:120m |
索引重新整理間隔調整: refresh_interval
refresh_interval
預設情況下索引的refresh_interval為1秒,這意味著資料寫1秒後就可以被搜尋到,每次索引的 refresh 會產生一個新的 lucene 段,這會導致頻繁的 segment merge 行為,如果你不需要這麼高的搜尋實時性,應該降低索引refresh 週期,如:
123 | index.refresh_interval:120s |
segment merge
segment merge 操作對系統 CPU 和 IO 佔用都比較高,從es 2.0開始,merge 行為不再由 es 控制,而是轉由 lucene 控制,因此以下配置已被刪除:
123456 | indices.store.throttle.typeindices.store.throttle.max_bytes_per_secindex.store.throttle.typeindex.store.throttle.max_bytes_per_sec |
改為以下調整開關:
1234 | index.merge.scheduler.max_thread_countindex.merge.policy.* |
最大執行緒數的預設值為:
123 | Math.max(1,Math.min(4,Runtime.getRuntime().availableProcessors()/2)) |
是一個比較理想的值,如果你只有一塊硬碟並且非 SSD, 應該把他設定為1,因為在旋轉儲存介質上併發寫,由於定址的原因,不會提升,只會降低寫入速度.
merge 策略有三種:
- tiered
- log_byete_size
- log_doc
預設情況下:
123 | index.merge.polcy.type:tiered |
索引建立時合併策略就已確定,不能更改,但是可以動態更新策略引數,一般情況下,不需要調整.如果堆疊經常有很多 merge, 可以嘗試調整以下配置:
123 | index.merge.policy.floor_segment |
該屬性用於阻止segment 的頻繁flush, 小於此值將考慮優先合併,預設為2M,可考慮適當降低此值
123 | index.merge.policy.segments_per_tier |
該屬性指定了每層分段的數量,取值約小最終segment 越少,因此需要 merge 的操作更多,可以考慮適當增加此值.預設為10,他應該大於等於 index.merge.policy.max_merge_at_once
123 | index.merge.policy.max_merged_segment |
指定了單個 segment 的最大容量,預設為5GB,可以考慮適當降低此值
Indexing Buffer
indexing buffer在為 doc 建立索引時使用,當緩衝滿時會刷入磁碟,生成一個新的 segment, 這是除refresh_interval外另外一個重新整理索引,生成新 segment 的機會. 每個 shard 有自己的 indexing buffer,下面的關於這個 buffer 大小的配置需要除以這個節點上所有的 shard 數量
123 | indices.memory.index_buffer_size |
預設為整個堆空間的10%
123 | indices.memory.min_index_buffer_size |
預設48mb
123 | indices.memory.max_index_buffer_size |
預設無限制
在大量的索引操作時,indices.memory.index_buffer_size預設設定可能不夠,這和可用堆記憶體,單節點上的 shard 數量相關,可以考慮適當增大.
bulk 執行緒池和佇列大小
建立索引的過程偏計算密集型任務,應該使用固定大小的執行緒池配置,來不及處理的放入佇列,執行緒數量配置為 CPU 核心數+1,避免過多的上下文切換.佇列大小可以適當增加.
磁碟間的任務均衡
如果你的部署方案是為path.data 配置多個路徑來使用多塊磁碟, es 在分配 shard 時,落到各磁碟上的 shard 可能並不均勻,這種不均勻可能會導致某些磁碟繁忙,利用率達到100%,這種不均勻達到一定程度可能會對寫入效能產生負面影響.
es 在處理多路徑時,優先將 shard 分配到可用空間百分比最多的磁碟,因此短時間內建立的 shard 可能被集中分配到這個磁碟,即使可用空間是99%和98%的差別.後來 es 在2.x 版本中開始解決這個問題的方式是:預估一下這個 shard 會使用的空間,從磁碟可用空間中減去這部分,直到現在6.x beta 版也是這種處理方式.但是實現也存在一些問題:
從可用空間減去預估大小
這種機制只存在於一次索引建立的過程中,下一次的索引建立,磁碟可用空間並不是上次做完減法以後的結果,這也可以理解,畢竟預估是不準的,一直減下去很快就減沒了.
但是最終的效果是,這種機制並沒有從根本上解決問題,即使沒有完美的解決方案,這種機制的效果也不夠好.
如果單一的機制不能解決所有的場景,至少應該為不同場景準備多種選擇.
為此,我們為 es 增加了兩種策略
簡單輪詢:系統初始階段,簡單輪詢的效果是最均勻的
基於可用空間的動態加權輪詢:以可用空間作為權重,在磁碟之間加權輪詢
節點間的任務均衡
為了在節點間任務儘量均衡,資料寫入客戶端應該把 bulk 請求輪詢傳送到各個節點.
當使用 java api ,或者 rest api 的 bulk 介面傳送資料時,客戶端將會輪詢的傳送的叢集節點,節點列表取決於:
當client.transport.sniff
為 true,(預設為 false),列表為所有資料節點
否則,列表為初始化客戶端物件時新增進去的節點.
java api 的 TransportClient 和 rest api 的 RestClient 都是執行緒安全的,當寫入程式自己建立執行緒池控制併發,應該使用同一個 Client 物件.在此建議使用 rest api,相容性好,只有吞吐量非常大才值得考慮序列化的開銷,顯然搜尋並不是高吞吐量的業務.
觀察bulk 請求在不同節點上的處理情況,通過cat 介面觀察 bulk 執行緒池和佇列情況,是否存在不均:
123 | _cat/thread_pool |
索引過程調整和優化
自動生成 doc ID
分析 es 寫入流程可以看到,寫入 doc 時如果是外部指定了 id,es 會先嚐試讀取原來doc的版本號, 判斷是否需要更新,使用自動生成 doc id 可以避免這個環節.
調整欄位 Mappings
欄位的 index 屬性設定為: not_analyzed,或者 no
對欄位不分詞,或者不索引,可以節省很多運算,降低 CPU 佔用.尤其是 binary 型別,預設情況下佔用 CPU 非常高,而這種型別根本不需要進行分詞做索引.
單個 doc 在建立索引時的運算複雜度,最大的因素 不在於 doc 的位元組數或者說某個欄位 value 的長度,而是欄位的數量. 例如在滿負載的寫入壓力測試中,mapping 相同的情況下,一個有10個欄位,200位元組的 doc, 通過增加某些欄位 value 的長度到500位元組,寫入 es 時速度下降很少,而如果欄位數增加到20,即使整個 doc 位元組數沒增加多少,寫入速度也會降低一倍.
使用不同的分析器:analyzer
不同的分析器在索引過程中運算複雜度也有較大的差異
調整_source 欄位
_source 欄位用於儲存 doc 原始資料,對於部分不需要儲存的欄位,可以通過 includes excludes 來過濾,或者將 _source 禁用,一般用於索引和資料分離
這樣可以降低 io 的壓力,不過實際場景大多數情況不會禁用 _source ,而即使過濾掉某些欄位,對於寫入速度的提示效果也不大,滿負荷寫入情況下,基本是CPU 先跑滿了,瓶頸在於 CPU.
禁用 _all 欄位
_all 欄位預設是開啟的,其中包含所有欄位分詞後的關鍵詞,作用是可以在搜尋的時候不指定特定欄位,從所有欄位中檢索.如果你不需要這個特性,可以禁用 _all,可以小幅的降低CPU 壓力,對速度影響並不明顯.
對於 Analyzed 的欄位禁用 Norms
Norms 用於在搜尋時計算 doc 的評分,如果不需要評分,可以禁用他:
123 | "title":{"type":"string","norms":{"enabled":false}} |
index_options 設定
index_options 用於控制在建立倒排索引過程中,哪些內容會被新增到倒排,例如 doc數量,詞頻,positions,offsets等資訊,優化這些設定可以一定程度降低索引過程中運算任務,節省 CPU 佔用率
不過實際場景中,通常很難確定業務將來會不會用到這些資訊,除非一開始方案就明確這樣設計的
方法比結論重要
當面對一個系統性問題的時候,往往是多種因素造成的,在處理叢集的寫入效能問題上,先將問題分解,在單臺上分析調整最高能力到某種系統資源達到極限,其中觀察利用率,io block,執行緒切換,堆疊狀態等,解決區域性問題,在此基礎上解決整體問題會容易很多
最後
jvm 引數除了 Xmx,Xms 其他儘量使用預設,一些看起來比較合理的引數實際效果可能適得其反。
共享下我的配置:
參考連結
https://doc.yonyoucloud.com/doc/mastering-elasticsearch/chapter-3/36_README.html
https://qbox.io/blog/maximize-guide-elasticsearch-indexing-performance-part-1
https://qbox.io/blog/maximize-guide-elasticsearch-indexing-performance-part-2
https://qbox.io/blog/maximize-guide-elasticsearch-indexing-performance-part-3