百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

JAVA架構發表於2019-12-24


背景

線上某叢集峰值TPS超過100萬/秒左右(主要為寫流量,讀流量很低),峰值tps幾乎已經到達叢集上限,同時平均時延也超過100ms,隨著讀寫流量的進一步增加,時延抖動嚴重影響業務可用性。該叢集採用mongodb天然的分片模式架構,資料均衡的分佈於各個分片中,新增片鍵啟用分片功能後實現完美的負載均衡。叢集每個節點流量監控如下圖所示:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從上圖可以看出叢集流量比較大,峰值已經突破120萬/秒,其中delete過期刪除的流量不算在總流量裡面(delete由主觸發刪除,但是主上面不會顯示,只會在從節點拉取oplog的時候顯示)。如果算上主節點的delete流量,總tps超過150萬/秒。

軟體最佳化

在不增加伺服器資源的情況下,首先做了如下軟體層面的最佳化,並取得了理想的數倍效能提升:

業務層面最佳化

Mongodb配置最佳化

儲存引擎最佳化

業務層面最佳化

該叢集總文件近百億條,每條文件記錄預設儲存三天,業務隨機雜湊資料到三天後任意時間點隨機過期淘汰。由於文件數目很多,白天平峰監控可以發現從節點經常有大量delete操作,甚至部分時間點delete刪除運算元已經超過了業務方讀寫流量,因此考慮把delete過期操作放入夜間進行,過期索引新增方法如下:

Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )

