MongoDB Primary 为何持续出现 oplog 全表扫描?

线上某 MongoDB 复制集实例(包含 Primary、Secondary、Hidden 3个节点 ),Primary 节点突然 IOPS 很高,调查后发现,其中 Hidden 处于 RECOVERING 状态,同时 Priamry 上持续有一全表扫描 oplog 的操作,正是这个 oplog 的 COLLSCAN 导致IO很高。

2017-10-23T17:48:01.845+0800 I COMMAND  [conn8766752] query local.oplog.rs query: { ts: { $gte: Timestamp 1505624058000|95, $lte: Timestamp 1505624058000|95 } } planSummary: COLLSCAN cursorid:20808023597 ntoreturn:0 ntoskip:0 keysExamined:0 docsExamined:44669401 keyUpdates:0 writeConflicts:0 numYields:353599 nreturned:0 reslen:20 locks:{ Global: { acquireCount: { r: 707200 } }, Database: { acquireount: { r: 353600 }, acquireWaitCount: { r: 15 }, timeAcquiringMicros: { r: 3667 } }, oplog: { acquireCount: { r: 353600 } } } 935646ms

上述问题,初步一看有2个疑问

  1. Hidden 上最新的 oplog 在 Primary 节点上是存在的,为什么 Hidden 会一直处于 RECOVERING 状态无法恢复?
  2. 同步拉取 oplog 时,会走 oplogHack 的路径,即快速根据oplog上次同步的位点定位到指点位置,这里会走一个二分查找,而不是COLLSCAN,然后从这个位点不断的tail oplog。既然有了这个优化,为什么会出现扫描所有的记录?

接下里将结合 MongoDB 同步的细节实现来分析下上述问题产生的原因。

备如何选择同步源?

MongoDB 复制集使用 oplog 来做主备同步,主将操作日志写入 oplog 集合,备从 oplog 集合不断拉取并重放,来保持主备间数据一致。MongoDB 里的 oplog 特殊集合拥有如下特性:

  1. 每条 oplog 都包含时间戳,按插入顺序递增,如果底层使用的KV存储引擎,这个时间戳将作为 oplog 在KV引擎里存储的key,可以理解为 oplog 在底层存储就是按时间戳顺序存储的,在底层能快速根据ts找位置。
  2. oplog 集合没有索引,它一般的使用模式是,备根据自己已经同步的时间戳,来定位到一个位置,然后从这个位置不断 tail query oplog。针对这种应用模式,对于 local.oplog.rs.find({ts: {$gte: lastFetechOplogTs}}) 这样的请求,会有特殊的oplogStartHack 的优化,先根据gte的查询条件在底层引擎快速找到起始位置,然后从该位置继续 COLLSCAN。
  3. oplog 是一个 capped collection,即固定大小集合(默认为磁盘大小5%),当集合满了时,会将最老插入的数据删除。

2

选择同步源,条件1:备上最新的oplog时间戳 >= 同步源上最旧的oplog时间戳

备在选择同步源时,会根据 oplog 作为依据,如果自己最新的oplog,比同步源上最老的 oplog 还有旧,比如 secondaryNewest < PrimaryOldest,则不能选择 Primary 作为同步源,因为oplog不能衔接上。如上图,Secondary1 可以选择 Primary 作为同步源,Secondary2 不能选择 Primary作为同步源,但可以选择 Secondary1 作为同步源。

如果所有节点都不满足上述条件,即认为找不到同步源,则节点会一直处于 RECOVERING 状态,并会打印 too stale to catch up -- entering maintenance mode 之类的日志,此时这个节点就只能重新全量同步了(向该节点发送 resync 命令即可)。

选择同步源,条件2:如果minvalid处于不一致状态,则minvalid里的时间戳在同步源上必须存在

