第26章:Mongo分片集群进阶——Chunk 迁移、热点与均衡
2026/7/23 4:48:59 网站建设 项目流程

1. 项目背景

业务场景:本地生活电商的分片集群上线半年后,运维发现监控大屏上出现了一个诡异现象——shard-1 的 CPU 使用率 85%,shard-2 只有 12%。打开sh.status()一看,shard-1 上有 387 个 Chunk,shard-2 只有 42 个。Balancer 明明在跑,为什么数据没有自动均衡?更严重的是,一个大促活动创建了一个"双11"的订单 Chunk 超过了 128MB,正常分裂失败,变成了 Jumbo Chunk——Balancer 无法移动它,导致 shard-1 的磁盘率先告警。团队讨论是否要紧急重启 Balancer、手动 moveChunk、还是上 MongoDB 5.0 的新特性 reshardCollection。

痛点:分片集群"搭起来"只是第一步,跑起来以后的问题才是真正的考验:Chunk 分裂和迁移的内部机制不透明——为什么有些 Chunk 到了 200MB 还不分裂?Balancer 在窗口期内的行为不可控——大促期间要不要手动暂停?发现热点分片后除了手动moveChunk还有没有自动化方案?Jumbo Chunk 阻塞均衡器的时候如何处理不中断服务?

2. 项目设计

小胖(盯着 Grafana 面板上两个分片的天壤之别):大师!我们 shard-1 快炸了,shard-2 闲得在摸鱼!Balancer 是不是坏了?

大师:先别急着怪 Balancer。看看那些没被均衡的 Chunk 是不是 Jumbo Chunk。

小胖:Jumbo Chunk 是啥?跟普通 Chunk 有啥区别?

大师:MongoDB 的 Chunk 默认最大 128MB。一个 Chunk 数据量超过阈值后,mongos 会自动把它"分裂"(split)成两个。但如果分片键的值空间不足——比如你的分片键只有一个值(如city: "深圳"),那这个 Chunk 全是同一个分片键值,MongoDB 找不到可以从中切开的点,就分裂失败,这个 Chunk 被标记为"Jumbo"——超大且不可分裂。Balancer 看到 Jumbo Chunk 会直接跳过——因为移也移不动,分了分不开。

技术映射:Jumbo Chunk 的根因是分片键基数不够——同一个分片键值的文档数量太多,单个 Chunk 装不下但也不能分。解决之道不是调大 Chunk 大小,而是细化分片键。

小胖:那我怎么处理 Jumbo Chunk?是不是只能reshardCollection

大师:在 MongoDB 4.4+ 中可以先用refineShardKey(微调分片键)——在原来的分片键基础上加一个字段增加基数。比如原先{city:1},微调为{city:1, userId:1}——这样同一个城市的文档可以根据 userId 分成多个 Chunk,Jumbo 问题就消失了。

从 MongoDB 5.0 开始直接用reshardCollection——这是更彻底但更重的手段。它会创建一个新的分片集合,在后台把数据从旧分片键迁移到新分片键,完成后原子切换。整个过程对线上读写几乎无影响,但会耗费大量资源。

技术映射refineShardKey= 小手术(加字段),reshardCollection= 大手术(换分片键)。前者要求在现有前缀基础上追加,后者可以任意更换。

小白(追问):那 Chunk 迁移(moveChunk)的内部流程是怎样的?迁移期间会影响读写吗?

大师:moveChunk 使用"异步复制 + 追增量"的模式,迁移过程对读写几乎透明。大致流程:

  1. Balancer 选择一个待迁移的 Chunk 和源/目标分片。
  2. 目标分片开始从源分片"克隆"这个 Chunk 的数据。
  3. 克隆期间,源分片上的写操作持续产生增量——目标分片通过追 Oplog 追上增量。
  4. 当增量足够小时,源分片短暂锁定这个 Chunk 的范围(通常几个毫秒),完成最后一次增量同步。
  5. Config Server 更新元数据——把 Chunk 的所有权转给目标分片。
  6. 源分片清理旧数据。

技术映射:Chunk 迁移的"短暂锁定"阶段通过criticalSection实现——仅影响正在迁移的 Chunk 对应的分片键范围的写入,不影响其他范围的读写。

小胖:那大促期间,我们应该把 Balancer 关掉吗?