上面的過期索引中expireAfterSeconds=0,代表collection集合中的文件的過期時間點在expireAt時間點過期,例如:

    db.collection.insert( {  //表示該文件在夜間凌晨1點這個時間點將會被過期刪除  "expireAt": new Date('July 22, 2019 01:00:00'),      "logEvent": 2,  "logMessage": "Success!" } )

透過隨機雜湊expireAt在三天後的凌晨任意時間點,即可規避白天高峰期觸發過期索引引入的叢集大量delete,從而降低了高峰期叢集負載,最終減少業務平均時延及抖動。

Delete過期Tips1: expireAfterSeconds含義

在expireAt指定的絕對時間點過期,也就是12.22日凌晨2:01過期

Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )db.log_events.insert( { "expireAt": new Date(Dec 22, 2019 02:01:00'),"logEvent": 2,"logMessage": "Success!"})

在expireAt指定的時間往後推遲expireAfterSeconds秒過期,也就是當前時間往後推遲60秒過期

    db.log_events.insert( {"createdAt": new Date(),"logEvent": 2,"logMessage": "Success!"} )Db.collection.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 60 } )

Delete過期Tips2: 為何mongostat只能監控到從節點有delete操作,主節點沒有?

原因是過期索引只在master主節點觸發,觸發後主節點會直接刪除呼叫對應wiredtiger儲存引擎介面做刪除操作,不會走正常的客戶端連結處理流程,因此主節點上看不到delete統計。

主節點過期delete後會生存對於的delete oplog資訊,從節點透過拉取主節點oplog然後模擬對於client回放,這樣就保證了主資料刪除的同時從資料也得以刪除,保證資料最終一致性。從節點模擬client回放過程將會走正常的client連結過程,因此會記錄delete count統計,詳見如下程式碼:

官方參考如下:

Mongodb配置最佳化(網路IO複用,網路IO和磁碟IO做分離)

由於叢集tps高,同時整點有大量推送,因此整點併發會更高,mongodb預設的一個請求一個執行緒這種模式將會嚴重影響系統負載,該預設配置不適合高併發的讀寫應用場景。官方介紹如下:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

Mongodb內部網路執行緒模型實現原理

mongodb預設網路模型架構是一個客戶端連結,mongodb會建立一個執行緒處理該連結fd的所有讀寫請求及磁碟IO操作。

Mongodb預設網路執行緒模型不適合高併發讀寫原因如下:

在高併發的情況下,瞬間就會建立大量的執行緒,例如線上的這個叢集,連線數會瞬間增加到1萬左右,也就是作業系統需要瞬間建立1萬個執行緒,這樣系統load負載就會很高。

此外,當連結請求處理完,進入流量低峰期的時候,客戶端連線池回收連結,這時候mongodb服務端就需要銷燬執行緒,這樣進一步加劇了系統負載,同時進一步增加了資料庫的抖動,特別是在PHP這種短連結業務中更加明顯,頻繁的建立執行緒銷燬執行緒造成系統高負債。 一個連結一個執行緒,該執行緒除了負責網路收發外,還負責寫資料到儲存引擎,整個網路I/O處理和磁碟I/O處理都由同一個執行緒負責,本身架構設計就是一個缺陷。

網路執行緒模型最佳化方法

為了適應高併發的讀寫場景,mongodb-3.6開始引入serviceExecutor: adaptive配置,該配置根據請求數動態調整網路執行緒數,並儘量做到網路IO複用來降低執行緒建立消耗引起的系統高負載問題。此外,加上serviceExecutor: adaptive配置後,藉助boost:asio網路模組實現網路IO複用,同時實現網路IO和磁碟IO分離。這樣高併發情況下,透過網路連結IO複用和mongodb的鎖操作來控制磁碟IO訪問執行緒數,最終降低了大量執行緒建立和消耗帶來的高系統負載,最終透過該方式提升高併發讀寫效能。

網路執行緒模型最佳化前後效能對比

在該大流量叢集中增加serviceExecutor: adaptive配置實現網路IO複用及網路IO與磁碟IO做分離後,該大流量叢集時延大幅度降低,同時系統負載和慢日誌也減少很多,具體如下:

最佳化前後系統負載對比

驗證方式:

該叢集有多個分片,其中一個分片配置最佳化後的主節點和同一時刻未最佳化配置的主節點load負載比較:

未最佳化配置的load

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

最佳化配置的load

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

最佳化前後慢日誌對比

驗證方式:

該叢集有多個分片,其中一個分片配置最佳化後的主節點和同一時刻未最佳化配置的主節點慢日誌數比較:

同一時間的慢日誌數統計:

未最佳化配置的慢日誌數 (19621)

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

最佳化配置後的慢日誌數 (5222):

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

最佳化前後平均時延對比

驗證方式:

該叢集所有節點加上網路IO複用配置後與預設配置的平均時延對比如下:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從上圖可以看出,網路IO複用後時延降低了1-2倍。

wiredtiger儲存引擎最佳化

從上一節可以看出平均時延從200ms降低到了平均80ms左右,很顯然平均時延還是很高,如何進一步提升效能降低時延?繼續分析叢集,我們發現磁碟IO一會兒為0,一會兒持續性100%,並且有跌0現象,現象如下:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從圖中可以看出,I/O寫入一次性到2G,後面幾秒鐘內I/O會持續性阻塞,讀寫I/O完全跌0,avgqu-sz、awit巨大,util次序性100%,在這個I/O跌0的過程中,業務方反應的TPS同時跌0。

此外,在大量寫入IO後很長一段時間util又持續為0%,現象如下:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

總體IO負載曲線如下:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從圖中可以看出IO很長一段時間持續為0%,然後又飆漲到100%持續很長時間,當IO util達到100%後,分析日誌發現又大量滿日誌,同時mongostat監控流量發現如下現象:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從上可以看出我們定時透過mongostat獲取某個節點的狀態的時候,經常超時,超時的時候剛好是io util=100%的時候,這時候IO跟不上客戶端寫入速度造成阻塞。

有了以上現象,我們可以確定問題是由於IO跟不上客戶端寫入速度引起,第2章我們已經做了mongodb服務層的最佳化,現在我們開始著手wiredtiger儲存引擎層面的最佳化,主要透過以下幾個方面:

cachesize調整

髒資料淘汰比例調整

checkpoint最佳化

cachesize調整最佳化(為何cacheSize越大效能越差)

於是檢視mongod.conf配置檔案,發現配置檔案中配置的cacheSizeGB: 110G,可以看出,儲存引擎中KV總量幾乎已經達到110G,按照5%髒頁開始刷盤的比例,峰值情況下cachesSize設定得越大,裡面得髒資料就會越多,而磁碟IO能力跟不上髒資料得產生速度,這種情況很可能就是造成磁碟I/O瓶頸寫滿,並引起I/O跌0的原因。

此外,檢視該機器的記憶體,可以看到記憶體總大小為190G,其中已經使用110G左右,幾乎是mongod的儲存引起佔用,這樣會造成核心態的page cache減少,大量寫入的時候核心cache不足就會引起磁碟缺頁中斷,引起大量的寫盤。

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

解決辦法:透過上面的分析問題可能是大量寫入的場景,髒資料太多容易造成一次性大量I/O寫入,於是我們可以考慮把儲存引起cacheSize調小到50G,來減少同一時刻I/O寫入的量,從而規避峰值情況下一次性大量寫入的磁碟I/O打滿阻塞問題。

儲存引擎dirty髒資料淘汰最佳化

調整cachesize大小解決了5s請求超時問題,對應告警也消失了,但是問題還是存在,5S超時消失了,1s超時問題還是偶爾會出現。

因此如何在調整cacheSize的情況下進一步規避I/O大量寫的問題成為了問題解決的關鍵,進一步分析儲存引擎原理,如何解決記憶體和I/O的平衡關係成為了問題解決的關鍵,mongodb預設儲存因為wiredtiger的cache淘汰策略相關的幾個配置如下:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

調整cacheSize從120G到50G後,如果髒資料比例達到5%,則極端情況下如果淘汰速度跟不上客戶端寫入速度,這樣還是容易引起I/O瓶頸,最終造成阻塞。

解決辦法: 如何進一步減少持續性I/O寫入,也就是如何平衡cache記憶體和磁碟I/O的關係成為問題關鍵所在。從上表中可以看出,如果髒資料及總內佔用存達到一定比例,後臺執行緒開始選擇page進行淘汰寫盤,如果髒資料及記憶體佔用比例進一步增加,那麼使用者執行緒就會開始做page淘汰,這是個非常危險的阻塞過程,造成使用者請求驗證阻塞。平衡cache和I/O的方法: 調整淘汰策略,讓後臺執行緒儘早淘汰資料,避免大量刷盤,同時降低使用者執行緒閥值,避免使用者執行緒進行page淘汰引起阻塞。最佳化調整儲存引起配置如下:

eviction_target: 75%

eviction_trigger:97%

eviction_dirty_target: %3

eviction_dirty_trigger:25%

evict.threads_min:8

evict.threads_min:12

總體思想是讓後臺evict儘量早點淘汰髒頁page到磁碟,同時調整evict淘汰執行緒數來加快髒資料淘汰,調整後mongostat及客戶端超時現象進一步緩解。

儲存引擎checkpoint最佳化調整

儲存引擎得checkpoint檢測點,實際上就是做快照,把當前儲存引擎的髒資料全部記錄到磁碟。觸發checkpoint的條件預設又兩個,觸發條件如下:

固定週期做一次checkpoint快照,預設60s

增量的redo log(也就是journal日誌)達到2G

當journal日誌達到2G或者redo log沒有達到2G並且距離上一次時間間隔達到60s,wiredtiger將會觸發checkpoint,如果在兩次checkpoint的時間間隔類evict淘汰執行緒淘汰的dirty page越少,那麼積壓的髒資料就會越多,也就是checkpoint的時候髒資料就會越多,造成checkpoint的時候大量的IO寫盤操作。如果我們把checkpoint的週期縮短,那麼兩個checkpoint期間的髒資料相應的也就會減少,磁碟IO 100%持續的時間也就會縮短。

checkpoint調整後的值如下:

checkpoint=(wait=25,log_size=1GB)

儲存引擎最佳化前後IO對比

透過上面三個方面的儲存引擎最佳化後,磁碟IO開始平均到各個不同的時間點,iostat監控最佳化後的IO負載如下:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從上面的io負載圖可以看出,之前的IO一會兒為0%,一會兒100%現象有所緩解,總結如下圖所示:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

儲存引擎最佳化前後時延對比

最佳化前後時延對比如下(注: 該叢集有幾個業務同時使用,最佳化前後時延對比如下):

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從上圖可以看出,儲存引擎最佳化後時間延遲進一步降低並趨於平穩,從平均80ms到平均20ms左右,但是還是不完美,有抖動。

伺服器系統磁碟IO問題解決

伺服器IO硬體問題背景

如第3節所述,當wiredtiger大量淘汰資料後,發現只要每秒磁碟寫入量超過500M/s,接下來的幾秒鐘內util就會持續100%,w/s幾乎跌0,於是開始懷疑磁碟硬體存在缺陷。

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從上圖可以看出磁碟為nvMe的ssd盤,檢視相關資料可以看出該盤IO效能很好,支援每秒2G寫入,iops能達到2.5W/S,而我們線上的盤只能每秒寫入最多500M。

伺服器IO硬體問題解決後效能對比

於是考慮把該分片叢集的主節點全部遷移到另一款伺服器,該伺服器也是ssd盤,io效能達到2G/s寫入(注意:只遷移了主節點,從節點還是在之前的IO-500M/s的伺服器)。 遷移完成後,發現效能得到了進一步提升,時延遲降低到2-4ms/s,三個不同業務層面看到的時延監控如下圖所示:

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)
百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)

