桔妹導讀:滴滴ElasticSearch平台承接了公司內部所有使用ElasticSearch的業務,包括核心搜索、RDS從庫、日志檢索、安全數據分析、指標數據分析等等。平台規模達到了3000+節點,5PB 的數據存儲,超過萬億條數據。平台寫入的峰值寫入TPS達到了2000w/s,每天近 10 億次檢索查詢。為了承接這么大的體量和豐富的使用場景,滴滴ElasticSearch需要解決穩定性、易用性、性能、成本等諸多問題。我們在4年多的時間里,做了大量優化,積攢了非常豐富的經驗。通過建設滴滴搜索平台,打造滴滴ES引擎,全方位提升用戶使用ElasticSearch體驗。這次給大家分享的是滴滴在寫入性能優化的實踐,優化后,我們將ES索引的寫入性能翻倍,結合數據冷熱分離場景,支持大規格存儲的物理機,給公司每年節省千萬左右的服務器成本。
1.背景
前段時間,為了降低用戶使用ElasticSearch的存儲成本,我們做了數據的冷熱分離。為了保持集群磁盤利用率不變,我們減少了熱節點數量。ElasticSearch集群開始出現寫入瓶頸,節點產生大量的寫入rejected,大量從kafka同步的數據出現寫入延遲。我們深入分析寫入瓶頸,找到了突破點,最終將Elasticsearch的寫入性能提升一倍以上,解決了ElasticSearch瓶頸導致的寫入延遲。這篇文章介紹了我們是如何發現寫入瓶頸,並對瓶頸進行深入分析,最終進行了創新性優化,極大的提升了寫入性能。
2.寫入瓶頸分析
2.1發現瓶頸
我們去分析這些延遲問題的時候,發現了一些不太好解釋的現象。之前做性能測試時,ES節點cpu利用率能超過80%,而生產環境延遲索引所在的節點cpu資源只使用了不到50%,集群平均cpu利用率不到40%,這時候IO和網絡帶寬也沒有壓力。通過提升寫入資源,寫入速度基本沒增加。於是我們開始一探究竟,我們選取了一個索引進行驗證,該索引使用10個ES節點。從下圖看到,寫入速度不到20w/s,10個ES節點的cpu,峰值在40-50%之間。
為了確認客戶端資源是足夠的,在客戶端不做任何調整的情況下,將索引從10個節點,擴容到16個節點,從下圖看到,寫入速度來到了30w/s左右。
這證明了瓶頸出在服務端,ES節點擴容后,性能提升,說明10個節點寫入已經達到瓶頸。但是上圖可以看到,CPU最多只到了50%,而且此時IO也沒達到瓶頸。
2.2 ES寫入模型說明
這里要先對ES寫入模型進行說明,下面分析原因會跟寫入模型有關。
客戶端一般是准備好一批數據寫入ES,這樣能極大減少寫入請求的網絡交互,使用的是ES的BULK接口,請求名為BulkRequest。這樣一批數據寫入ES的ClientNode。ClientNode對這一批數據按數據中的routing值進行分發,組裝成一批BulkShardRequest請求,發送給每個shard所在的DataNode。發送BulkShardRequest請求是異步的,但是BulkRequest請求需要等待全部BulkShardRequest響應后,再返回客戶端。
2.3 尋找原因
我們在ES ClientNode上有記錄BulkRequest寫入slowlog。
-
items
是一個BulkRequest的發送請求數 -
totalMills
是BulkRequest請求的耗時 -
max
記錄的是耗時最長的BulkShardRequest請求 -
avg
記錄的是所有BulkShardRequest請求的平均耗時。
我這里截取了部分示例。
[xxx][INFO ][o.e.m.r.RequestTracker ] [log6-clientnode-sf-5aaae-10] bulkDetail||requestId=null||size=10486923||items=7014||totalMills=2206||max=2203||avg=37
[xxx][INFO ][o.e.m.r.RequestTracker ] [log6-clientnode-sf-5aaae-10] bulkDetail||requestId=null||size=210506||items=137||totalMills=2655||max=2655||avg=218
從示例中可以看到,2條記錄的avg相比max都小了很多。一個BulkRequest請求的耗時,取決於最后一個BulkShardRequest請求的返回。這就很容易聯想到分布式系統的長尾效應。
接下來再看一個現象,我們分析了某個節點的write線程的狀態,發現節點有時候write線程全是runnable狀態,有時候又有大量在waiting。此時寫入是有瓶頸的,runnable狀態可以理解,但卻經常出現waiting狀態。所以這也能印證了CPU利用率不高。同時也論證長尾效應的存在,因為長尾節點繁忙,ClientNode在等待繁忙節點返回BulkShardRequest請求,其他節點可能出現相對空閑的狀態。下面是一個節點2個時刻的線程狀態:
時刻一:
時刻二:
2.4 瓶頸分析
谷歌大神Jeffrey Dean《The Tail At Scale》介紹了長尾效應,以及導致長尾效應的原因。總結下來,就是正常請求都很快,但是偶爾單次請求會特別慢。這樣在分布式操作時會導致長尾效應。我們從ES原理和實現中分析,造成ES單次請求特別慢的原因。發現了下面幾個因素會造成長尾問題:
2.4.1 lucene refresh
我們打開lucene引擎內部的一些日志,可以看到:
write線程是用來處理BulkShardRequest請求的,但是從截圖的日志可以看到,write線程也會會進行refresh操作。這里面的實現比較復雜,簡單說,就是ES定期會將寫入buffer的數據refresh成segment,ES為了防止refresh不過來,會在BulkShardRequest請求的時候,判斷當前shard是否有正在refresh的任務,有的話,就會幫忙一起分攤refresh壓力,這個是在write線程中進行的。這樣的問題就是造成單次BulkShardRequest請求寫入很慢。還導致長時間占用了write線程。在write queue的原因會具體介紹這種危害。
2.4.2 translog ReadWriteLock
ES的translog類似LSM-Tree的WAL log。ES實時寫入的數據都在lucene內存buffer中,所以需要依賴寫入translog保證數據的可靠性。ES translog具體實現中,在寫translog的時候會上ReadLock。在translog過期、翻滾的時候會上WriteLock。這會出現,在WriteLock期間,實時寫入會等待ReadLock,造成了BulkShardRequest請求寫入變慢。我們配置的tranlog寫入模式是async,正常開銷是非常小的,但是從圖中可以看到,寫translog偶爾可能超過100ms。
2.4.3 write queue
ES DataNode的寫入是用標准的線程池模型是,提供一批active線程,我們一般配置為跟cpu個數相同。然后會有一個write queue,我們配置為1000。DataNode接收BulkShardRequest請求,先將請求放入write queue,然后active線程有空隙的,就會從queue中獲取BulkShardRequest請求。這種模型下,當寫入active線程繁忙的時候,queue中會堆積大量的請求。這些請求在等待執行,而從ClientNode角度看,就是BulkShardRequest請求的耗時變長了。下面日志記錄了action的slowlog,其中waitTime就是請求等待執行的時間,可以看到等待時間超過了200ms。
[xxx][INFO ][o.e.m.r.RequestTracker ] [log6-datanode-sf-4f136-100] actionStats||action=indices:data/write/bulk[s][p]||requestId=546174589||taskId=6798617657||waitTime=231||totalTime=538
[xxx][INFO ][o.e.m.r.RequestTracker ] [log6-datanode-sf-4f136-100] actionStats||action=indices:data/write/bulk[s][p]||requestId=546174667||taskId=6949350415||waitTime=231||totalTime=548
2.4.4 JVM GC
ES正常一次寫入請求基本在亞毫秒級別,但是jvm的gc可能在幾十到上百毫秒,這也增加了BulkShardRequest請求的耗時。這些加重長尾現象的case,會導致一個情況就是,有的節點很繁忙,發往這個節點的請求都delay了,而其他節點卻空閑下來,這樣整體cpu就無法充分利用起來。
2.5 論證結論
長尾問題主要來自於BulkRequest的一批請求會分散寫入多個shard,其中有的shard的請求會因為上述的一些原因導致響應變慢,造成了長尾。如果每次BulkRequest只寫入一個shard,那么就不存在寫入等待的情況,這個shard返回后,ClientNode就能將結果返回給客戶端,那么就不存在長尾問題了。
我們做了一個驗證,修改客戶端SDK,在每批BulkRequest寫入的時候,都傳入相同的routing值,然后寫入相同的索引,這樣就保證了BulkRequest的一批數據,都寫入一個shard中。
優化后,第一個平穩曲線是,每個bulkRequest為10M的情況,寫入速度在56w/s左右。之后將bulkRequest改為1M(10M差不多有4000條記錄,之前寫150個shard,所以bulkSize比較大)后,性能還有進一步提升,達到了65w/s。
從驗證結果可以看到,每個bulkRequest只寫一個shard的話,性能有很大的提升,同時cpu也能充分利用起來,這符合之前單節點壓測的cpu利用率預期。
3. 性能優化
從上面的寫入瓶頸分析,我們發現了ES無法將資源用滿的原因來自於分布式的長尾問題。於是我們着重思考如何消除分布式的長尾問題。然后也在探尋其他的優化點。整體性能優化,我們分成了三個方向:
-
橫向優化,優化寫入模型,消除分布式長尾效應。
-
縱向優化,提升單節點寫入能力。
-
應用優化,探究業務節省資源的可能。
這次的性能優化,我們在這三個方向上都取得了一些突破。
3.1 優化寫入模型
寫入模型的優化思路是將一個BulkRequest請求,轉發到盡量少的shard,甚至只轉發到一個shard,來減少甚至消除分布式長尾效應。我們完成的寫入模型優化,最終能做到一個BulkRequest請求只轉發到一個shard,這樣就消除了分布式長尾效應。
寫入模型的優化分成兩個場景。一個是數據不帶routing的場景,這種場景用戶不依賴數據分布,比較容易優化的,可以做到只轉發到一個shard。另一個是數據帶了routing的場景,用戶對數據分布有依賴,針對這種場景,我們也實現了一種優化方案。
3.1.1 不帶routing場景
由於用戶對routing分布沒有依賴,ClientNode在處理BulkRequest請求中,給BulkRequest的一批請求帶上了相同的隨機routing值,而我們生成環境的場景中,一批數據是寫入一個索引中,所以這一批數據就會寫入一個物理shard中。
3.1.2 帶routing場景
下面着重介紹下我們在帶routing場景下的實現方案。這個方案,我們需要在ES Server層和ES SDK都進行優化,然后將兩者綜合使用,來達到一個BulkRequest上的一批數據寫入一個物理shard的效果。優化思路ES SDK做一次數據分發,在ES Server層做一次隨機寫入來讓一批數據寫入同一個shard。
先介紹下Server層引入的概念,我們在ES shard之上,引入了邏輯shard的概念,命名為number_of_routing_size
。ES索引的真實shard我們稱之為物理shard,命名是number_of_shards
。
物理shard必須是邏輯shard的整數倍,這樣一個邏輯shard可以映射到多個物理shard。一組邏輯shard,我們命名為slot,slot總數為number_of_shards / number_of_routing_size
。
數據在寫入ClientNode的時候,ClientNode會給BulkRequest的一批請求生成一個相同的隨機值,目的是為了讓寫入的一批數據,都能寫入相同的slot中。數據流轉如圖所示:
最終計算一條數據所在shard的公式如下:
slot = hash(random(value)) % (number_of_shards/number_of_routing_size)
shard_num = hash(_routing) % number_of_routing_size + number_of_routing_size * slot
然后我們在ES SDK層進一步優化,在BulkProcessor寫入的時候增加邏輯shard參數,在add數據的時候,可以按邏輯shard進行hash,生成多個BulkRequest。這樣發送到Server的一個BulkRequest請求,只有一個邏輯shard的數據。最終,寫入模型變為如下圖所示:
經過SDK和Server的兩層作用,一個BulkRequest中的一批請求,寫入了相同的物理shard。
這個方案對寫入是非常友好的,但是對查詢會有些影響。由於routing值是對應的是邏輯shard,一個邏輯shard要對應多個物理shard,所以用戶帶routing的查詢時,會去一個邏輯shard對應的多個物理shard中查詢。
我們針對優化的是日志寫入的場景,日志寫入場景的特征是寫多讀少,而且讀寫比例差別很大,所以在實際生產環境中,查詢的影響不是很大。
3.2 單節點寫入能力提升
單節點寫入性能提升主要有以下優化:
backport社區優化,包括下面2方面:
-
merge 社區flush優化特性:[#27000] Don't refresh on
_flush
_force_merge
and_upgrade
-
merge 社區translog優化特性,包括下面2個:
-
[#45765] Do sync before closeIntoReader when rolling generation to improve index performance
-
[#47790] sync before trimUnreferencedReaders to improve index preformance
這些特性我們在生產環境驗證下來,性能大概可以帶來18%的性能提升。
我們還做了2個可選性能優化點:
-
優化translog,支持動態開啟索引不寫translog,不寫translog的話,我們可以不再觸發translog的鎖問題,也可以緩解了IO壓力。但是這可能帶來數據丟失,所以目前我們做成動態開關,可以在需要追數據的時候臨時開啟。后續我們也在考慮跟flink團隊結合,通過flink checkpoint保證數據可靠性,就可以不依賴寫入translog。從生產環境我們驗證的情況看,在寫入壓力較大的索引上開啟不寫translog,能有10-30%不等的性能提升。
-
優化lucene寫入流程,支持在索引上配置在write線程不同步flush segment,解決前面提到長尾原因中的lucene refresh問題。在生產環境上,我們驗證下來,能有7-10%左右的性能提升。
3.2.1 業務優化
在本次進行寫入性能優化探究過程中,我們還和業務一起發現了一個優化點,業務的日志數據中存在2個很大的冗余字段(args、response),這兩個字段在日志原文中存在,還另外用了2個字段存儲,這兩個字段並沒有加索引,日志數據寫入ES時可以不從日志中解析出這2個字段,在查詢的時候直接從日志原文中解析出來。
不清洗大的冗余字段,我們驗證下來,能有20%左右的性能提升,該優化同時還帶來了10%左右存儲空間節約。
4. 生產環境性能提升結果
4.1 寫入模型優化
我們重點看下寫入模型優化的效果,下面的優化,都是在客戶端、服務端資源沒做任何調整的情況下的生產數據。
下圖所示索引開啟寫入模型優化后,寫入tps直接從50w/s,提升到120w/s。
生產環境索引寫入性能的提升比例跟索引混部情況、索引所在資源大小(長尾問題影響程度)等因素影響。從實際優化效果看,很多索引都能將寫入速度翻倍,如下圖所示:
4.2 寫入拒絕量(write rejected)下降
然后再來看一個關鍵指標,寫入拒絕量(write rejected)。ES datanode queue滿了之后就會出現rejected。
rejected異常帶來個危害,一個是個別節點出現rejected,說明寫入隊列滿了,大量請求在隊列中等待,而region內的其他節點卻可能很空閑,這就造成了cpu整體利用率上不去。
rejected異常另一個危害是造成失敗重試,這加重了寫入負擔,增加了寫入延遲的可能。
優化后,由於一個bulk請求不再分到每個shard上,而是寫入一個shard。一來減少了寫入請求,二來不再需要等待全部shard返回。
4.3 延遲情況緩解
最后再來看下寫入延遲問題。經過優化后,寫入能力得到大幅提升后,極大的緩解了當前的延遲情況。下面截取了集群優化前后的延遲情況對比。
5.總結
這次寫入性能優化,滴滴ES團隊取得了突破性進展。寫入性能提升后,我們用更少的SSD機器支撐了數據寫入,支撐了數據冷熱分離和大規格存儲物理機的落地,在這過程中,我們下線了超過400台物理機,節省了每年千萬左右的服務器成本。在整個優化過程中,我們深入分析ES寫入各個環節的耗時情況,去探尋每個耗時環節的優化點,對ES寫入細節有了更加深刻的認識。我們還在持續探尋更多的優化方式。而且我們的優化不僅在寫入性能上。在查詢的性能和穩定性,集群的元數據變更性能等等方面也都在不斷探索。我們也在持續探究如何給用戶提交高可靠、高性能、低成本、更易用的ES,未來會有更多干貨分享給大家。
團隊介紹
滴滴雲平台事業群滴滴搜索平台在開源 Elasticsearch 基礎上提供企業級的海量數據的 binlog 數倉,數據分析、日志搜索,全文檢索等場景的服務。 經過多年的技術沉淀,基於滴滴深度定制的Elasticsearch內核,打造了穩定易用,低成本、高性能的搜索服務。滴滴搜索平台除了服務滴滴內部使用Elasticsearch的全部業務,還在進行商業化輸出,已和多家公司展開商業合作。目前團隊內部有三位Elasticsearch Contributor。
作者簡介
滴滴Elasticsearch引擎負責人,負責帶領引擎團隊深入Elasticsearch內核,解決在海量規模下Elasticsearch遇到的穩定性、性能、成本方面的問題。曾在盛大、網易工作,有豐富的引擎建設經驗。
延伸閱讀
內容編輯 | Charlotte
聯系我們 | DiDiTech@didiglobal.com