一、問題分析
最近公司的es插入/更新性能大幅度下降,單日數據(70w)刷入從原來10min+,變成了現在的解決3h。插入效率從1k-2k條/s,到現在100-200條/s。
總結了下問題的原因,有以下幾點:
- 堆內存不足
- segment數量過多導致內存吃緊
- 業務線程阻塞在BulkProcessor對象
二、問題優化方案
1、堆內存不足問題
如下圖可看出,堆內存已經非常吃緊。Index Memory/segment這些都會吃掉我們大量內存。3g堆內存已經不能滿足業務需求,只能充錢擴容了。
2、segment數量過多導致內存吃緊
-
更新數據導致索引變大
我們的業務是一個月一個索引,默認5個分片。每個一段時間會對幾個月的數據進行重刷,重刷完后改動幾率很低,基本只供查詢。另外我對數據設置了唯一ID,沒有用自動生成對ID。按道理,相同id對數據進行覆蓋,重刷后索引大小不應該發生變化。但是每次重刷完后,索引大小都會變大幾個G。這是什么原因導致對呢?
這里用mysql的索引和es的segment索引進行對比,mysql的索引在做了更新操作只會,會重構索引樹。對於大量數據,這個操作是非常耗時的。我們看看es官方文檔對於索引更新的介紹:
es對於更新和刪除操作,不會重構原來的索引,這樣會非常耗時,不夠快。怎么最快?把原來的數據標記為刪除,新建索引。
(地址:https://www.elastic.co/guide/cn/elasticsearch/guide/current/dynamic-indices.html)
對於刪除和更新:
段是不可改變的,所以既不能從把文檔從舊的段中移除,也不能修改舊的段來進行反映文檔的更新。 取而代之的是,每個提交點會包含一個 .del 文件,文件中會列出這些被刪除文檔的段信息。
當一個文檔被 “刪除” 時,它實際上只是在 .del 文件中被 標記 刪除。一個被標記刪除的文檔仍然可以被查詢匹配到, 但它會在最終結果被返回前從結果集中移除。
文檔更新也是類似的操作方式:當一個文檔被更新時,舊版本文檔被標記刪除,文檔的新版本被索引到一個新的段中。 可能兩個版本的文檔都會被一個查詢匹配到,但被刪除的那個舊版本文檔在結果集返回前就已經被移除。
在 “段合並” , 我們展示了一個被刪除的文檔是怎樣被文件系統移除的。
總結:更新操作導致索引變大的原因是因為舊的數據實際上並沒有被刪除,要刪除舊的doc,只能通過“段合並”的方式。
-
segment數目太多,需要合並
段數目太多會帶來較大的麻煩。 每一個段都會消耗文件句柄、內存和cpu運行周期。更重要的是,每個搜索請求都必須輪流檢查每個段;所以段越多,搜索也就越慢。"段合並"操作應用於不常更新的索引。
對不常更新的索引進行"段合並",每個分區合並為一個段。合並的原理如圖:
基於5.X版本的段合並:
(1)獲取目前索引各個分片的段大小和內存占用情況
GET /_cat/segments/imy-index-202003?v&h=shard,segment,size,size.memory
(這是測試環境的情況,段數及其占用內存都比較小)
(2)進行段合並,每個分區合並為一個段
POST /my-index-202003/_forcemerge?max_num_segments=1
max_num_segments:各分區合並后的段數
(3)段合並后的情況
3、業務線程阻塞在BulkProcessor對象優化
業務場景:10個線程並發執行,共享9個type的BulkProcessor(每個type一個BulkProcessor對象);
用jconsole命令查看線程運行情況,發現線程阻塞在BulkProcessor對象的獲取。刷一天數據,單個線程的阻塞數去到幾千。
原因分析:
(1)es未進行擴容前,內存吃緊。寫入es非常耗時,導致BulkProcessor提交數據非常耗時(BulkProcessor默認累計1000個doc或者數據達到5m就會觸發提交)。鎖住了BulkProcessor對象,影響了業務線程調用BulkProcessor對象的add()方法。
(2)BulkProcessor類是允許多線程提交的,通過設置concurrentRequests參數(默認:1),這個參數代表並發數,用於創建信號量。經查看,原來是公司組件設置了1個並發,emmm。
優化:
(1)修改基礎組建,提高並發數。
(2)如何在並發情況下,不讓業務線程阻塞在BulkProcessor.add()呢?(如何讓業務線程可以繼續執行業務操作,不受鎖的影響)
借鑒BulkProcessor對象的1000個doc/5m數據才觸發提交的思想。用空間換時間,在業務線程和BulkProcessor間新增一個阻塞隊列(緩存作用),用於存放doc。
-
- n個線程負責計算業務,生成doc對象,添加到阻塞隊列;
- n個線程負責從阻塞隊列讀取數據,並把數據往BulkProcessor.add()。
這樣就可以避免業務線程因為調用BulkProcessor.add()被Blocked的情況。