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

導語:在騰訊金融科技數據應用部的全民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:寫入文件

在這裏插入圖片描述
[ translog作用 ]
除了上面的直接流程,還有三個相關的異步流程
· 定期進行flush,對lucene進行commit
· 定期對translog進行滾動(生成新文件),更新check point文件
· 定期執行merge操作,合併lucene分段,這是一個比較消耗資源的操作,但默認情況下都是配置了一個線程。

優化第一步-參數調優
寫lucene前面已經優化過,那麼第一步的文檔查找其實是在所有分段中進行查找,因爲只提供了一個線程進行merge,如果merge不及時,導致分段過的,必然影響文檔版本這一塊的耗時。
所以我們觀察了寫入過程中分段數的變化:
在這裏插入圖片描述
[ 寫入過程中分段的變化 ]

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

在這裏插入圖片描述
[ 小分段的產生 ]
在這裏插入圖片描述
[ 調整後比較緩和的分段增長 ]

測試結果一看:18分鐘!基本沒有效果!
理論上可行的方案,爲什麼卻沒有效果,帶着這個疑問繼續潛入深水區。

優化繼續-線程分析
這時候就需要進行堆棧分析了,多次取樣後,發現了下面的一個頻繁出現的現象:
在這裏插入圖片描述
[ 被堵塞的線程 ]

發現很多線程都停在了獲取鎖的等待上,而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,所以我很自然的想到了把寫日誌和刷盤異步化。
在這裏插入圖片描述
[ 初版提交社區的方案 ]

一開始的方案則想引入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之外),先執行一次刷盤操作。
在這裏插入圖片描述
[ 代碼修改 ]

這個調整的效果可以讓每次rollGeneration操作的耗時從平均570ms降低到280ms,在我的基準測試中(配置flush_threhold_size=30G,該參數僅用於單索引壓測設計,不能在生產環境使用),耗時會從18分鐘下降到15分鐘。
事實上,這並不是一個非常令人滿意的解決方案,這裏選擇這個方案主要出於兩點考慮:
1.未來新的版本將考慮不使用Translog進行副分片的recovery,translog的滾動策略會進行調整(具體方案elasitc未透露)
2.這個修改非常的風險非常小

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

總結和待續
下面是es寫入中的影響關係和調用關係圖,從圖中可以看到各個因素直接的相互影響。

在這裏插入圖片描述
[ 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源碼解析與優化實戰》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章