大师:大促期间关掉 Balancer 是标准操作——原因不是 Balancer 本身有问题,而是 Chunk 迁移产生的网络和磁盘 IO 会挤占正常业务的资源。关掉以后,等大促结束、流量平稳再打开。大促前也可以手动做一轮主动均衡——把热点 Chunk 提前迁移到资源充裕的分片上。

大师(总结):分片集群运维三件事——Jumbo Chunk 的本质是分片键基数不足(refineShardKey/reshardCollection 解决);Chunk 迁移是透明的但占资源(大促关 Balancer);手动moveChunk是应急手段不要作为常态。

3. 项目实战

3.1 环境准备

需要分片集群环境。如果本地没有完整的分片集群,本章大部分操作可以在单机 mongod 上演示概念(非分片集群环境sh.status()不可用,但 Jumbo Chunk 和refineShardKey相关 API 可以通过注释/模拟理解)。

3.2 分步实现

步骤一:观察 Chunk 分布与 Balancer 状态

目标:通过 mongos 查看分片集群的 Chunk 分布和均衡状态。

// 连接 mongos// 1. 全局分片状态概览sh.status()// 2. 查看 Balancer 是否运行sh.isBalancerRunning()// true = 正在迁移 Chunk,false = 空闲// 3. 查看 Balancer 窗口配置(允许在什么时间段运行)use config db.settings.findOne({_id:"balancer"})// 如果 activeWindow 存在,Balancer 只在指定的时间窗口内运行// 例如:{ start: "02:00", stop: "06:00" } 表示仅凌晨 2-6 点运行// 4. 设置 Balancer 运行窗口(只在夜间运行)// sh.setBalancerState(false) // 先停掉// db.settings.updateOne(// { _id: "balancer" },// { $set: { activeWindow: { start: "02:00", stop: "06:00" } } },// { upsert: true }// )// sh.setBalancerState(true) // 再开启// 5. 各分片的 Chunk 数分布db.chunks.aggregate([{$group:{_id:"$shard",chunkCount:{$sum:1}}},{$sort:{chunkCount:-1}}]).toArray().forEach(s=>{print(`Shard${s._id}:${s.chunkCount}chunks`)})

步骤二:检测和处理 Jumbo Chunk

目标:从 config 库中找出 Jumbo Chunk 并理解处理方法。

use config// 查找所有 Jumbo ChunkconstjumboChunks=db.chunks.find({jumbo:true}).toArray()print("Jumbo Chunk 数量:",jumboChunks.length)jumboChunks.forEach(c=>{print(`集合:${c.ns}`)print(`分片键范围:${JSON.stringify(c.min)}${JSON.stringify(c.max)}`)print(`所在分片:${c.shard}`)print(`---`)})// Jumbo Chunk 的处理步骤:// 方式 A:手动 trigger split(如果数据量已经增长到可分)// 在 mongos 上执行:// sh.splitAt("local_life.orders_hashed", { userId: "中间值" })// 注意:需要知道数据分布——选择一个合理的中间值// 方式 B:手动 moveChunk(如果能分裂更好先分裂)// sh.moveChunk("local_life.orders_hashed", { userId: MinKey }, "shard-2")// 方式 C:refineShardKey(推荐)// 把分片键 { city: 1 } 微调为 { city: 1, userId: 1 }// 在 mongos 上执行:// sh.refineShardKey(// "local_life.orders_shard",// { city: 1, userId: 1 } // 新分片键必须在原分片键基础上追加字段// )// 注意:refineShardKey 执行期间原分片键的查询仍然有效// 方式 D:reshardCollection(MongoDB 5.0+)// sh.reshardCollection(// "local_life.orders_shard",// { userId: "hashed" } // 可以完全换新分片键// )// reshardCollection 会创建目标分片集合 → 后台拷贝数据 → 原子切换// 检查 refineShardKey 是否完成// sh.status() 可看到正在进行的分片键微调状态

步骤三:手动均衡操作

目标:在特殊情况下手动移动 Chunk 来平衡负载。

