在MongoDB分片集群的运维过程中,故障码1510是一个让不少人头疼的问题。它的典型表现是:balancer一直在运行,chunk却怎么也搬不动,mongos日志中反复出现cannot split chunk或者错误码1510的记录,个别分片的磁盘占用和数据量远超其他分片。追根溯源,这个问题几乎都指向同一个原因——分片键的基数(cardinality)太低,导致单个chunk膨胀成了无法切分的jumbo chunk。这篇文章就来详细拆解这个故障的前因后果,并给出排查和处理方案。

故障码1510是怎么产生的:从chunk切分说起
要理解1510,先要理解MongoDB分片的基本单位——chunk。一个chunk是一段连续的分片键范围,默认大小为64MB(3.6以下版本部分为1MB到64MB不等)。当某个chunk的大小超过阈值时,balancer会尝试把它切分成两个更小的chunk,然后把多余的chunk迁移到其他分片,从而实现数据均衡。
问题的关键在于:chunk的切分依赖分片键的取值分布。如果分片键只有有限的几个不同取值(基数低),比如一个status字段只有active和inactive两个值,那么无论MongoDB怎么尝试切分,最多也只能切出两个chunk。假设这两个值对应的数据量各达到几十GB,每个chunk内部就再也切不动了,因为切分点必须落在分片键的边界上,同一个键值范围内的数据是不可分割的。
这种无法再切分、又超过大小限制的chunk,就是所谓的jumbo chunk。此时balancer反复尝试切分失败,日志中就会出现1510错误。jumbo chunk还有一个连带伤害:它虽然可以被标记为jumbo从而跳过迁移尝试,但它占用的数据量会永久性地让所在分片背负沉重的负载,整个集群的负载均衡形同虚设。
哪些分片键设计容易踩坑
第一类低基数陷阱是直接使用枚举型字段。比如订单表用order_type做分片键,只有支付、退款、充值几种取值;或者用地区码做分片键,全国也就三十几个省级行政区。假设你有16个分片,但分片键只有30个取值,那么chunk数量上限就是30,根本喂不饱16个分片,均衡器再怎么努力也是巧妇难为无米之炊。
第二类陷阱是单调递增字段,比如时间戳或ObjectId。这类字段基数其实很高,但问题出在写入模式上:所有新写入的数据都集中在分片键范围的最大端,永远落在同一个分片上,形成写入热点。MongoDB 4.2之前的哈希分片和范围分片都无法自动缓解这种热点,最后一个分片的压力会远大于其他分片。虽然严格来说这是另一个问题,但运维时它经常和1510一起出现,因为热点分片上的chunk增长最快,最先变成jumbo。
第三类陷阱是复合分片键搭配不当。有人为了提高基数会把低基数字段放在前面、高基数字段放在后面,比如用{region: 1, userId: 1}。这种写法本身没错,但如果查询几乎不带region条件,跨分片广播查询会很多;而如果两个都是低基数字段,比如{region: 1, status: 1},总基数是两者的乘积,依然可能不够用。判断复合键基数的原则很简单:所有字段的基数乘积必须显著大于集群的chunk总数需求,一般建议至少是分片数量的十倍以上。
如何排查和确认问题
排查的第一步是查看chunk分布情况。连接mongos后,使用sh.status()可以看到每个分片的chunk数量,如果某个分片chunk数明显偏多,或者某个chunk被标记为jumbo(显示为带JUMBO字样),基本可以确认问题。更精确的做法是用下面这个脚本统计jumbo chunk:
use config
db.chunks.find({jumbo: true}).count()
// 查看具体哪些chunk是jumbo,以及它们的分片键范围
db.chunks.find({jumbo: true}).pretty()
// 统计各分片的chunk数量分布
db.chunks.aggregate([
{ $group: { _id: "$shard", count: { $sum: 1 } } }
])第二步是评估分片键的实际基数。对目标集合跑一个distinct统计,看看分片键字段到底有多少个不同取值:
// 统计单字段分片键的基数
db.orders.distinct("customerType").length
// 统计复合分片键的组合基数,可以用聚合近似
db.orders.aggregate([
{ $group: { _id: { region: "$region", status: "$status" } } },
{ $count: "distinctCombos" }
])如果基数只有几十几百,而集群有十几个分片,那问题就实锤了。还可以结合db.collection.stats()中的shardSize或avgObjSize来估算单个chunk大概承载多少文档,从而判断当前基数能支撑的chunk上限。
jumbo chunk的处理与长期修复方案
短期处理上,对于已经产生的jumbo chunk,可以在MongoDB 4.2以前的版本中手动清除jumbo标记,让balancer重新尝试迁移。操作方式是连接到config server主节点执行:
use config
// 找到jumbo chunk的min和max边界,然后清除标记
var jumboChunk = db.chunks.findOne({jumbo: true})
db.chunks.update(
{ _id: jumboChunk._id },
{ $unset: { jumbo: "" } }
)需要注意的是,从MongoDB 4.2开始,balancer改成了基于分片数据量而非chunk数量的均衡策略,jumbo chunk的处理逻辑有较大变化,清除标记的操作不再总是必要,系统会尝试迁移jumbo chunk本身。如果chunk确实无法切分且数据量巨大,迁移它也会带来网络和性能开销,最好安排在业务低峰期进行。
长期方案只有一个:换分片键。MongoDB 4.2之前的版本,修改分片键需要导出数据、重建集合再导入;4.2及以后提供了refineCollectionShardKey命令,可以在原分片键后面追加后缀字段来提高基数,不需要停机导数据:
// 在原分片键 {region: 1} 后追加 userId,形成复合分片键
db.adminCommand({
refineCollectionShardKey: "mydb.orders",
key: { region: 1, userId: 1 }
})注意refineCollectionShardKey只能追加字段,不能删除或修改原有字段,所以如果原来的分片键本身选得完全不合理,还是得走重建集合的老路。重建时的标准流程是:创建新集合并指定新的分片键,用$out或mongodump、mongorestore迁移数据,最后通过rename切换,切换期间需要短暂停写或做好双写方案。
一套靠谱的分片键设计原则
总结下来,选分片键时建议从四个维度依次校验。第一是基数:分片键的取值数量要远大于集群分片数,让每个分片能分到足够多的chunk,经验值是基数至少为分片数乘以十。第二是分布均匀:取值不能倾斜,如果某个键值占了一半以上的数据,高基数也没意义。第三是写入分散:避免单调递增键导致热点,除非使用哈希分片来打散写入。第四是查询命中:最常见的查询应该携带分片键前缀,否则每次查询都要scatter-gather到所有分片,性能会大幅下降。
如果拿不准,哈希分片键是一个稳妥的默认选择。对高基数且随机的字段(比如userId)做hashed分片,可以同时保证数据均匀和写入分散,代价是范围查询无法利用分片键路由。而对于既需要范围查询又需要高基数的场景,复合分片键把查询过滤字段放前面、高基数随机字段放后面,例如{region: 1, userId: 1},是目前比较通用的实践。
最后提一句,分片键一旦定下很难更改,哪怕是4.2之后也只能追加不能修改。所以在建集合之前,先花半小时统计一下候选字段的基数和分布,用distinct和聚合管道做个简单评估,远比事后处理1510故障省心得多。
MongoDB分片键故障码1510jumbo chunk修改时间:2026-09-05 22:43:00