滴滴ElasticSearch千萬級TPS寫入效能翻倍技術剖析

滴滴技術發表於2020-08-15

桔妹導讀:滴滴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遇到的穩定性、效能、成本方面的問題。曾在盛大、網易工作,有豐富的引擎建設經驗。

延伸閱讀

  

 

相關文章