MongoDB Secondary同步慢問題分析(續)
在 MongoDB Scondary同步慢問題分析 文中介紹了因Primary上寫入qps過大,導致Secondary節點的同步無法追上的問題,本文再分享一個case,因oplog的寫入被放大,導致同步追不上的問題。
MongoDB用于同步的 oplog 具有一個重要的『冪等』特性,也就是說,一條oplog在備上重放多次,得到的結果跟重放一次結果是一樣的,這個特性簡化了同步的實現,Secondary不需要有專門的邏輯去保證一條oplog在備上『必須僅能重放』一次。
為了保證冪等性,記錄oplog時,通常需要對寫入的請求做一下轉換,舉個例子,某文檔x字段當前值為100,用戶向Primary發送一條 {$inc: {x: 1}} ,記錄oplog時會轉化為一條 {$set: {x: 101} 的操作,才能保證冪等性。
冪等性的代價
簡單元素的操作, $inc 轉化為 $set 并沒有什么影響,執行開銷上也差不多,但當遇到數組元素操作時,情況就不一樣了。
當前文檔內容
mongo-9551:PRIMARY> db.coll.find() { "_id" : 1, "x" : [ 1, 2, 3 ] }
在數組尾部push 2個元素,查看oplog發現$push操作被轉換為了$set操作(設置數組指定位置的元素為某個值)。
mongo-9551:PRIMARY> db.coll.update({_id: 1}, {$push: {x: { $each: [4, 5] }}}) WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) mongo-9551:PRIMARY> db.coll.find() { "_id" : 1, "x" : [ 1, 2, 3, 4, 5 ] } mongo-9551:PRIMARY> use local switched to db local mongo-9551:PRIMARY> db.oplog.rs.find().sort({$natural: -1}).limit(1) { "ts" : Timestamp(1464081601, 1), "h" : NumberLong("7793405363406192063"), "v" : 2, "op" : "u", "ns" : "test.coll", "o2" : { "_id" : 1 }, "o" : { "$set" : { "x.3" : 4, "x.4" : 5 } } }
$push 轉換為 帶具體位置的$set 開銷上也差不多,但接下來再看看往數組的頭部添加2個元素
mongo-9551:PRIMARY> db.coll.update({_id: 1}, {$push: {x: { $each: [6, 7], $position: 0 }}}) WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) mongo-9551:PRIMARY> db.coll.find() { "_id" : 1, "x" : [ 6, 7, 1, 2, 3, 4, 5 ] } mongo-9551:PRIMARY> use local switched to db local mongo-9551:PRIMARY> db.oplog.rs.find().sort({$natural: -1}).limit(1) { "ts" : Timestamp(1464082056, 1), "h" : NumberLong("6563273714951530720"), "v" : 2, "op" : "u", "ns" : "test.coll", "o2" : { "_id" : 1 }, "o" : { "$set" : { "x" : [ 6, 7, 1, 2, 3, 4, 5 ] } } }
可以發現,當向數組的頭部添加元素時,oplog里的$set操作不再是設置數組某個位置的值(因為基本所有的元素位置都調整了),而是$set數組最終的結果,即 整個數組的內容都要寫入oplog 。當push操作指定了$slice或者$sort參數時,oplog的記錄方式也是一樣的,會將整個數組的內容作為$set的參數。
$pull, $addToSet等更新操作符也是類似,更新數組后,oplog里會轉換成 $set數組的最終內容 ,才能保證冪等性。
案例分析
當數組非常大時,對數組的一個小更新,可能就需要把整個數組的內容記錄到oplog里,我們遇到一個實際的生產環境案例,用戶的文檔內包含一個很大的數組字段,1000個元素總大小在64KB左右,這個數組里的元素按時間反序存儲,新插入的元素會放到數組的最前面($position: 0),然后保留數組的前1000個元素($slice: 1000)。
上述場景導致,Primary上的每次往數組里插入一個新元素(請求大概幾百字節),oplog里就要記錄整個數組的內容,Secondary同步時會拉取oplog并重放,『Primary到Secondary同步oplog』的流量是『客戶端到Primary網絡流量』的上百倍,導致主備間網卡流量跑滿,而且由于oplog的量太大,舊的內容很快被刪除掉,最終導致Secondary追不上,轉換為RECOVERING狀態。
MongoDB對json的操作支持很強大,尤其是對數組的支持,但在文檔里使用數組時,一定得注意上述問題,避免數組的更新導致同步開銷被無限放大的問題。使用數組時,盡量注意
- 數組的元素個數不要太多,總的大小也不要太大
- 盡量避免對數組進行更新操作
- 如果一定要更新,盡量只在尾部插入元素,復雜的邏輯可以考慮在業務層面上來支持
比如上述場景,有如下的改進思路
- 將數組的內容放到單獨的集合存儲,將數組的操作轉化為對集合的操作(capped collection能很好的支持$slice的功能)
- 如果一定要用數組,插入數組元素時,直接放到尾部,讓記錄就是按時間戳升序存儲,在使用時反向遍歷({$natural: -1})取最新的元素。保持最近1000條的功能,則可在業務邏輯里實現掉,比如增加后臺任務來檢測,當數組元素超過某個閾值如2000時,就將數組截斷到1000條。
再說同步
在 MongoDB Scondary同步慢問題分析 我介紹了通過修改Secondary上重放oplog的線程數來提升備的同步能力的方法。但其實對于MongoDB的同步,并沒有一種配置,能完美的解決所有同步場景,Primary上的workload不同,主備間同步的狀況也會不同。
為了盡量避免出現Secondary追不上的場景,需要注意以下幾點
- 保證Primary節點有充足的服務能力,如果用戶的請求就能把Primary的資源跑得很滿,那么勢必會影響到主備同步。
- 合理配置oplog的大小,可以結合寫入的情況,預估下oplog的大小,比如oplog能存儲一天的寫入量,這樣即使備同步慢、故障、或者臨時下線維護等,只要不超過1天,恢復后還是有希望繼續同步的。
- 盡量避免復雜的數組更新操作,盡量避免慢更新(比如更新的查詢條件需要遍歷整個集合)
參考資料
來自: http://blog.yunnotes.net/index.php/mongodb-secondary-sync/