在 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的操作支持很强大,尤其是对数组的支持,但在文档里使用数组时,一定得注意上述问题,避免数组的更新导致同步开销被无限放大的问题。使用数组时,尽量注意
比如上述场景,有如下的改进思路
在 MongoDB Scondary同步慢问题分析 我介绍了通过修改Secondary上重放oplog的线程数来提升备的同步能力的方法。但其实对于MongoDB的同步,并没有一种配置,能完美的解决所有同步场景,Primary上的workload不同,主备间同步的状况也会不同。
为了尽量避免出现Secondary追不上的场景,需要注意以下几点