百萬級高併發mongo叢集效能數十倍提升優化實踐(上)-2019年mongodb中文社群年度一等獎

y123456yzzyz發表於2020-10-12

關於作者

前滴滴出行技術專家,現任OPPO 文件資料庫 mongodb 負責人,負責 oppo 千萬級峰值 TPS/ 十萬億級資料量文件資料庫 mongodb 研發和運維工作,一直專注於分散式快取、高效能服務端、資料庫、中介軟體等相關研發。後續持續分享《 MongoDB 核心原始碼設計、效能優化、最佳運維實踐》, Github 賬號地址 : https://github.com/y123456yz

《分散式資料庫mongodb 核心原始碼設計實現、效能優化、最佳運維實踐》專欄詳見: http://blog.itpub.net/column/150/

1. 背景

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

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

1. 軟體優化

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

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!"})

 

2.  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/manual/tutorial/expire-data/

 

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 優化前後系統負載對比

驗證方式:

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 下期分享

  下期繼續分享<<百萬級高併發mongo叢集效能數十倍提升優化實踐(上)-2019年mongodb中文社群年度一等獎>>


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

相關文章