分片集群游标易报CursorNotFound,因会话、mongos、分片三层计时器叠加且独立超时;需同步管理会话刷新、分片游标续命、驱动超时配置及索引优化。

分片集群里游标超时不是单点问题,而是会话、游标、mongos 三层计时器叠加的结果——光调 noCursorTimeout=True 或 batch_size() 很可能白忙。
为什么分片集群的游标更容易报 CursorNotFound
在分片集群中,mongos 是查询路由层,它本身不持有游标状态,而是把游标分散到后端多个分片上。每个分片独立维护自己的游标空闲计时器(默认 10 分钟),同时整个会话(session)又受全局 30 分钟过期限制。一旦某个分片上的游标因空闲超时被清理,而 mongos 还在尝试 fetch 下一批,就会直接抛出 CursorNotFound,且错误信息里通常不提示是哪个分片出的问题。
常见错误现象:pymongo.errors.CursorNotFound: cursor id 123456789 not found,尤其在聚合管道含 $lookup 或跨分片 $facet 时高频出现;用 explain("executionStats") 查看执行计划,发现 cursor.exhaust 为 false,说明游标不可“耗尽式”拉取,必须主动续命。
- mongos 不缓存分片游标,每次
getMore都要转发请求,网络抖动或分片负载高会导致响应延迟,间接触发空闲超时 - 分片副本集主节点切换期间,原主上的游标会被强制销毁,但 mongos 可能还没感知到新主,继续发
getMore到旧地址 - MongoDB Atlas 的 M0/Flex 集群禁用
noCursorTimeout,且分片模式下mongos版本若低于 6.0,对会话刷新支持不完整
batch_size() 在分片集群中必须配合 allowDiskUse(True)
分片集群跑聚合时,batch_size() 控制的是 mongos 向客户端返回的每批文档数,但真正影响游标生命周期的是各分片上子游标的 batchSize。如果聚合中间结果太大(比如 $group 后内存溢出),分片可能拒绝返回下一批,导致 mongos 等待超时。
实操建议:
- 对含
$group、$sort的聚合,务必加.allowDiskUse(True),否则分片可能直接 OOM 并 kill 游标 - 设
batch_size(1000)起步,观察 mongos 日志里的getMore延迟;若某次 >30s,说明该批在某个分片上卡住,需检查对应分片负载或索引 - 避免在
$lookup的 from 集合上缺失分片键索引——这会导致广播查询,所有分片都参与,游标创建和销毁压力倍增
必须显式管理会话,且刷新操作要发到配置服务器
分片集群中,会话元数据存在配置服务器副本集(CSRS)里,而不是某个分片上。所以 refreshSessions 命令不能发给任意 mongos 或分片,必须走 admin 数据库直连配置服务器(或通过 mongos 转发,但要求 mongos ≥ 6.0)。
实操要点:
- 用
client.start_session()创建会话后,所有查询必须传入session=xxx参数,否则不绑定会话 - 刷新命令必须这样写:
client.admin.command("refreshSessions", [session.session_id]),不能漏掉admin.前缀 - 刷新间隔建议 ≤25 分钟(留 5 分钟 buffer),且放在批处理循环体外——比如每处理完 5000 条后调一次,别在每条后刷
- 异常分支里必须调
session.end_session(),否则 CSRS 上残留会话会持续占用连接数,最终阻塞新会话创建
最容易被忽略的点:maxTimeMS 和驱动版本耦合
PyMongo 4.7+ 默认对所有查询启用 maxTimeMS=60000(1 分钟),这个值会透传给每个分片,而分片实际执行时间是各子查询最大值。如果某分片因磁盘慢或锁争用耗时 70 秒,整个游标就提前终止,报错却是 CursorNotFound,掩盖了真实瓶颈。
排查方式:
- 在聚合管道开头加
{$addFields: {__ts: {$literal: "$$NOW"}}},对比各分片返回的__ts时间戳差值,定位慢分片 - 临时关闭驱动级超时:
collection.find(..., max_time_ms=None),再结合explain("executionStats")看各分片executionTimeMillis - 确认 PyMongo 版本 ≥ 4.8,低版本在分片场景下对
maxTimeMS处理有竞态 bug(server-102082)
分片集群的游标稳定性,本质是会话生命周期、分片局部游标、mongos 路由策略三者的协同问题。任何一层掉链子,都会在客户端表现为“游标消失”。别只盯着代码里那行 find(),得顺着 mongos → 分片 → 配置服务器这条链路,逐层确认计时器是否被正确重置。











