問題背景
最近生產環境出現多次Primary寫入QPS太高,導致Seconary的同步無法跟上的問題(Secondary上的最新oplog時間戳比Primary上最舊oplog時間戳小),使得Secondary變成RECOVERING狀態,這時需要人工介入處理,向Secondary發送resync命令,讓Secondary重新全量同步一次。
同步過程
下圖是MongoDB數據同步的流程
Primary上的寫入會記錄oplog,存儲到一個固定大小的 capped collection 裏,Secondary主動從Primary上拉取oplog並重放應用到自身,以保持數據與Primary節點上一致。
initial sync
新節點加入(或者主動向Secondary發送resync)時,Secondary會先進行一次initial sync,即全量同步,遍歷Primary上的所有DB的所有集合,將數據拷貝到自身節點,然後讀取『全量同步開始到結束時間段內』的oplog並重放。全量同步不是本文討論的重點,將不作過多的介紹。
tailing oplog
全量同步結束後,Secondary就開始從結束時間點建立tailable cursor,不斷的從同步源拉取oplog並重放應用到自身,這個過程並不是由一個線程來完成的,mongodb爲了提升同步效率,將拉取oplog以及重放oplog分到了不同的線程來執行。
- producer thread,這個線程不斷的從同步源上拉取oplog,並加入到一個BlockQueue的隊列裏保存着,BlockQueue最大存儲240MB的oplog數據,當超過這個閾值時,就必須等到oplog被replBatcher消費掉才能繼續拉取。
- replBatcher thread,這個線程負責逐個從producer thread的隊列裏取出oplog,並放到自己維護的隊列裏,這個隊列最多允許5000個元素,並且元素總大小不超過512MB,當隊列滿了時,就需要等待oplogApplication消費掉。
- oplogApplication會取出replBatch thread當前隊列的所有元素,並將元素根據docId(如果存儲引擎不支持文檔鎖,則根據集合名稱)分散到不同的replWriter線程,replWriter線程將所有的oplog應用到自身;等待所有oplog都應用完畢,oplogApplication線程將所有的oplog順序寫入到local.oplog.rs集合。
producer的buffer和apply線程的統計信息都可以通過db.serverStatus().metrics.repl來查詢到,在測試過程中,向Primary模擬約10000 qps的寫入,觀察Secondary上的同步,寫入速率遠小於Primary,大致只有3000左右的qps,同時觀察到 producer的buffer很快就達到飽和,可以判斷出oplog重放的線程跟不上 。
默認情況下,Secondary採用16個replWriter線程來重放oplog,可通過啓動時設置replWriterThreadCount參數來定製線程數,當提升線程數到32時,同步的情況大大改觀,主備寫入的qps基本持平,主備上數據同步的延時控制在1s以內,進一步驗證了上述結論。
改進思路
如果因Primary上的寫入qps很高,經常出現Secondary同步無法追上的問題,可以考慮以下改進思路
- 配置更高的replWriterThreadCount,Secondary上加速oplog重放,代價是更高的內存開銷
- 使用更大的oplog,可按照官方教程 修改oplog的大小 , 阿里雲MongoDB數據庫 增加了patch,能做到在線修改oplog的大小。
- 將writeOpsToOplog步驟分散到多個replWriter線程來併發執行,這個是官方目前在考慮的策略之一,參考 Secondaries unable to keep up with primary under WiredTiger
參考資料
capped collection
SERVER-18908
修改oplog的大小
阿里雲MongoDB數據庫
來自: http://blog.yunnotes.net/index.php/mongodb-scondary-cannot-catchup/