分片集群出现跨分片全表扫描,是因为mongos路由层在查询条件未包含有效分片键(或无法精确路由)时,会将请求广播至所有分片,各分片被迫执行COLLSCAN,导致N次全表扫描叠加、CPU线性飙升。

为什么分片集群里会出现跨分片全表扫描
分片集群本身不自动规避全表扫描,mongos 路由层在缺乏有效分片键过滤时,会把查询广播到所有分片。一旦查询条件没包含分片键(或只含前缀但无法精确路由),mongos 就不得不向每个分片发请求,每个分片再各自执行 COLLSCAN —— 这不是“一次扫描”,而是 N 次扫描叠加,CPU 使用直接线性翻倍。
常见触发场景包括:
- 查询语句中完全没带分片键字段,例如对
orders集合按user_email查询,但分片键是order_id - 使用了分片键,但用了
$ne、$not、$regex(非前缀)等无法路由的操作符 - 分片键是复合键(如
{region: 1, timestamp: -1}),但查询只用了timestamp,没带region - 聚合管道开头没用
$match过滤分片键,导致$lookup或$group在所有分片上并行执行
如何快速定位跨分片无效请求
核心是区分“真慢查询”和“假高负载”:后者往往不是单个操作慢,而是大量轻量请求因广播机制把 CPU 带起来。
用 db.currentOp() 查看当前活跃操作时,重点关注:
-
secs_running值不大(比如 <1 秒),但client字段显示多个不同 IP 或连接池标识 -
ns相同,但opid密集出现,且query中缺失分片键字段 - 输出里带
"fromRouter": true,说明来自mongos,且"desc": "conn"后常跟大量重复模式
更直接的方式是登录任意分片节点(不是 mongos),运行:
db.setProfilingLevel(1, {slowms: 50})
然后查 system.profile,筛选出 docsExamined 高但 executionStats.nReturned 很低的记录——这类就是典型“扫了一万文档,只返回两条”的跨分片无效扫描。
怎么验证某个查询是否真的跨了所有分片
别猜,用 explain("executionStats") 看执行计划:
db.orders.explain("executionStats").find({status: "pending"})关键看两处:
-
"shards"数组长度是否等于你集群的分片总数 - 每个分片的
"executionStats.docsExamined"是否都显著大于 0,且数值接近(说明每个分片都在全扫)
如果发现某次查询本应只打一个分片,却出现在全部 shards 下,基本可断定分片键设计或查询写法有问题。
修复时最容易忽略的三个点
- 分片键不可变:改分片键要重新分片(
sh.splitAt() 不行,得用 moveChunk 或导出导入),线上几乎不可行,所以建库时必须想清
-
mongos 不缓存分片路由规则:每次查询都实时查 config server 的 chunks 元数据,若 config server 响应慢或网络抖动,会加剧请求排队,间接抬高 CPU
- 聚合管道里加
$limit 没用:它在最后阶段生效,前面的 $match 如果没命中分片键,照样先扫全量;必须把分片键过滤放在第一个 $match,且不能被 $or、$in(含空数组)等破坏索引选择性
sh.splitAt() 不行,得用 moveChunk 或导出导入),线上几乎不可行,所以建库时必须想清mongos 不缓存分片路由规则:每次查询都实时查 config server 的 chunks 元数据,若 config server 响应慢或网络抖动,会加剧请求排队,间接抬高 CPU$limit 没用:它在最后阶段生效,前面的 $match 如果没命中分片键,照样先扫全量;必须把分片键过滤放在第一个 $match,且不能被 $or、$in(含空数组)等破坏索引选择性真正压 CPU 的,往往不是单条重查询,而是每天几百万次的轻量广播请求。盯住 system.profile 里的 docsExamined 总和,比盯单个 CPU 百分比更有诊断价值。


