// 连接 mongos// 1. 找到热点分片上的 Chunkuse configconsthotShard="shard-1"consthotChunks=db.chunks.find({shard:hotShard}).limit(5).toArray()print(`热点分片${hotShard}上的 Chunk 数:`,db.chunks.countDocuments({shard:hotShard}))// 2. 手动移动一个 Chunk 到负载较低的分片// sh.moveChunk(// "local_life.orders_hashed", // 命名空间// { userId: "U_500" }, // 查找包含该分片键值的 Chunk// "shard-2" // 目标分片// )// 3. 大促前预均衡——把一个热点范围的 Chunk 提前分布开// 对 hashed 分片集合,可以手动 split 热点范围// for (let i = 0; i < 100; i++) {// sh.splitAt("local_life.orders_hashed", { userId: `U_${i * 100}` })// }// 然后让 Balancer 自然均衡这些 Chunk// 4. 关闭/开启 Balancer// sh.stopBalancer() // 大促期间关掉// sh.startBalancer() // 大促结束打开// 查看 Balancer 日志// 最近的 Chunk 迁移记录db.changelog.find({what:"moveChunk.commit"}).sort({time:-1}).limit(10).toArray().forEach(log=>{print(`${log.time}|${log.ns}|${log.details.from}${log.details.to}|${log.details.duration||'?'}ms`)})

步骤四:分片集群备份要点

目标:理解分片集群备份与复制集备份的关键差异。

# 分片集群备份方案对比# 方案 A:mongodump 通过 mongos(逻辑备份)# 优点:一个命令备份整个集群# 缺点:备份速度受限于单 mongos 的吞吐,大集群慢;一致性需依赖 --oplogmongodump--host=mongos:27017--oplog--out=/backup/cluster_dump# 方案 B:单独备份 Config Server + 每个 Shard# 优点:并行备份,速度快,可单独恢复某个 Shard# 缺点:需要手动协调各部分的备份时间点一致性# 备份 Config Servermongodump--host=config1:27019--db=config--out=/backup/config# 备份每个 Shardmongodump--host=shard1a:27017--oplog--out=/backup/shard1 mongodump--host=shard2a:27017--oplog--out=/backup/shard2# 方案 C:文件系统快照(LVM/ZFS/EBS Snapshot)# 在所有 Shard 和 Config Server 同一时刻做快照# 一致性最高、恢复速度最快(TB 级集群的推荐方案)

步骤五:新分片加入 + 扩容实战

目标:为分片集群增加新分片,观察 Chunk 自动重新分布。

// 连接 mongos// 1. 新分片加入集群sh.addShard("shard3/shard3a:27017,shard3b:27017,shard3c:27017")// 2. 观察 Balancer 自动开始均衡// 旧分片上的 Chunk 会陆续迁移到新分片// 用 sh.status() 持续观察// 3. 监控迁移进度functionmonitorMigration(){constbefore=db.getSiblingDB("config").chunks.aggregate([{$group:{_id:"$shard",count:{$sum:1}}}]).toArray()sleep(60000)// 1 分钟后constafter=db.getSiblingDB("config").chunks.aggregate([{$group:{_id:"$shard",count:{$sum:1}}}]).toArray()print("Chunk 分布变化:")after.forEach(s=>{constprev=before.find(b=>b._id===s._id)?.count||0print(`${s._id}:${prev}${s.count}(${s.count-prev>0?'+':''}${s.count-prev})`)})}monitorMigration()

3.3 完整代码清单

文件用途
mongodb-lab/sharding/chunk-monitor.jsChunk 分布与 Balancer 状态监控
mongodb-lab/sharding/jumbo-chunk-detect.jsJumbo Chunk 检测
mongodb-lab/sharding/manual-balance.js手动 moveChunk 与 split
mongodb-lab/sharding/backup-shard.js分片集群备份方案

3.4 测试验证

// 在 mongos 中执行// 1. 确认 Balancer 状态可查询constbalancerConfig=db.getSiblingDB("config").settings.findOne({_id:"balancer"})print("Balancer 配置:",balancerConfig?"PASS":"N/A")// 2. 确认 Chunk 分布数据可读constchunkCount=db.getSiblingDB("config").chunks.countDocuments({})print("总 Chunk 数:",chunkCount,chunkCount>0?"PASS":"FAIL")// 3. 确认 Jumbo Chunk 检测逻辑constjumboCount=db.getSiblingDB("config").chunks.countDocuments({jumbo:true})print("Jumbo Chunk:",jumboCount,jumboCount>=0?"PASS":"N/A")// 4. 确认迁移日志可读constrecentLogs=db.getSiblingDB("config").changelog.find({what:"moveChunk.commit"}).count()print("最近迁移日志:",recentLogs>=0?"PASS":"N/A")print("\n=== 分片集群进阶验证完成 ===")

4. 项目总结

4.1 Chunk 管理工具速查

