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