第26章:Mongo分片集群进阶——Chunk 迁移、热点与均衡

发布时间:2026/7/23 4:49:11
第26章:Mongo分片集群进阶——Chunk 迁移、热点与均衡 1. 项目背景业务场景本地生活电商的分片集群上线半年后运维发现监控大屏上出现了一个诡异现象——shard-1 的 CPU 使用率 85%shard-2 只有 12%。打开sh.status()一看shard-1 上有 387 个 Chunkshard-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 分成多个 ChunkJumbo 问题就消失了。从 MongoDB 5.0 开始直接用reshardCollection——这是更彻底但更重的手段。它会创建一个新的分片集合在后台把数据从旧分片键迁移到新分片键完成后原子切换。整个过程对线上读写几乎无影响但会耗费大量资源。技术映射refineShardKey 小手术加字段reshardCollection 大手术换分片键。前者要求在现有前缀基础上追加后者可以任意更换。小白追问那 Chunk 迁移moveChunk的内部流程是怎样的迁移期间会影响读写吗大师moveChunk 使用异步复制 追增量的模式迁移过程对读写几乎透明。大致流程Balancer 选择一个待迁移的 Chunk 和源/目标分片。目标分片开始从源分片克隆这个 Chunk 的数据。克隆期间源分片上的写操作持续产生增量——目标分片通过追 Oplog 追上增量。当增量足够小时源分片短暂锁定这个 Chunk 的范围通常几个毫秒完成最后一次增量同步。Config Server 更新元数据——把 Chunk 的所有权转给目标分片。源分片清理旧数据。技术映射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 正在迁移 Chunkfalse 空闲// 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 ChunkconstjumboChunksdb.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)// 方式 CrefineShardKey推荐// 把分片键 { city: 1 } 微调为 { city: 1, userId: 1 }// 在 mongos 上执行// sh.refineShardKey(// local_life.orders_shard,// { city: 1, userId: 1 } // 新分片键必须在原分片键基础上追加字段// )// 注意refineShardKey 执行期间原分片键的查询仍然有效// 方式 DreshardCollectionMongoDB 5.0// sh.reshardCollection(// local_life.orders_shard,// { userId: hashed } // 可以完全换新分片键// )// reshardCollection 会创建目标分片集合 → 后台拷贝数据 → 原子切换// 检查 refineShardKey 是否完成// sh.status() 可看到正在进行的分片键微调状态步骤三手动均衡操作目标在特殊情况下手动移动 Chunk 来平衡负载。// 连接 mongos// 1. 找到热点分片上的 Chunkuse configconsthotShardshard-1consthotChunksdb.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)})步骤四分片集群备份要点目标理解分片集群备份与复制集备份的关键差异。# 分片集群备份方案对比# 方案 Amongodump 通过 mongos逻辑备份# 优点一个命令备份整个集群# 缺点备份速度受限于单 mongos 的吞吐大集群慢一致性需依赖 --oplogmongodump--hostmongos:27017--oplog--out/backup/cluster_dump# 方案 B单独备份 Config Server 每个 Shard# 优点并行备份速度快可单独恢复某个 Shard# 缺点需要手动协调各部分的备份时间点一致性# 备份 Config Servermongodump--hostconfig1:27019--dbconfig--out/backup/config# 备份每个 Shardmongodump--hostshard1a:27017--oplog--out/backup/shard1 mongodump--hostshard2a: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(){constbeforedb.getSiblingDB(config).chunks.aggregate([{$group:{_id:$shard,count:{$sum:1}}}]).toArray()sleep(60000)// 1 分钟后constafterdb.getSiblingDB(config).chunks.aggregate([{$group:{_id:$shard,count:{$sum:1}}}]).toArray()print(Chunk 分布变化:)after.forEach(s{constprevbefore.find(bb._ids._id)?.count||0print(${s._id}:${prev}→${s.count}(${s.count-prev0?:}${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 与 splitmongodb-lab/sharding/backup-shard.js分片集群备份方案3.4 测试验证// 在 mongos 中执行// 1. 确认 Balancer 状态可查询constbalancerConfigdb.getSiblingDB(config).settings.findOne({_id:balancer})print(Balancer 配置:,balancerConfig?PASS:N/A)// 2. 确认 Chunk 分布数据可读constchunkCountdb.getSiblingDB(config).chunks.countDocuments({})print(总 Chunk 数:,chunkCount,chunkCount0?PASS:FAIL)// 3. 确认 Jumbo Chunk 检测逻辑constjumboCountdb.getSiblingDB(config).chunks.countDocuments({jumbo:true})print(Jumbo Chunk:,jumboCount,jumboCount0?PASS:N/A)// 4. 确认迁移日志可读constrecentLogsdb.getSiblingDB(config).changelog.find({what:moveChunk.commit}).count()print(最近迁移日志:,recentLogs0?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 适用场景本章操作适用分片不均衡的定期巡检——每周查看 Chunk 和文档数分布。大促前后的 Balancer 管理——前预均衡、中关 Balancer、后恢复均衡。发现 Jumbo Chunk 后的应急处理——refineShardKey 增加分片键基数。分片扩容——新分片加入后观察 Chunk 迁移进度。分片集群备份策略设计——Config Server 各 Shard 的一致性快照。4.3 注意事项注意事项说明moveChunk不可频繁手动操作手动 moveChunk 会跳过 Balancer 的阈值判断可能引发乒乓效应来回迁移Jumbo Chunk 的处理不要拖Jumbo Chunk 持续存在会导致 Balancer 永远无法均衡该集合reshardCollection不是轻量级操作它会占用 2 倍的临时空间新集合拷贝并产生大量 OplogBalancer 窗口设置得太短如果窗口内迁不完所有目标 Chunk下次开启继续——导致迁移永远追不上写入Config Server 的备份Config Server 是分片集群的大脑Config 库的数据量很小但绝不能丢4.4 常见踩坑经验故障案例一Balancer 窗口设太短导致迁移永远赶不上某团队把 Balancer 窗口设为凌晨 3-4 点1 小时但写入量很大加上白天积累了数百个待迁移 Chunk1 小时的窗口只能迁完 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 思考题分片集群中如果删除了一个分片上的大量文档这个分片的 Chunk 数会自动减少吗如果reshardCollection执行到一半 mongos 挂了重启后 reshard 操作会恢复吗数据会损坏吗答案将在第 27 章末尾揭晓上一章思考题答案新分片加入后旧 Chunk 会自动迁移——Balancer 检测到不同分片的 Chunk 数不平衡差异超过阈值自动触发 Chunk 迁移将部分 Chunk 从旧分片移到新分片。迁移是自动的无需手动干预。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 实战修炼与源码剖析