广告位 · 页头(后台-模块管理-新建模块后替换此区域)

MongoDB故障码2350:过期数据未被及时清理如何排查?

在MongoDB中,TTL索引通过后台的TTL监视器删除过期文档。这个过程不是实时的,默认每60秒扫描一次,并且删除动作本身是异步、分批进行的。当业务同时使用条件删除命令清理过期数据时,可能会在驱动层看到错误码2350。这个错误码在多数场景下表示No matching document found,意思是删除条件没有命中任何文档,它并不等价于MongoDB存储故障或索引损坏。真正需要分辨的是:过期数据到底是没有被及时删除,还是已经被删除但业务仍然按旧路径重复删除。

MongoDB故障码2350:过期数据未被及时清理如何排查?

故障码2350是如何被触发的

错误码2350常见于findOneAndDelete、findAndModify这类既查询又删除的命令。服务端先根据查询条件寻找文档,如果找不到匹配项,在部分驱动或旧版本错误处理路径中,就会抛出带有code=2350的异常。以会话表为例,文档结构通常包含expireAt字段,并建立了TTL索引。应用清理任务执行如下条件删除命令:

db.sessions.findOneAndDelete({ _id: "abc123", expireAt: { $lte: new Date() } })

如果该文档已经被TTL监视器删除,或者前一次请求已经删除,那么上述命令不会命中任何文档,就可能产生2350。在MongoDB shell中,未命中时通常返回null,但如果驱动将服务端No matching document found包装成异常,就会带出2350。此时业务层如果把2350当作致命错误告警,会造成大量误报。合理的做法是将这种结果归为幂等成功,因为目标数据本来就已经不存在。真正要关注的是为什么TTL清理出现延迟,以及为什么业务删除和TTL删除会同时存在。

还需要注意,使用findOneAndDelete时如果只是普通查询并删除,不指定过期时间条件,也可能在文档已经自然删除后出现2350。因此排查2350时,先要确认触发命令的查询条件是否包含TTL时间字段,以及同一个键是否被多个任务重复清理。

TTL过期数据未被及时清理的常见原因

TTL监视器默认每60秒运行一次,它并不会在文档过期的那个瞬间立即删除数据。如果一张表在短时间内产生大量相同过期时间的文档,例如缓存或会话表在某个活动期间写入了几百万条会话,TTL监视器需要多轮扫描才能慢慢追平。这种现象属于异步删除的固有延迟,不是故障码2350造成的,但会让业务在短暂窗口内仍能读到本该过期的数据。

第二个原因是索引定义不符合要求。MongoDB的TTL索引只能建立在单个日期类型字段上。如果字段存储的是时间戳数字、字符串日期或嵌套对象,TTL监视器会直接忽略这些文档。可以通过getIndexes()确认索引是否带有expireAfterSeconds,再抽查字段类型是否为Date。还有一种情况是索引创建在备库不可用,TTL删除只在主库执行,如果业务读取配置为secondary,就可能从备库读到尚未被复制的过期数据。

第三个原因是主库压力过大或存在写冲突。TTL删除也是普通的删除操作,它会产生写入并写入oplog。如果主库正在处理大量业务写入、磁盘IO打满或存在长事务导致的锁等待,TTL删除会被延迟。此时即便每60秒触发一次,单轮能够清理的文档数量也会下降,过期数据看起来就像一直未被清理。

业务侧如何消除2350误报

针对删除动作,应用层应当把文档不存在视为正常结果。下面是一段Node.js示例,当findOneAndDelete未命中或抛出2350时,都会返回已过期状态而不是继续抛出异常:

async function removeExpiredSession(sessionId) {
  try {
    const result = await db.collection('sessions').findOneAndDelete({
      _id: sessionId,
      expireAt: { $lte: new Date() }
    });
    if (result && result.value) {
      return { removed: true };
    }
    return { removed: false, alreadyGone: true };
  } catch (err) {
    if (err.code === 2350 || err.codeName === 'NoMatchingDocument') {
      return { removed: false, alreadyGone: true };
    }
    throw err;
  }
}

如果使用批量删除,deleteMany不会因为未命中而抛出2350,可以通过deletedCount判断是否有文档被删除。对于过期数据清理,优先使用deleteMany配合较小的分批条件,比如每次删除1000条,避免大范围删除长时间占用锁。这样既不会与TTL删除产生过多竞争,也能更快地处理历史积压数据。

业务侧还应该避免同时维护多套清理任务。例如A服务已经依赖TTL索引,B服务又定时执行条件删除,两者并发时很容易让B服务频繁看到2350。统一过期数据的所有权,明确由TTL负责删除还是由业务任务负责删除,可以显著减少这种竞态。

结合serverStatus指标和参数调优快速定位

要判断TTL监视器是否正常工作,可以先查看serverStatus中的TTL统计信息。下面这条命令可以直接看到累计删除文档数和扫描轮数:

db.serverStatus().metrics.ttl

返回结果通常包含deletedDocuments和passes两个字段。如果passes持续增长而deletedDocuments几乎不变化,说明监视器在扫描但没有匹配到可删除文档,优先检查字段类型和索引定义。如果passes也不再增长,则需要检查TTL监视器是否被禁用或进程是否卡住。还可以通过db.currentOp({ desc: /TTL/ })观察正在执行的TTL删除任务。

对于集中过期造成的延迟,可以把ttlMonitorSleepSecs参数调整到更小值,例如从默认60秒调整为30秒。需要注意的是,这个调整会增加后台删除频率,如果文档量非常大会占用更多IO,应该先在测试环境验证。设置命令如下:

db.adminCommand({ setParameter: 1, ttlMonitorSleepSecs: 30 })

更稳妥的方法是分散过期时间。不要把所有会话的expireAt都设置成整点或同一个固定值,可以使用随机偏移,比如在过期基数上增加几秒到几分钟的随机量。这样TTL监视器每轮面对的是相对均匀的删除压力,而不是一次集中冲击。

广告位 · 文章底部(后台-模块管理替换此区域)