從上圖時延可以看出,遷移主節點到IO能力更好的機器後,時延進一步降低到平均2-4ms。

雖然時延降低到了平均2-4ms,但是還是有很多幾十ms的尖刺,鑑於篇幅將在下一期分享大家原因,最終儲存所有時延控制在5ms以內,並消除幾十ms的尖刺。

此外,nvme的ssd io瓶頸問題原因,經過和廠商確認分析,最終定位到是linux核心版本不匹配引起,如果大家nvme ssd盤有同樣問題,記得升級linux版本到3.10.0-957.27.2.el7.x86_64版本,升級後nvme ssd的IO能力達到2G/s以上寫入。

總結及遺留問題

透過mongodb服務層配置最佳化、儲存引擎最佳化、硬體IO提升三方面的最佳化後,該大流量寫入叢集的平均時延從之前的平均數百ms降低到了平均2-4ms,整體效能提升數十倍,效果明顯。

但是,從4.2章節最佳化後的時延可以看出,叢集偶爾還是會有抖動,鑑於篇幅,下期會分享如果消除4.2章節中的時延抖動,最終保持時間完全延遲控制在2-4ms,並且無任何超過10ms的抖動,敬請期待,下篇會更加精彩。

此外,在叢集最佳化過程中採了一些坑,下期會繼續分析大流量叢集採坑記。

注意: 文章中的一些最佳化方法並不是一定適用於所有mongodb場景,請根據實際業務場景和硬體資源能力進行最佳化,而不是按部就班。

粉絲福利:以下資源加架構㪊:705--127--209 領取資料,裡面會分享一些資深架構師錄製的影片錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能最佳化這些成為架構師必備的資料

百萬級高併發mongodb叢集效能數十倍提升最佳化實踐(上篇)


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912582/viewspace-2670365/,如需轉載,請註明出處,否則將追究法律責任。

相關文章