---
title: 动态再平衡
url: https://doc.liz6.com/distributed-systems/04-partitioning-and-routing/03-dynamic-rebalancing
locale: zh
area: distributed-systems
tags:
- distributed-systems
- 分区与路由
date: 2026-06-30
modified: 2026-07-16
description: 集群扩容或节点故障时,数据需要从旧分布迁移到新分布。好的再平衡只迁移必要的最少数据,且不中断读写——一致性哈希的虚拟节点让这件事变得去中心化。
---

# 动态再平衡

> 集群扩容或节点故障时,数据需要从旧分布迁移到新分布。好的再平衡只迁移必要的最少数据,且不中断读写——一致性哈希的虚拟节点让这件事变得去中心化。

## 概述

[一致性哈希](/distributed-systems/04-partitioning-and-routing/01-consistent-hashing.md)和[分片策略](/distributed-systems/04-partitioning-and-routing/02-sharding-strategies.md)解决了"数据怎么分布到节点"。但当集群扩容(加节点)或缩容(节点故障/下线)时,数据需要**从旧分布迁移到新分布**——这是再平衡(rebalancing)。一个糟糕的再平衡方案可能迁移远超必要的数据量,甚至在迁移期间拒绝服务。好的再平衡**只迁移必要的最少数据**,且**不中断读写**。

## 三种再平衡策略

### 固定分区:简单但粗糙

预创建远超节点数的分区(如每个节点 100 个分区,10 个节点共 1000 个分区),分区数不变。加节点时,从每个旧节点匀一些分区给新节点。

```
加节点前: Node-1: [p1..p100],  Node-2: [p101..p200], ... Node-10: [p901..p1000]
加节点后: Node-1: [p1..p90],   Node-2: [p91..p180],  ... Node-11(新): [p901..p990]
```

- 迁移粒度:整个分区(N 个 key 一起搬),不是逐 key。
- 缺点:**分区大小和节点数强绑定**——如果初始估算偏差(分区太大或太小),后续难调;分区数是静态的,不能随着数据量增长而分裂。

Redis Cluster 使用这个方案:16384 个固定 slot。

### 动态分区:分区随数据增长分裂

每个分区设一个大小上限(如 10GB),超过就分裂成两个新分区。节点数变化时,分区整体迁移到新节点——和固定分区类似,但分区数本身是动态的。

```
分区 p3 到达 10GB → 分裂成 p3a 和 p3b (各 ~5GB)
需要迁移时 → 整体搬 p3a 到新节点
```

- 优势:分区数和总数据量成正比,不会出现固定分区那种"预创建太多或太少"的问题。
- 代价:需要集中式协调者追踪分区元数据(哪个分区在哪个节点、哪个正在分裂)。HBase 用这个方案。

### 一致性哈希 + 虚拟节点:去中心化再平衡

[一致性哈希](/distributed-systems/04-partitioning-and-routing/01-consistent-hashing.md)里每个物理节点对应多个虚拟节点(vnode,如每节点 ~100 个),分布在整个哈希环上。加节点时,新节点插入环,带走它 vnode 覆盖的 token range(从相邻节点的 vnode 匀过来);删节点时,它的 vnode 对应的 range 被邻近 vnode 接管。

```
加节点 N11(带 vnode v1101..v1200) 到环:
  原来 token [5000, 5200] 在 N3 的 v303 上
  现在 v1101 覆盖 [5000, 5200] → 这个 range 从 N3 迁移到 N11
  只迁移 token 在新节点范围内的 key, 其他 key 不动
```

- **最小迁移量**:只迁移落在新 vnode 范围内的 key,不是整个分区。
- **去中心化**:每个节点通过 [Gossip 协议](/distributed-systems/05-members-and-discovery/02-gossip-protocol.md) 知道全局路由表,自己算自己该接管哪些 range,不依赖协调者。
- 代价:迁移粒度是单个 vnode 覆盖的 range,可能造成热点(某个 vnode 的 range 数据特别多)——解决办法是**动态调整 vnode 数**或**按负载分裂 vnode**(Cassandra 的 virtual node splitting)。

## 迁移工程:怎么在不中断的情况下搬数据

无论哪种策略,实际搬数据的过程有一些通用实践:

### 双写过渡

迁移期间,新旧两个节点**同时接受对该 range 的写**:

```
1. 元数据更新: range [5000, 5200] 从 N3 迁移到 N11
2. 过渡期: 读写仍然发给 N3, N3 把新写的数据流式传(N11)
3. N11 收到完成信号 → 元数据更新: [5000, 5200] 现归 N11
4. 后续读写直接发 N11
```

双写保证迁移期间不丢写:即使新节点还没完全就绪,旧节点仍在服务。Cassandra 的 `nodetool move` / `decommission` 就是这套。

### 限流与优先级

数据迁移吃网络和 IO——在业务高峰期全速迁移会拖垮正常请求:

- **限流**:限制迁移带宽(如 Cassandra 的 `stream_throughput_outbound_megabits_per_sec`)。
- **低谷迁移**:手动触发,在低峰执行。
- **增量再同步**:先传全量 snapshot,再传迁移期间的新写(类似 MySQL 的 binlog 追赶)。

### 一致性的坑

迁移期间,一条数据的 N 个副本可能**跨越新旧分布**——部分副本按旧路由,部分已按新路由。解法是**先迁移数据,再切换路由元数据**(原子切换,不是逐个副本切换)。或像 Cassandra 那样,读的时候去新旧两个位置都查一下(过渡期间有短暂的双路由)。

## 权衡与失败模式

- **全量重分布**:加一个节点就把所有 key 重新 hash → 迁移量 = 全量数据/N,大量无谓迁移。永远用一致性哈希或固定分区 + 搬分区。
- **热点 vnode**:某个 vnode 覆盖的 range 数据特别多,迁移时成为瓶颈 → 监控 vnode 大小,动态分裂。
- **迁移期间数据丢失**:先切路由再搬数据 → 始终先搬数据,再原子切路由。
- **迁移风暴**:一次性加大量节点,同时迁移可能压垮集群 → 逐个节点加,每次等再平衡完成再加下一个(Cassandra 推荐)。

## 参考

- **论文**: "Dynamo"(一致性哈希再平衡, 2007)

*Keywords: rebalancing, 固定分区, 动态分区, hash ring, virtual node, vnode, 数据迁移, 最小迁移量, 双写过渡, 渐进迁移, 限流, atomic routing switch, hotspot vnode*