local.replset.minvalid(后简称minvalid)是 MongoDB 里的一个特殊集合,用于存储节点同步的一致时间点,在备重放oplog、回滚数据的时候都会用到,正常情况下,这个集合里包含一个ts字段,跟最新的oplog时间戳一致,即 { ts: lastOplogTimestamp }

  1. 当备拉取到一批 oplog 后,假设第一条和最后一条 oplog 的时间戳分别为 firstOplogTimestamp、lastOplogTimestamp,则备在重放之前,会先把 minvalid 更新为 { ts: lastOplogTimestamp, begin: firstOplogTimestamp},加了begin字段后就说明,当前处于一个不一致的状态,等一批 oplog 全部重放完,备将 oplog 写到本地,然后更新 minvalid 为{ ts: lastOplogTimestamp},此时又达到一致的状态。
  2. 节点在ROLLBACK时,会将 minvalid 先更新为{ ts: lastOplogTimestampInSyncSource, begin: rollbackCommonPoint},标记为不一致的状态,直到继续同步后才会恢复为一致的状态。比如

     
        主节点  A B C F G H
        备节点1 A B C F G 
        备节点2 A B C D E
    
        备节点就需要回滚到 CommonPoint C,如果根据主来回滚,则minvalid会被更新为 { ts: H, begin:C}` 
    

    在选择同步源时,如果 minvalid 里包含 begin 字段,则说明它上次处于一个不一致的状态,它必须先确认 ts 字段对应的时间戳(命名为 requiredOptime)在同步源上是否存在,主要目的是:

  3. 重放时,如果重放过程异常结束,重新去同步时,必须要找包含上次异常退出时oplog范围的节点来同步
  4. ROLLBACK后选择同步源,必须选择包含ROLLBACK时参考节点对应的oplog范围的节点来同步;如上例,备节点2回滚时,它的参考节点包含了H,则在接下来选择同步源上,同步源一定要包含H才行。

为了确认 requireOptime 是否存在,备会发一个 ts: {$gte: requiredOptime, $lte: requiredOptime} 的请求来确认,这个请求会走到 oplogStartHack的路径,先走一次二分查找,如果能找到(绝大部分情况),皆大欢喜,如果找不到,就会引发一次 oplog 集合的全表扫描,如果oplog集合很大,这个开销非常大,而且会冲掉内存中的cache数据。

oplogStartHack 的本质

通过上面的分析发现,如果 requiredOptime 在同步源上不存在,会引发同步源上的一次oplog全表扫描,这个主要跟oplog hack的实现机制相关。

对于oplog的查找操作,如果其包含一个 ts: {$gte: beginTimestamp} 的条件,则 MongoDB 会走 oplogStartHack 的优化,先从引擎层获取到第一个满足查询条件的RecordId,然后把RecordId作为表扫描的参数。

  1. 如果底层引擎查找到了对应的点,oplogStartHack优化有效
  2. 如果底层引擎没有没有找到对应的点,RecordId会被设置为空值,对接下来的全表扫描不会有任何帮助。(注:个人认为,这里作为一个优化,应该将RecordId设置为Max,让接下里的全表扫描不发生。)
     if (查询满足oplogStartHack的条件) { 
        startLoc = collection->getRecordStore()->oplogStartHack(txn, goal.getValue());  // 1. 将起始值传到底层引擎,通过二分查找找到起始值对应的RecordId
     }

    // Build our collection scan...
    CollectionScanParams params;
    params.collection = collection;
    params.start = *startLoc;                               // 2. 将起始RecordId作为表扫描的参数
    params.direction = CollectionScanParams::FORWARD;
    params.tailable = cq->getParsed().isTailable();

总结

结合上述分析,当一致时间点对应的oplog在同步源上找不到时,会在同步源上触发一次oplog的全表扫描。当主备之间频繁的切换(比如线上的这个实例因为写入负载调大,主备角色切换过很多次),会导致多次ROLLBACK发生,最后出现备上minvalid里的一致时间点在同步源上找不到,引发了oplog的全表扫描;即使发生全表扫描,因为不包含minvalid的oplog,备也不能选择这个节点当同步源,最后就是一直找不到同步源,处于RECOVERING状态无法恢复,然后不断重试,不断触发主上的oplog全表扫描,恶性循环。

如何避免上述问题?

  1. 上述问题一般很难遇到,而且只有oplog集合大的时候影响才会很恶劣。
  2. 终极方法还是从代码上修复,我们已经在阿里云MongoDB云数据库里修复这个问题,并会向官方提一个PR,在上述的场景不产生全表扫描,而是返回找不到记录。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值