OPPO百萬級高併發MongoDB集羣性能數十倍提高優化實踐(上)

本文來自OPPO互聯網技術團隊,轉載請註名做者。同時歡迎關注OPPO互聯網技術團隊的公衆號:OPPO_tech,與你分享OPPO前沿互聯網技術及活動。linux

1. 背景

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

從上圖能夠看出集羣流量比較大,峯值已經突破120萬/秒,其中delete過時刪除的流量不算在總流量裏面(delete由主觸發刪除,可是主上面不會顯示,只會在從節點拉取oplog的時候顯示)。若是算上主節點的delete流量,總tps超過150萬/秒。mongodb

2. 軟件優化

在不增長服務器資源的狀況下,首先作了以下軟件層面的優化,並取得了理想的數倍性能提高:數據庫

  1. 業務層面優化
  2. Mongodb配置優化
  3. 存儲引擎優化

2.1 業務層面優化

該集羣總文檔近百億條,每條文檔記錄默認保存三天,業務隨機散列數據到三天後任意時間點隨機過時淘汰。因爲文檔數目不少,白天平峯監控能夠發現從節點常常有大量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 含義架構

  1. 在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!"})
  1. 在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統計。

官方參考以下:https://docs.mongodb.com/manu...

2.2 Mongodb配置優化(網絡IO複用,網絡IO和磁盤IO作分離)

因爲集羣tps高,同時整點有大量推送,所以整點併發會更高,mongodb默認的一個請求一個線程這種模式將會嚴重影響系統負載,該默認配置不適合高併發的讀寫應用場景。官方介紹以下:

2.2.1 Mongodb內部網絡線程模型實現原理

mongodb默認網絡模型架構是一個客戶端連接,mongodb會建立一個線程處理該連接fd的全部讀寫請求及磁盤IO操做。

Mongodb默認網絡線程模型不適合高併發讀寫緣由以下:

  1. 在高併發的狀況下,瞬間就會建立大量的線程,例如線上的這個集羣,鏈接數會瞬間增長到1萬左右,也就是操做系統須要瞬間建立1萬個線程,這樣系統load負載就會很高。
  2. 此外,當連接請求處理完,進入流量低峯期的時候,客戶端鏈接池回收連接,這時候mongodb服務端就須要銷燬線程,這樣進一步加重了系統負載,同時進一步增長了數據庫的抖動,特別是在PHP這種短連接業務中更加明顯,頻繁的建立線程銷燬線程形成系統高負債。
  3. 一個連接一個線程,該線程除了負責網絡收發外,還負責寫數據到存儲引擎,整個網絡 I/O 處理和磁盤 I/O 處理都由同一個線程負責,自己架構設計就是一個缺陷。
2.2.2 網絡線程模型優化方法

爲了適應高併發的讀寫場景,mongodb-3.6開始引入serviceExecutor: adaptive配置,該配置根據請求數動態調整網絡線程數,並儘可能作到網絡IO複用來下降線程建立消耗引發的系統高負載問題。

此外,加上serviceExecutor: adaptive配置後,藉助boost:asio網絡模塊實現網絡IO複用,同時實現網絡IO和磁盤IO分離。這樣高併發狀況下,經過網絡連接IO複用和mongodb的鎖操做來控制磁盤IO訪問線程數,最終下降了大量線程建立和消耗帶來的高系統負載,最終經過該方式提高高併發讀寫性能。

2.2.3 網絡線程模型優化先後性能對比

在該大流量集羣中增長serviceExecutor: adaptive配置實現網絡IO複用及網絡IO與磁盤IO作分離後,該大流量集羣時延大幅度下降,同時系統負載和慢日誌也減小不少,具體以下:

2.2.3.1 優化先後系統負載對比

驗證方式

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

未優化配置的load

優化配置的load

2.2.3.2 優化先後慢日誌對比

驗證方式

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

同一時間的慢日誌數統計

未優化配置的慢日誌數(19621):

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

2.2.3.3 優化先後平均時延對比

驗證方式

該集羣全部節點加上網絡IO複用配置後與默認配置的平均時延對好比下:

從上圖能夠看出,網絡IO複用後時延下降了1-2倍。

2.3 wiredtiger存儲引擎優化

從上一節能夠看出平均時延從200ms下降到了平均80ms左右,很顯然平均時延仍是很高,如何進一步提高性能下降時延?繼續分析集羣,咱們發現磁盤IO一下子爲0,一下子持續性100%,而且有跌0現象,現象以下:

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

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

整體IO負載曲線以下:

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

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

有了以上現象,咱們能夠肯定問題是因爲IO跟不上客戶端寫入速度引發,第2章咱們已經作了mongodb服務層的優化,如今咱們開始着手wiredtiger存儲引擎層面的優化,主要經過如下幾個方面:

  1. cachesize調整
  2. 髒數據淘汰比例調整
  3. checkpoint優化
2.3.1 cachesize調整優化(爲什麼cacheSize越大性能越差)

前面的IO分析能夠看出,超時時間點和I/O阻塞跌0的時間點一致,所以如何解決I/O跌0成爲了解決改問題的關鍵所在。

找個集羣平峯期(總tps50萬/s)查看當時該節點的TPS,發現TPS不是很高,單個分片也就3-4萬左右,爲什麼會有大量的刷盤,瞬間可以達到10G/S,形成IO util持續性跌0(由於IO跟不上寫入速度)。繼續分析wiredtiger存儲引擎刷盤實現原理,wiredtiger存儲引擎是一種B+樹存儲引擎,mongodb文檔首先轉換爲KV寫入wiredtiger,在寫入過程當中,內存會愈來愈大,當內存中髒數據和內存總佔用率達到必定比例,就開始刷盤。同時當達到checkpoint限制也會觸發刷盤操做,查看任意一個mongod節點進程狀態,發現消耗的內存過多,達到110G,以下圖所示:

