MongoDB Priamry 为何持续出现 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 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,在上述的场景不产生全表扫描,而是返回找不到记录。

作者简介

张友东,阿里云技术专家,主要关注分布式存储、NoSQL数据库等技术领域,先后参与TFS(淘宝分布式文件系统)Redis云数据库等项目,目前主要从事MongoDB云数据库的研发工作,致力于让开发者用上最好的MongoDB云服务。

发表评论