MongoDB Balancer 源码深度解析:均衡、去碎片化与自动合并的策略与实现
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
分片集群(Sharded Cluster)中数据分布的均衡程度直接决定了查询路由效率与各分片(Shard)的负载水平。MongoDB 的Balancer(均衡器)正是运行在配置服务器(Config Server)主节点上的后台守护进程:它持续监控分片集合(sharded collection)的数据分布,并下发命令改善这种分布。本文以当前仓库中 src/mongo/db/s/balancer/README.md 为骨架,结合 balancer.h、balancer.cpp 等源码实现,系统讲解 Balancer 的双线程架构、三大动作策略(块选择、去碎片化、自动合并)的工作流程、核心判定阈值以及全部可调参数,帮助读者既掌握运维层面的开关与调优,也理解底层调度原理。
一、Balancer 是什么:职责与基本能力
Balancer 是构建在分片(sharding)之上的一类应用:它监控分片集合的数据分布情况,并下发命令来改善该分布。它的基本特性包括:
- 默认启用:无需任何手工配置即可运行;
- 可随时关闭/重新开启:既可以对整个集群(cluster)关闭或开启,也可以对单个集合(per-collection)进行控制;
- 支持均衡窗口(balancing window):可以配置为每天仅在特定时间段内运行。
从源码看,Balancer 是一个继承自ReplicaSetAwareServiceConfigSvr的ReplicaSetAwareService(见 balancer.h),这意味着它跟随配置服务器副本集的主节点(PRIMARY)生命周期启动:主节点当选(step up)时通过initiate()拉起均衡线程,主节点退位(step down)时通过requestTermination()与joinTermination()有序终止线程。
Balancer 作为配置服务器主节点的后台守护进程,由两个相互独立的线程组成,它们下发由动作策略(action policies)生成的请求。每个线程在执行期间都会查询不同的均衡策略,以决定下一步执行哪些操作。三个动作策略分别是:
| 策略 | 职责 |
|---|---|
| ChunkSelectionPolicy | 常规集合均衡动作(拆分块、迁移块) |
| DefragmentationPolicy | 集合去碎片化(合并小块) |
| AutoMergerPolicy | 合并连续块 |
在 balancer.h 中可以看到这三个策略实例均作为Balancer的成员存在(_chunkSelectionPolicy、_defragmentationPolicy、_autoMergerPolicy),此外还包含_clusterStats(集群统计)、_commandScheduler(命令调度器)与_moveUnshardedPolicy(未分片集合迁移策略)。
二、主线程(MainThread):轮次驱动的拆分与迁移
主线程持续运行,但以"轮次(rounds)"为单位推进,每两轮之间存在一段延迟(当前仓库中轮次默认间隔为kBalanceRoundDefaultInterval(10 * 1000),即 10 秒)。主线程负责下发两类操作:
- 拆分(splits):由块选择策略(ChunkSelectionPolicy)生成;
- 迁移(migrations):分别来自块选择策略与去碎片化策略(DefragmentationPolicy)。
一个完整的操作周期如下面的时序图所示:
在每个均衡轮次中:
- 主线程先派发一批拆分命令,并等待它们全部完成;
- 然后派发一批迁移命令,并等待全部完成;
- 轮次内下发的所有命令都必须在轮次结束前被等待完毕;
- 遇到任何错误只记录日志,主线程本身忽略错误继续运行。
从 balancer.h 可以看到主循环对应私有方法_mainThread(),轮次生命周期由_beginRound()/_endRound()管理,轮次间的等待通过_sleepFor()(条件变量等待 + 超时)实现,而跨区域边界的块拆分由_splitChunksIfNeeded()完成,批量迁移则由_doMigrations()调度。
Jumbo chunks(超大块)的处理
如果主线程为一个大于两倍最大块大小(2 × max chunk size)的块下发迁移命令,迁移会失败,并告知 Balancer 该块过大、无法迁移。此时主线程会尝试拆分该块:
- 拆分成功:问题解决,正常继续;
- 拆分失败(例如块内相同分片键值的文档过多,找不到合适的拆分点):该块会被标记为jumbo(超大块)。
这个 jumbo 标记会被块选择策略用来避免在未来继续选中这些大块进行迁移,从而防止无效迁移反复重试。
需要注意的是,jumbo 块并非永远无法移动:当用户下发moveChunk/moveRange命令并携带forceJumbo: true参数时,jumbo 块仍可以被迁移出所在分片。用户可以利用该选项手动将 jumbo 块重新分布到集群中的其他分片。在源码中,迁移请求的载体结构 MigrateInfo 便包含ForceJumbo forceJumbo字段,用于在迁移时显式指定是否强制迁移 jumbo 块。
三、辅助线程(Secondary Thread):异步命令执行
辅助线程等待在条件变量上,必须由其他进程(通常是客户端线程或主均衡线程)发出信号后才开始工作。它负责下发来自以下两个策略的非迁移类命令:
- 去碎片化策略:
merge(合并)、datasize(数据量统计)等命令; - 自动合并策略(AutoMergerPolicy):
mergeAllChunksOnShard命令。
当两个策略同时处于激活状态时,辅助线程会随机从去碎片化策略或自动合并策略中挑选命令下发。
与主线程同步等待每批命令完成不同,辅助线程是异步派发服务器命令的:任意时刻最多允许有50 个未完成(outstanding)的操作。该上限对应 balancer.h 中的常量kMaxOutstandingStreamingOperations = 50,并由成员_outstandingStreamingOps(Atomic<int>)实时计数。
为了降低下发命令对从节点(secondary nodes)上目录缓存刷新(catalog cache refresh)速率的影响,辅助线程还引入了节流参数,在两个调度操作之间插入等待。默认值与含义如下(定义见 sharding_config_server_parameters.idl):
| 参数 | 默认值 | 含义 |
|---|---|---|
chunkDefragmentationThrottlingMS | 1000(1 秒) | 两次不同的去碎片化动作下发之间的最小间隔,即通过configureCollectionBalancing开启集合去碎片化后,Balancer 下发的相邻两次mergeChunks/splitChunk命令的最小时间间隔 |
autoMergerThrottlingMS | 15000(15 秒) | 针对同一集合的两次自动合并命令下发之间的最小间隔 |
对应源码位置:chunkDefragmentationThrottlingMS 与 autoMergerThrottlingMS,两者均为Atomic<int32_t>,可在启动时(startup)或运行时(runtime)动态修改。
四、数据基础:ClusterStatistics 与 CollectionDataSizeInfoForBalancing
两个均衡策略做出的所有迁移决策都依赖于集群数据分布信息。每个均衡策略都持有一个 ClusterStatistics 的引用——这是一个接口,允许策略获取集群的数据分布与分片利用率统计,包括:
- 每个块(chunk)归属哪个分片;
- 集合定义的区域(zones);
- 每个块属于哪个区域。
除此之外,块选择策略还需要更细粒度的信息——每个集合在每个分片上的数据量大小,它通过 CollectionDataSizeInfoForBalancing(内部持有ShardDataSizeMap类型的分片→数据量映射)来跟踪这一信息。正是基于这些数据量统计,策略才能判断"哪个分片过载、哪个分片欠载",从而做出迁移决策。
五、ChunkSelectionPolicy:块选择策略
块选择策略 负责生成操作,以维持集群内分片集合的平衡。判断"集合是否平衡"的标准是:所有分片拥有的数据量是否大致相等。
拆分(Splits)
当某个块跨越区域边界(zone boundaries)时,块选择策略会生成拆分请求。拆分命令会创建出一个更小的块,其min 与 max 值恰好等于区域边界值,从而使区域的边界能够精确对应到块边界(这是区域约束生效的前提)。
迁移(Migrations)
块选择策略会扫描所有分片集合,生成一份迁移清单,让 Balancer 据此把数据更均匀地分布到各分片。对于每个集合,策略会选出所有"可迁移"的范围(range),并按以下优先级排序:
- 排水分片优先:如果某个范围位于正在被移除(draining / 排水)的分片上,优先选择该范围;
- 区域违规优先:如果某些范围违反了区域约束(落在了错误的分片上),选择这些范围;
- 均衡迁移兜底:若以上两种情况都不存在,策略为了达到"每个分片数据量相等"的目标,会找出负载最高的分片(数据量最多)与负载最低的分片(数据量最少),并选择从数据量较大的分片移出范围——前提是两者数据量之差大于 3 倍最大块大小(3 × max chunk size)。
在以上三种场景中,范围都会被移动到"仍然可用且负载最低"的分片。所谓"可用",是指该分片未处于排水状态,并且没有参与当前均衡轮次中已被选中的任何其他迁移。
这一优先级逻辑与 balancer_policy.h 中定义的迁移原因枚举高度对应:enum MigrationReason { none, drain, zoneViolation, chunksImbalance }——即"排水""区域违规""块数/数据量不均衡"三种迁移动机,外加"无迁移"。
块选择策略会在每个均衡轮次中尽可能多地提交迁移。但需要注意:去碎片化策略在迁移下发上拥有更高优先级——任何正在为去碎片化而迁移数据的分片,块选择策略都无法再向它调度迁移。
六、DefragmentationPolicy:去碎片化策略
集合去碎片化(Collection defragmentation)的目标是:在保证数据可路由的前提下,尽可能多地合并集合中的块,以减少集合的块总数。块是存储在分片路由表(sharding routing table)中、用于路由命令的用户数据细分单位,因此减少集合的块数量可以:
- 缩小路由信息(routing information)的规模;
- 加快分片刷新(sharding refresh)的速度。
去碎片化策略 负责在整个去碎片化过程中生成各类命令。去碎片化由三个阶段构成:
MergeAndMeasureChunks(合并并测量块)MoveAndMergeChunks(迁移并合并块)MergeChunks(合并块)
关键约束:一旦用户对某个集合发起去碎片化,在去碎片化运行期间,该集合只会被去碎片化策略考虑,不会参与常规均衡(configureCollectionBalancing命令会持久化这一状态)。在 balancer.h 中提供了abortCollectionDefragmentation()方法,用于用户主动中止某个集合的去碎片化进程。
第一阶段:MergeAndMeasureChunksPhase(合并并测量)
第一阶段由集合内所有块的 merge 与 datasize 命令组成:
- 在每个分片上,去碎片化策略会为每组连续块生成一个 merge 请求;
- 每次 merge 完成后,会为刚创建出的新块生成一个datasize命令;
- datasize 的结果会被持久化到
config.chunks条目中,供第二阶段使用;去碎片化结束后这些值会被清理; - merge 与 datasize 命令由辅助线程以并行方式下发,并为每个请求调度一个回调,将执行结果通知给去碎片化策略。
第二阶段:MoveAndMergeChunksPhase(迁移并合并)
第二阶段是整个流程中最复杂的阶段。它使用第一阶段计算出的数据量,为任何小于集合最大块大小 25% 的块(即"小块")生成迁移请求:
- 每个"小块"会被迁移到一个包含与其连续块的分片;
- 如果存在两个这样的候选分片,则按以下标准(按重要性降序排列)为候选分片打分:
- 该候选分片是否就是此块当前所在的分片?
- 该块是否比候选分片上将要与之合并的块更小?
- 将该块与候选分片上的块合并后,得到的块是否足够大、从而不再是"小块"?
- 该候选分片的数据量是否比另一个候选分片更少?
每回答一个"是",候选分片获得更高分数;第一个问题的权重最高,最后一个问题权重最低。得分最高的候选分片将成为该块迁移的目的分片。
在主线程完成迁移之后,去碎片化策略会为"迁移过来的块"和"它去相遇的那个连续块"生成一个 merge 请求,该 merge 动作由辅助线程执行。此过程循环进行,直到所有块都大于最大块大小的 25%。
在源码中,这一 25% 阈值由 balancer_defragmentation_policy.h 中的常量kSmallChunkSizeThresholdPctg = 25定义,各阶段通过DefragmentationPhase抽象接口(getNextPhase()、popNextMigration()、isComplete()等)统一驱动,保证每个集合的去碎片化状态机可以独立推进。
第三阶段:MergeChunksPhase(合并块)
最后一个阶段与第一阶段非常相似,但不再下发 datasize 命令。该阶段会为所有分片上的全部连续块生成 merge 请求。与第一阶段相同,所有这些命令都由辅助线程下发。
错误处理(Error Handling)
去碎片化过程中存在两类错误:
| 错误类型 | 处理方式 |
|---|---|
| 可重试错误(retriable) | 策略会重复下发同一操作,直到成功为止 |
| 不可重试错误(non-retriable) | 去碎片化会重新开始执行,退回到某个阶段的起点 |
具体回退规则:对于MergeAndMeasureChunksPhase与MergeChunksPhase,从该阶段起点重新开始;对于MoveAndMergeChunksPhase,则回退到MergeAndMeasureChunksPhase重新开始。
七、AutoMergerPolicy:自动合并策略
从v7.0开始,分片组件中新增了auto-merger(自动合并器):它会周期性地扫描块,识别出可合并(mergeable)的块并将它们合并(squash)到一起。
除非被显式禁用,自动合并器会按照 autoMergerIntervalSecs 指定的周期(可配置参数,默认 1 小时 = 3600 秒)检查是否存在可合并的块,并最终通过辅助线程下发mergeAllChunksOnShard动作。
自动合并策略实现的算法可以概括如下(伪代码与 auto_merger_policy.h 的注释描述一致):
While(true): -- Identify all the <shard, namespace> pairs for which there are mergeable chunks -- While(there are mergeable chunks): ---- For each shard: ------ For each namespace: -------- Schedule a mergeAllChunksOnShard action (max 10 actions per time) ------ Apply throttling of autoMergerThrottlingMS -- Sleep for autoMergerIntervalSecs其中"每次最多 10 个动作"的上限对应 auto_merger_policy.h 中的常量MAX_NUMBER_OF_CONCURRENT_MERGE_ACTIONS = 10,配合 autoMergerThrottlingMS(默认 15 秒)实现同一集合上两次合并命令的间隔控制。
可合并块(mergeable chunks)的定义
属于同一集合的两个或更多连续块,在满足以下条件时视为可合并:
- 它们归属于同一个分片;
- 它们的历史(history)可以被安全清理,而不会破坏事务(transactions)或快照读(snapshot reads)。
另外,jumbo 块不可合并,因为 jumbo 块无法参与迁移(合并前需要先移动块,而 jumbo 块被排除在常规迁移之外)。
形式上,两个或更多连续的非 jumbo 块必须满足以下条件才能被合并:
- 从未被迁移过;
或者
- 涉及其中任一块的最后一次迁移发生在:
- 距今超过
minSnapshotHistoryWindowInSeconds(快照历史窗口,默认 300 秒)并且 - 距今超过
transactionLifetimeLimitSeconds(事务生命周期上限,默认 60 秒)。
- 距今超过
该条件的物理意义是:只有确定"旧历史"已经超出事务与快照读所需保留的窗口,合并才安全——因为合并块会改写块历史,若仍有活动事务或快照读依赖旧历史,则可能导致读取失败。
合并示例
以下示例假设所有块的历史均为空,且没有任何块被标记为 jumbo,因此所有属于同一分片的连续区间都是可合并的。
考虑路由表中属于集合db.coll(分片键为x)的如下块区间:
| CHUNK | MIN | MAX | SHARD |
|---|---|---|---|
| A | x: 0 | x: 10 | Shard0 |
| B | x: 10 | x: 20 | Shard0 |
| C | x: 20 | x: 30 | Shard0 |
| D | x: 30 | x: 40 | Shard0 |
| E | x: 40 | x: 50 | Shard1 |
| F | x: 50 | x: 60 | Shard1 |
| G | x: 60 | x: 70 | Shard0 |
| H | x: 70 | x: 80 | Shard0 |
| I | x: 80 | x: 90 | Shard1 |
当自动合并器运行时,可合并块被合并,最终结果如下:
| CHUNK | MIN | MAX | SHARD |
|---|---|---|---|
| A-B-C-D | x: 0 | x: 40 | Shard0 |
| E-F | x: 40 | x: 60 | Shard1 |
| G-H | x: 60 | x: 80 | Shard0 |
| I | x: 80 | x: 90 | Shard1 |
可以看到:同一分片上的连续区间被各自合并为一个块(A-B-C-D、E-F、G-H),而I在 Shard1 上虽与 G-H 相邻,但由于分片归属不同(I 在 Shard1,G-H 在 Shard0),不满足"同一分片"条件,因此保持独立。
八、相关可调参数速查
结合 sharding_config_server_parameters.idl 与 README 描述,Balancer 及关联组件常用的可调参数汇总如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
balancerMigrationsThrottlingMs | 1000 ms | 相邻两个均衡轮次之间的最小间隔;设置过低可能导致 CRUD 因无法建立稳定分片版本而失败 |
balancerChunksSelectionTimeoutMs | 5000 ms | 每个均衡轮次中 Balancer 用于决策"迁移哪些范围"的最大耗时 |
chunkDefragmentationThrottlingMS | 1000 ms | 去碎片化期间相邻两次 merge/split 命令的最小间隔 |
autoMergerIntervalSecs | 3600 s(1 小时) | 自动合并器两次扫描之间的间隔 |
autoMergerThrottlingMS | 15000 ms(15 秒) | 同一集合相邻两次自动合并命令的最小间隔 |
autoMergerMaxTimeProcessingChunksMS | 500 ms | 自动合并器调度的mergeAllChunksOnShard命令在单次请求内查找可合并块的最大耗时 |
autoMergerMaxChunksToMerge | 100(最小值 2) | 单个mergeAllChunksOnShard请求中最多合并的块数 |
以上参数均支持在启动时(startup)或运行时(runtime)通过setParameter动态调整(autoMergerIntervalSecs为Atomic<int>,其余大多为Atomic<int32_t>/Atomic<bool>),为生产环境按需调优提供了灵活手段。
九、源码导览:从文档到实现
如果希望进一步深入源码,推荐按以下路径阅读:
- 线程与轮次:balancer.cpp 中的
_mainThread()(主线程轮次循环)与_consumeActionStreamLoop()(辅助线程消费动作流),对应 balancer.h; - 策略接口与迁移模型:balancer_policy.h(
MigrateInfo、SplitInfo、MigrationReason、CollectionDataSizeInfoForBalancing); - 三个策略实现:balancer_chunk_selection_policy.cpp、balancer_defragmentation_policy.cpp、auto_merger_policy.cpp;
- 集群统计:cluster_statistics.h 与 cluster_statistics_impl.cpp;
- 测试用例:同目录下的 auto_merger_policy_test.cpp、balancer_defragmentation_policy_test.cpp、balancer_chunk_selection_policy_test.cpp 与 cluster_statistics_mock.h 覆盖了三大策略的关键行为,是理解判定逻辑(如 3× max chunk size 阈值、25% 小块阈值、可合并块条件)最快的入口。
总结
MongoDB 的 Balancer 远不止"搬块"这么简单:它以双线程(同步主线程 + 异步辅助线程)为执行骨架,以三种动作策略为决策中枢,分别处理常规均衡、去碎片化与自动合并;其每一次迁移、拆分、合并决策都建立在集群数据分布统计(ClusterStatistics)与集合数据量信息(CollectionDataSizeInfoForBalancing)之上。理解 jumbo 块的成因与forceJumbo的逃生通道、去碎片化三阶段的回退语义、自动合并的安全条件(历史窗口 + 事务生命周期 + 非 jumbo),以及一张参数速查表,足以让运维与研发人员在生产集群中精准控制数据分布行为,并为阅读和二次开发 src/mongo/db/s/balancer 目录下的源码打下坚实基础。
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考