因而查看mongod.conf配置文件,發現配置文件中配置的cacheSizeGB: 110G,能夠看出,存儲引擎中KV總量幾乎已經達到110G,按照5%髒頁開始刷盤的比例,峯值狀況下cachesSize設置得越大,裏面得髒數據就會越多,而磁盤IO能力跟不上髒數據得產生速度,這種狀況極可能就是形成磁盤I/O瓶頸寫滿,並引發I/O跌0的緣由。

此外,查看該機器的內存,能夠看到內存總大小爲190G,其中已經使用110G左右,幾乎是mongod的存儲引發佔用,這樣會形成內核態的page cache減小,大量寫入的時候內核cache不足就會引發磁盤缺頁中斷。

解決辦法:經過上面的分析問題多是大量寫入的場景,髒數據太多容易形成一次性大量I/O寫入,因而咱們能夠考慮把存儲引發cacheSize調小到50G,來減小同一時刻I/O寫入的量,從而規避峯值狀況下一次性大量寫入的磁盤I/O打滿阻塞問題。

2.3.2 存儲引擎dirty髒數據淘汰優化

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

所以如何在調整cacheSize的狀況下進一步規避I/O大量寫的問題成爲了問題解決的關鍵,進一步分析存儲引擎原理,如何解決內存和I/O的平衡關係成爲了問題解決的關鍵,mongodb默認存儲由於wiredtiger的cache淘汰策略相關的幾個配置以下:

wiredtiger淘汰相關配置 默認值 工做原理
eviction_target 80 當用掉的內存超過總內存的百分比達到eviction_target,後臺evict線程開始淘汰
eviction_trigger 95 當用掉的內存超過總內存的 eviction_trigger,用戶線程也開始淘汰
eviction_dirty_target 5 當cache中髒數據比例超過eviction_dirty_target,後臺evict線程開始淘汰
eviction_dirty_trigger 20 當cache中髒數據比例超過eviction_dirty_trigger, 用戶線程也開始淘汰
evict.threads_min 4 後臺evict線程最小數
evict.threads_max 4 後臺evict線程最大數

調整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及客戶端超時現象進一步緩解。

2.3.3 存儲引擎checkpoint優化調整

存儲引擎得checkpoint檢測點,實際上就是作快照,把當前存儲引擎的髒數據所有記錄到磁盤。觸發checkpoint的條件默認又兩個,觸發條件以下:

  1. 固定週期作一次checkpoint快照,默認60s
  2. 增量的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)
2.3.4 存儲引擎優化先後IO對比

經過上面三個方面的存儲引擎優化後,磁盤IO開始平均到各個不一樣的時間點,iostat監控優化後的IO負載以下:

從上面的IO負載圖能夠看出,以前的IO一下子爲0%,一下子100%現象有所緩解,總結以下圖所示:

2.3.5 存儲引擎優化先後時延對比

優化先後時延對好比下(注: 該集羣有幾個業務同時使用,優化先後時延對好比下):

從上圖能夠看出,存儲引擎優化後時間延遲進一步下降並趨於平穩,從平均80ms到平均20ms左右,可是仍是不完美,有抖動。

3. 服務器系統磁盤IO問題解決

3.1 服務器IO硬件問題背景

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

從上圖能夠看出磁盤爲nvMe的ssd盤,查看相關數據能夠看出該盤IO性能很好,支持每秒2G寫入,iops能達到2.5W/S,而咱們線上的盤只能每秒寫入最多500M。

3.2 服務器IO硬件問題解決後性能對比

因而考慮把該分片集羣的主節點所有遷移到另外一款服務器,該服務器也是ssd盤,io性能達到2G/s寫入(注意:只遷移了主節點,從節點仍是在以前的IO-500M/s的服務器)。遷移完成後,發現性能獲得了進一步提高,時延遲下降到2-4ms/s,三個不一樣業務層面看到的時延監控以下圖所示:

從上圖時延能夠看出,遷移主節點到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以上寫入。

4. 總結及遺留問題

經過mongodb服務層配置優化、存儲引擎優化、硬件IO提高三方面的優化後,該大流量寫入集羣的平均時延從以前的平均數百ms下降到了平均2-4ms,總體性能提高數十倍,效果明顯。

可是,從4.2章節優化後的時延能夠看出,集羣偶爾仍是會有抖動,鑑於篇幅,下期會分享若是消除4.2章節中的時延抖動,最終保持時間徹底延遲控制在2-4ms,而且無任何超過10ms的抖動,敬請期待,下篇會更加精彩。

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

注意:文章中的一些優化方法並非必定適用於全部mongodb場景,請根據實際業務場景和硬件資源能力進行優化,而不是循序漸進。

5. 最後……

咱們近期還將繼續分享以下主題,敬請關注:

  1. 百萬級高併發MongoDB集羣性能數十倍提高原理(下)
  2. 百萬計高併發MongoDB集羣性能優化採坑記
  3. 線上典型集羣抖動、不可用等問題彙總分析
  4. MongoDB文檔數據庫業務使用最佳案例分享

最後的最後,順便打一個廣告,OPPO互聯網運維雲存儲團隊急缺多個崗位

若是對MongoDB內核源碼、Wiredtiger存儲引擎、RocksDB存儲引擎、數據庫機房多活、數據鏈路同步系統、中間件、數據庫等有興趣的同窗。歡迎加入OPPO你們庭,一塊兒參與OPPO百萬級高併發文檔數據庫研發。

工做地點:成都 / 深圳

郵箱:yangyazhou#oppo.com

相關文章
相關標籤/搜索