Elasticsearch高併發寫入優化的開源協同經歷

騰訊安全平臺發表於2019-09-12

導語:在騰訊金融科技資料應用部的全民BI專案裡,我們每天面對超過10億級的資料寫入,提高es寫入效能迫在眉睫,在最近的一次優化中,有幸參與到了elasticsearch開源社群中。


背景

為了更便捷地分析資料,騰訊金融科技資料應用部去年推出了全民BI的系統。這個系統通過elasticsearch進行基礎的統計,超過10億級的資料量需要儘可能快速地匯入到es系統中。即使經過多次的引數優化,我們依然需要幾個小時才能完成匯入,這是系統此前存在的一大瓶頸。

在這樣的背景下,我們開始決定進一步深入es,尋找優化點。


優化前的準備

我們準備了1000萬的資料,並在原程式(spark程式寫入)上進行了幾輪單機壓測,得到了一些基本的效能資料。

機器配置:CPU 24核,記憶體 64G

ES基本配置:

· 堆記憶體31G

· 其他引數調整包括lock memory,translog.durability調整成async等(更詳細的策略可以參見https://github.com/elastic/elasticsearch/issues/45371)

文件數:1000萬,欄位400個(沒有text欄位)

寫入耗時:26分鐘
CPU:80%+


尋找理論值

在往下進入深水區之前,我們需要先回顧一下es本身,es本身是基於lucene基礎上設計的分散式搜尋系統,在寫入方面主要提供了:

· 事務日誌和成組提交的機制提高寫入效能並保證可靠性

· 提供schema的欄位定義(對映到lucene的欄位型別)

要進行優化,首先得驗證一個問題:lucene的極限速率能到達多少,所以我在我的本機上構建了這樣的一個測試。

Macbook pro 15,6核12執行緒
資料量1000萬,每個document 400個欄位,10個執行緒併發(考慮mac cpu Turbo 4.5G ,伺服器2.4G(24核),所以只採用10執行緒併發)
驗證寫入耗時549s(約10分鐘)。

26分鐘 —> 10分鐘,意味著理論上是可行的。那剩下的就看如何接近這個極限。因為那說明一定是es本身的一些預設特性導致了寫入速率無法提升。

下面的介紹忽略了一些相對簡單的引數調優,比如關閉docvalues,這個對於非text欄位,es預設開啟,對於不需要groupby的場景,是不必要的,這個可以減少不少效能。
經過初步的引數優化寫入耗時降低到了18分鐘,這是後面繼續往下優化的基礎。


理解es寫入的機制

es的寫入流程(主分片節點)主要有下面的幾步

· 根據文件id獲取文件版本資訊,判斷進行add或update操作

· 寫lucene:這裡只寫記憶體,會定期進行成組提交到磁碟生成新分段

· 寫translog:寫入檔案

Elasticsearch高併發寫入優化的開源協同經歷

[ translog作用 ]


除了上面的直接流程,還有三個相關的非同步流程

· 定期進行flush,對lucene進行commit

· 定期對translog進行滾動(生成新檔案),更新check point檔案

· 定期執行merge操作,合併lucene分段,這是一個比較消耗資源的操作,但預設情況下都是配置了一個執行緒。


優化第一步-引數調優

寫lucene前面已經優化過,那麼第一步的文件查詢其實是在所有分段中進行查詢,因為只提供了一個執行緒進行merge,如果merge不及時,導致分段過的,必然影響文件版本這一塊的耗時。

所以我們觀察了寫入過程中分段數的變化:

Elasticsearch高併發寫入優化的開源協同經歷

[ 寫入過程中分段的變化 ]


觀察發現,分段的增長速度比預期的快很多。按照預設配置,index_buffer=10%,堆記憶體31G的情況,按lucene的寫分段機制,平均到每個執行緒,也有125M,分段產生的速度不應該那麼快。
而這個問題的根源就是flush_threshold_size預設值只有512M ,這個參數列示在當未提交的translog日誌達到該閾值的時候進行一次刷盤操作。

Elasticsearch高併發寫入優化的開源協同經歷

[ 小分段的產生 ]


Elasticsearch高併發寫入優化的開源協同經歷

[ 調整後比較緩和的分段增長 ]


測試結果一看:18分鐘!基本沒有效果!

理論上可行的方案,為什麼卻沒有效果,帶著這個疑問繼續潛入深水區。


優化繼續-執行緒分析

這時候就需要進行堆疊分析了,多次取樣後,發現了下面的一個頻繁出現的現象:

Elasticsearch高併發寫入優化的開源協同經歷

[ 被堵塞的執行緒 ]


發現很多執行緒都停在了獲取鎖的等待上,而writeLock被rollGeneration佔用了。

寫執行緒需要獲取readLock
rollGeneration拿走了writeLock,會阻塞readLock

而在高flush_threshold_size的配置下,rollGeneration發生了300+次,每次平均耗時560ms,浪費了超過168s,而這個時間裡寫入執行緒都只能等待,小分段的優化被這個抵消了。

這裡有很多的關聯關係,lush操作和rollGeneration操作是互斥的,因為flush耗時較長(5~10秒左右),在預設flush_threshold_size配置下,rollGeneration並沒有這麼頻繁在100次左右,提高flush_threshold放大了這個問題。


初步優化方案提交

因為我們在寫入過程中使用的translog持久化策略是async,所以我很自然的想到了把寫日誌和刷盤非同步化。

Elasticsearch高併發寫入優化的開源協同經歷

[ 初版提交社群的方案 ]


一開始的方案則想引入disruptor,消除寫執行緒之間的競爭問題,後面因為es的第三方元件檢查禁止使用sun.misc.Unsafe (disruptor無鎖機制基於Unsafe實現)而放棄。
基於這個方案,測試結果終於出現了跨越:13分鐘。

初版的方案問題比較多,但是它有兩個特點:

· 足夠激進:在配置為async策略時,將底層都非同步化了

· 凸顯了原方案的問題:讓大家看到了Translog寫入的影響


Elastic創始人加入討論

沒想到的是,在社群提交幾次優化後,竟然吸引了大佬Simon Willnauer的加入。

Simon Willnauer

· elastic公司創始人之一和技術Leader

· Lucene Core Commiter and PMC Member

Simon的加入讓我們重新覆盤的整個問題。
通過對關鍵的地方增加統計資訊,我最終明確了關鍵的問題點在於FileChannel.force方法,這個操作是最耗時的一步。

sync操作會呼叫FileChannel.force,但沒有在writer的物件鎖範圍中,所以影響較小。但是因為rollGeneration在writeLock中執行,所以阻塞的影響範圍就變大了

跟社群討論後,Simon最後建議了一個折中的小技巧,就是在關閉原translog檔案之前(writeLock之外),先執行一次刷盤操作。

Elasticsearch高併發寫入優化的開源協同經歷

[ 程式碼修改 ]


這個調整的效果可以讓每次rollGeneration操作的耗時從平均570ms降低到280ms,在我的基準測試中(配置flush_threhold_size=30G,該引數僅用於單索引壓測設計,不能在生產環境使用),耗時會從18分鐘下降到15分鐘。

事實上,這並不是一個非常令人滿意的解決方案,這裡選擇這個方案主要出於兩點考慮:

1. 未來新的版本將考慮不使用Translog進行副分片的recovery,translog的滾動策略會進行調整(具體方案elasitc未透露)

2. 這個修改非常的風險非常小


提交社群

最後根據討論的最終結論,我們重新提交了PR,提交了這個改動,併合併到了主幹中。


總結和待續

下面是es寫入中的影響關係和呼叫關係圖,從圖中可以看到各個因素直接的相互影響。

Elasticsearch高併發寫入優化的開源協同經歷

[ InternalEngine中的影響關係 ]


最近提交的優化實時上只優化了rollGeneration,而實際上這裡還有一些優化空間trimUnreferenceReader,這個也在跟社群溝通中,並需要足夠的測試資料證明調整的效果,這個調整還在測試中。

而在我們目前實際應用場景中,我們通過調整下面兩個引數提高效能:

· index.translog.flush_threshold_size 預設512M,可以適當調大,但不能超過indexBufferSize*1.5倍/(可能併發寫的大索引數量),否則會觸發限流,並導致JVM記憶體不釋放!

· index.translog.generation_threshold_size(預設64M,系統支援,但官方文件沒有的引數,超過該閾值會產生新的translog檔案),要小於index.translog.flush_threshold_size,否則會影響flush,進而觸發限流機制


參考文件

1. 張超《Elasticsearch原始碼解析與優化實戰》













相關文章