工具用途影响范围风险等级
sh.status()查看集群整体状态-
Balancer自动均衡 Chunk 分布Chunk 迁移期间的 IO 和网络低(可设窗口期)
sh.moveChunk()手动移动 Chunk仅影响被移动的 Chunk 范围中(迁移期间短暂锁)
sh.splitAt()手动分裂 Chunk仅影响被分裂的 Chunk
refineShardKey()追加分片键字段整个集合,缓慢但非阻塞
reshardCollection()完全换分片键整个集合,后台拷贝中(资源消耗大)

4.2 适用场景

本章操作适用

  1. 分片不均衡的定期巡检——每周查看 Chunk 和文档数分布。
  2. 大促前后的 Balancer 管理——前预均衡、中关 Balancer、后恢复均衡。
  3. 发现 Jumbo Chunk 后的应急处理——refineShardKey 增加分片键基数。
  4. 分片扩容——新分片加入后观察 Chunk 迁移进度。
  5. 分片集群备份策略设计——Config Server + 各 Shard 的一致性快照。

4.3 注意事项

注意事项说明
moveChunk不可频繁手动操作手动 moveChunk 会跳过 Balancer 的阈值判断,可能引发"乒乓效应"(来回迁移)
Jumbo Chunk 的处理不要拖Jumbo Chunk 持续存在会导致 Balancer 永远无法均衡该集合
reshardCollection不是轻量级操作它会占用 2 倍的临时空间(新集合拷贝),并产生大量 Oplog
Balancer 窗口设置得太短如果窗口内迁不完所有目标 Chunk,下次开启继续——导致迁移永远追不上写入
Config Server 的备份Config Server 是分片集群的"大脑",Config 库的数据量很小但绝不能丢

4.4 常见踩坑经验

故障案例一:Balancer 窗口设太短导致迁移永远赶不上

某团队把 Balancer 窗口设为凌晨 3-4 点(1 小时),但写入量很大加上白天积累了数百个待迁移 Chunk,1 小时的窗口只能迁完 30-40 个。剩余 Chunk 越来越多,热点分片的问题持续恶化。解决:先把 Balancer 打开跑一整天完成积累的迁移,再把窗口改成 3-6 点(3 小时),并在大促前做一次预均衡预处理。

故障案例二:moveChunk 期间出现了丢失写入

某运维手动 moveChunk 一个正在被大量写入的 Chunk。迁移过程中最后的 criticalSection 阶段短暂锁定了该 Chunk 范围的写入。因为并发太高,MongoDB 多次尝试同步增量均失败,最终 Chunk 迁移被回滚,但应用层已有部分写入被"卡住"超时。解决:不要在 Chunk 活跃写入时手动 moveChunk;最好在大促前、写入低峰期完成均衡。

故障案例三:refineShardKey执行后原索引仍存在

某团队 refineShardKey 后(从{city:1}变为{city:1, userId:1}),发现查询速度没有改善。原来优化器仍然选用了旧的{city:1}分片键索引,而非新的{city:1, userId:1}复合前缀。解决:refineShardKey 后 MongoDB 保留了旧索引——手动hideIndex掉旧的、仅用新的;同时清除 Plan Cache 让优化器重新为查询竞速。

4.5 思考题

  1. 分片集群中如果删除了一个分片上的大量文档,这个分片的 Chunk 数会自动减少吗?
  2. 如果reshardCollection执行到一半 mongos 挂了,重启后 reshard 操作会恢复吗?数据会损坏吗?

(答案将在第 27 章末尾揭晓)

上一章思考题答案

  1. 新分片加入后旧 Chunk 会自动迁移——Balancer 检测到不同分片的 Chunk 数不平衡(差异超过阈值),自动触发 Chunk 迁移,将部分 Chunk 从旧分片移到新分片。迁移是自动的,无需手动干预。

  2. MongoDB 不允许updateMany修改分片键值,因为改了分片键意味着文档所属的分片会改变——而updateMany无法保证跨分片的原子性。如果允许修改分片键,更新的一部分文档在分片 A、一部分在分片 B,出现失败时无法回滚。要实现分片键变更,要么用reshardCollection,要么用事务内delete + insert的文档级迁移。

延伸阅读与资源

MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战

大型语言模型(LLM) vLLM 高性能推理落地实战

Agent开发之LlamaIndex 实战修炼与源码进阶

大语言模型Transformers 实战修炼与源码剖析

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询