Kafka 扩容后新 Broker 空转 3 小时:加机器不是扩容,重分配才是
引言
去年双十一前扩容,我们从 3 台 Broker 扩到 6 台。
机器加完,监控上看新 Broker:CPU 2%,磁盘 IO 为零,网络流量只有心跳。3 小时后有人问了一句:"新机器是不是没加入集群?"
查了一圈,集群成员确实是 6 台,但 kafka-topics.sh --describe 里所有分区的 Replicas 还是老三台的 ID:
Topic: order-events Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
Topic: order-events Partition: 1 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1
...所有分区,一个新 Broker 都没有
Kafka 不会自动把分区搬到新 Broker。 加机器只是让集群多了一个可用节点,要把负载真正分过去,必须做分区重分配——这一步没人做,新机器就是白买的。
同一次扩容还差点出第二个事故:重分配方案生成后直接 --execute,没加 --throttle。跨机房带宽瞬间打满,在线消费的 P99 从 15ms 飙到 800ms,告警群已经开始响了,我们手动停掉命令才压住。
还有一次缩容,同事图省事直接 kill -9 下线一台 Broker,瞬间触发 200 多个分区的 Leader 切换,UnderReplicatedPartitions 冲到 300 多,持续了 7 分钟。
三次教训指向同一件事:Kafka 的高可用运维,难的不是命令,是顺序——先算什么、再动什么、每一步用什么验证、出事怎么退。 先放全流程总表:
| 阶段 | 核心动作 | 关键命令 | 验证信号 |
|---|---|---|---|
| 扩容前 | 算容量、算带宽、确认版本 | — | 磁盘/网络/分区数三笔账 |
| 加 Broker | 配置、格式化、启动、注册 | kafka-storage.sh、kafka-broker-api-versions.sh | 新节点出现在集群成员里 |
| 重分配 | 生成、审阅、限流执行、验证 | kafka-reassign-partitions.sh | 分区数均衡、URP 回落 |
| 扩分区 | 评估顺序性后扩容 | kafka-topics.sh --alter --partitions | 新分区有流量、无乱序 |
| 缩容 | 先迁移、再优雅停机 | 重分配 + controlled.shutdown | 目标 Broker 副本数归零 |
| 监控 | 指标、分级、去重、值班手册 | JMX + kafka-exporter + Alertmanager | URP/Offline/ISR/磁盘 |
一句话破题:扩容 = 加节点 + 重分配 + 验证三件事,缺一件新机器就空转;缩容 = 先搬家再拆房,顺序反了就是事故;而监控不是扩容之后的收尾,是整个过程的安全带——没有 URP 和 OfflinePartitions 的实时看板,你根本不知道自己走到了哪一步。
一、动手之前:先算三笔账
1.1 第一笔账:你到底缺什么
"集群扛不住了"这句话太模糊,先拆成三种诉求,动作完全不同:
| 诉求 | 判断依据 | 该做什么 |
|---|---|---|
| 吞吐不够 | Broker CPU 高、RequestHandler 空闲率低、生产延迟上升 | 扩 Broker + 重分配,或扩分区 |
| 容量不够 | 磁盘水位 >70%、retention 被迫缩短 | 扩 Broker + 重分配(分摊分区) |
| 容灾不够 | 同一分区的 3 个副本都在同一机架/同一可用区 | 扩到新机架 + 重分配时保证跨机架 |
| 单机风险 | 某台 Broker 分区数远超其他 | 重分配做均衡,不一定要加机器 |
最常见的误判是"吞吐不够就加机器"。如果瓶颈是分区数(消费者并行度上不去、单分区写入打满),加 Broker 不解决问题——因为分区不会自动变多,新 Broker 分不到活。先看瓶颈在分区还是节点,再决定加什么。
1.2 第二笔账:容量与带宽
磁盘需求 ≈ 峰值消息字节/秒 × retention 秒数 × 副本数 × 1.3(余量与索引)
网络需求 ≈ 峰值生产流量 × 副本数(写放大)+ 重分配期间的迁移流量
分区需求 ≈ 峰值 TPS ÷ 单分区安全处理吞吐
举个具体的:峰值 20MB/s 写入、保留 3 天、RF=3:
20MB/s × 86400 × 3 × 3 ≈ 15.5TB(裸容量)
加 30% 余量 ≈ 20TB,摊到 6 台 Broker,每台约 3.4TB
重分配的迁移时间也要提前算:
需要迁移的数据量 ÷ throttle(每 Broker)
例:单 Broker 迁移 500GB,限流 100MB/s → 约 85 分钟
如果迁移时间超过业务低峰窗口,就把 throttle 调大,或者拆成两晚做。 不要在执行到一半时才发现天亮了大促。
1.3 第三笔账:顺序性与版本
| 风险 | 触发动作 | 后果 | 规避 |
|---|---|---|---|
| key 的 hash 漂移 | 扩分区 | 同 key 新消息进新分区,顺序乱 | 顺序敏感 Topic 不在线扩分区 |
| 副本跨机架丢失 | 重分配方案没审 | 机房故障仍有丢副本风险 | 审阅 Replicas 的机架分布 |
| 迁移带宽挤占 | 重分配没限流 | 在线 P99 飙升 | --throttle 必设 |
| 副本数被改小 | 手改重分配 JSON | 低于 min.insync.replicas 直接拒写 | 执行前核对 RF 与 min ISR |
版本差异也要先确认,命令和监控指标都不一样:
| 项目 | ZooKeeper 模式(≤3.x 可选) | KRaft 模式(3.3+ 生产可用,4.0 起唯一) |
|---|---|---|
| 集群元数据 | ZK | Controller Quorum |
| 新 Broker 加入 | 启动即注册到 ZK | 必须用同一个 cluster.id 格式化 |
| Controller 指标 | ActiveControllerCount | kafka-metadata-quorum.sh |
| 扩容命令 | 基本一致 | 基本一致,--bootstrap-server 指向 Broker |
本文命令以 Kafka 3.7+ / KRaft 为准,ZK 模式的差异单独标注。
二、加 Broker:从配置到注册成功
2.1 配置清单
新 Broker 的 server.properties,五个字段必须逐项核对:
# 身份:集群内唯一,不能和现有 Broker 冲突
node.id=6
process.roles=broker
# 机架:容灾的基础,必须和现有 Broker 的 rack 规划一致
broker.rack=rack-b
# 网络:listeners 是绑定地址,advertised 是给客户端看的地址
listeners=PLAINTEXT://10.0.0.16:9092
advertised.listeners=PLAINTEXT://10.0.0.16:9092
# 数据目录:独立磁盘,不要和系统盘共用
log.dirs=/data/kafka-logs
# 必须和现有集群完全一致,否则注册被拒
controller.quorum.voters=1@10.0.0.11:9093,2@10.0.0.12:9093,3@10.0.0.13:9093
三个高频翻车点:
node.id重复:新 Broker 起不来,日志报Duplicate node.id。controller.quorum.voters写错:Broker 能启动,但一直连不上 Controller,表现为"进程活着、集群里没有"。advertised.listeners写成内网 IP 之外的地址:Broker 注册成功,但客户端连不上,元数据里能看到它、流量进不来。
2.2 格式化:必须用现有集群的 cluster.id
KRaft 模式下,新 Broker 的数据目录必须先格式化,而且必须用现有集群的 cluster.id,不能重新生成一个:
# 从现有 Broker 上取 cluster.id(两种方式任选)
kafka-metadata-quorum.sh --bootstrap-server kafka:9092 describe --status | grep ClusterId
# 或直接看现有 Broker 的 meta.properties
cat /data/kafka-logs/meta.properties | grep cluster.id
# 用这个 id 格式化新节点
kafka-storage.sh format -t <existing-cluster-id> -c /opt/kafka/config/server.properties
# 确认格式化结果里的 cluster.id 一致
kafka-storage.sh info -c /opt/kafka/config/server.properties
如果这里用 random-uuid 重新生成,新 Broker 会因为 cluster.id 不匹配被拒绝加入,日志里是 Cluster ID mismatch。这是 KRaft 扩容最常见的第一个坑。
2.3 启动与验证:三个信号缺一不可
# 启动
kafka-server-start.sh -daemon /opt/kafka/config/server.properties
# 信号一:进程与日志
tail -f /data/kafka-logs/server.log | grep -E "started|Registered|ERROR"
# 信号二:Broker 出现在集群成员列表里
kafka-broker-api-versions.sh --bootstrap-server kafka:9092 | grep 10.0.0.16
# 信号三:Controller 视角下节点已注册
kafka-metadata-quorum.sh --bootstrap-server kafka:9092 describe --status
三个信号都齐了,才能进入下一步。只看到进程活着不算成功——"进程活着但没注册"和"注册了但客户端连不上"是两种完全不同的故障,验证要分开做。
2.4 新 Broker 此时仍然是"空转"的
注册成功只代表它加入了集群,不代表它承担了任何分区。kafka-topics.sh --describe 里依然看不到它:
Topic: order-events Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
↑ 没有 6
这是正常的。Kafka 的分配是"粘性"的:已有分区不会因为新节点加入就自动迁移,否则每加一台机器都要全集群搬一次数据。把分区搬过去,是下一章的重分配要做的事。
三、分区重分配:扩缩容的核心动作
3.1 重分配到底在做什么
kafka-reassign-partitions.sh 做三件事:
- 计算一份新的"分区 → 副本列表"映射
- 让相关 Broker 按新映射复制数据(新增副本从 Leader 拉取)
- 复制完成后切换副本集合,多余的副本被删除
重分配前:P0 Replicas: 1,2,3
重分配后:P0 Replicas: 1,2,6
过程:
Broker 6 从 Leader(Broker 1)拉取 P0 的全量数据
→ 追上 LEO,进入 ISR
→ Controller 更新元数据:Replicas 变为 1,2,6
→ Broker 3 上的副本被标记删除
数据是"先复制、后切换、再删除",所以重分配过程中集群的副本数是暂时变多的,磁盘会短时上涨。 容量规划时要把这部分余量算进去。
3.2 生成方案
先写要迁移的 Topic 清单:
cat > /tmp/topics-to-move.json <<'EOF'
{
"topics": [
{"topic": "order-events"},
{"topic": "order-paid"},
{"topic": "inventory-events"}
],
"version": 1
}
EOF
生成方案,目标 Broker 列表是 1–6:
kafka-reassign-partitions.sh --bootstrap-server kafka:9092 \
--topics-to-move-json-file /tmp/topics-to-move.json \
--broker-list "1,2,3,4,5,6" \
--generate
输出是两段 JSON:第一段是当前分配,第二段是建议分配。把第二段单独保存:
# 把 "Proposed partition reassignment configuration" 之后的那段 JSON
# 保存为 /tmp/reassignment.json
建议 JSON 的结构:
{
"version": 1,
"partitions": [
{"topic": "order-events", "partition": 0,
"replicas": [1, 2, 6], "log_dirs": ["any", "any", "any"]},
{"topic": "order-events", "partition": 1,
"replicas": [4, 5, 6], "log_dirs": ["any", "any", "any"]}
]
}
3.3 审阅:工具给的方案不能直接用
--generate 只保证"尽量均衡",不保证你的业务约束。执行前必须人工核对四件事:
| 检查项 | 怎么查 | 不合格的表现 |
|---|---|---|
| 副本数 | 每条 replicas 的长度 | 少于 RF=3,或低于 min.insync.replicas |
| 机架分布 | 对照 broker.rack 映射 | 同一分区的 3 个副本全在同一机架 |
| 单 Broker 分区数 | 统计各 Broker 出现次数 | 某台 800 个、某台 200 个 |
| Leader 倾斜 | 执行后看 LeaderCount | 新 Broker 一个 Leader 都没有 |
一个实用的检查脚本(统计建议方案里每台 Broker 分到多少个副本):
python3 - <<'PY'
import json, collections
plan = json.load(open('/tmp/reassignment.json'))
counts = collections.Counter()
for p in plan['partitions']:
for r in p['replicas']:
counts[r] += 1
for broker, n in sorted(counts.items()):
print(f"broker {broker}: {n} replicas")
PY
六台 Broker 的理想结果应该是每台 300–400 个副本(按分区总数 3 副本折算)。如果某台超过均值 50%,把方案改一改再执行——重分配的代价是搬数据,搬错一次要再搬一次。
3.4 执行:throttle 是必选项,不是可选项
kafka-reassign-partitions.sh --bootstrap-server kafka:9092 \
--reassignment-json-file /tmp/reassignment.json \
--throttle 104857600 \
--execute
--throttle 的单位是 字节/秒,按 Broker 计。104857600 = 100MB/s。执行期间工具会自动给相关 Broker 设置:
leader.replication.throttled.rate=104857600
follower.replication.throttled.rate=104857600
执行结束后自动清除。不设 throttle 会发生什么,引言里已经演过了:跨机房带宽被复制流量吃满,在线消费 P99 从 15ms 飙到 800ms。
限流值怎么定:
| 场景 | 建议值 | 理由 |
|---|---|---|
| 同机房,万兆网络 | 200–500MB/s | 带宽富余,快速搬完 |
| 跨机房/跨可用区 | 50–100MB/s | 给在线流量留余量 |
| 业务高峰期 | 20–50MB/s | 宁可慢,不可抖 |
| 磁盘 IO 紧张 | 先解决磁盘,再重分配 | 复制和在线读写抢 IO |
3.5 验证:看命令输出,也看监控
# 命令侧
kafka-reassign-partitions.sh --bootstrap-server kafka:9092 \
--reassignment-json-file /tmp/reassignment.json --verify
输出会逐分区显示 completed 或 in progress。全部 completed 之后,再用三个信号确认:
# 信号一:新 Broker 出现在 Replicas 里
kafka-topics.sh --bootstrap-server kafka:9092 --describe --topic order-events
# 信号二:URP 回落到 0
# 监控面板或 JMX:
# kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions
# 信号三:各 Broker 分区数与 Leader 数均衡
kafka-topics.sh --bootstrap-server kafka:9092 --describe | \
awk '/Replicas:/{for(i=1;i<=NF;i++) if($i=="Replicas:"){split($(i+1),a,","); for(k in a) c[a[k]]++}} END{for(b in c) print b, c[b]}'
重分配期间 URP 短时上升是正常的——新副本正在追赶 Leader,ISR 还没齐。判断标准是"趋势":URP 应该随时间下降,而不是持续上涨。持续 15 分钟不降,说明复制跟不上(带宽不够或磁盘太慢),要么调大 throttle,要么先停下来排查。
3.6 中途断了怎么办
重分配是幂等的。命令被中断、机器重启、甚至你手动 Ctrl-C,已经完成的迁移不会回滚,重新执行同一份 JSON 即可从当前进度继续:
kafka-reassign-partitions.sh --bootstrap-server kafka:9092 \
--reassignment-json-file /tmp/reassignment.json \
--throttle 104857600 --execute
不要重新 --generate 一份新方案再执行——那会让已经搬好的数据再搬一次,白白多一轮流量。
3.7 自动化选项:Cruise Control
手工重分配的问题在于:人只能看到"现在均不均衡",看不到"未来会不会倾斜"。LinkedIn 开源的 Cruise Control 做三件事:
- 持续采集 Broker/分区/副本指标,做异常检测
- 给出重分配建议(含机架感知、副本分布、磁盘均衡)
- 可配置自动执行,带 throttle 和 dry-run
适合集群规模大(几十台以上)、Topic 多、人工维护不过来的场景。小集群(3–6 台)手工做一次半小时,没必要引入额外组件。
四、扩分区:加分区不等于扩容
4.1 两个铁律
kafka-topics.sh --bootstrap-server kafka:9092 \
--alter --topic order-events --partitions 48
第一条:只能加,不能减。 --alter --partitions 只接受比当前大的值,想缩回去只能重建 Topic。所以扩之前问一句:这个数字三年后还认吗?
第二条:存量数据不会迁移。 从 24 扩到 48,老消息原地不动,新消息按 hash(key) % 48 分布。这意味着:
扩容前:hash("order-89757") % 24 = 5 → P5,历史消息都在 P5
扩容后:hash("order-89757") % 48 = 17 → 新消息进 P17
同一个订单:老消息在 P5 排队,新消息在 P17 被另一个消费者处理
→ 顺序错乱
顺序敏感的业务,不要在线扩分区。 正确姿势是《Kafka 顺序消息实战》2.4 的三选一:规划一步到位、停写扩容、新旧分代。扩分区不是"免费加吞吐",它的价格是顺序性。
4.2 什么时候该扩分区
| 场景 | 该扩分区吗 | 原因 |
|---|---|---|
| 消费者并行度受分区数限制 | ✅ | 分区数 = 消费并行度上限 |
| 单分区写入打满 | ✅ | 拆细热点 |
| 只是 Broker CPU 高 | ❌ | 扩 Broker + 重分配更直接 |
| 顺序敏感 Topic 在线扩容 | ❌ | hash 漂移打乱顺序 |
| 分区已经几百个 | ⚠ | 先评估 Controller/元数据压力 |
4.3 扩 Broker 和扩分区,别搞混
| 动作 | 改变什么 | 数据迁移 | 生效方式 |
|---|---|---|---|
| 加 Broker | 集群节点数 | 需重分配才迁移 | 分钟级,需人工触发 |
| 重分配 | 分区在 Broker 间的分布 | 复制数据 | 分钟到小时 |
| 扩分区 | 分区总数(并行度上限) | 不迁移 | 立即,但新分区只接新消息 |
一句话:扩 Broker 解决"每台机器太累",扩分区解决"并行度不够",重分配解决"活没分匀"。
五、缩容:先搬家,再拆房
5.1 正确顺序
① 生成不含目标 Broker 的重分配方案
② 执行 + 验证,确认目标 Broker 上副本数为 0
③ 优雅停机(controlled shutdown)
④ 观察 URP / OfflinePartitions / Leader 数
⑤ 下线机器
5.2 第一步:把分区搬走
假设要下线 Broker 6,目标列表只写 1–5:
kafka-reassign-partitions.sh --bootstrap-server kafka:9092 \
--topics-to-move-json-file /tmp/topics-to-move.json \
--broker-list "1,2,3,4,5" --generate
保存建议方案后执行、验证,然后用 kafka-topics.sh --describe 确认没有任何分区的 Replicas 里还有 6:
kafka-topics.sh --bootstrap-server kafka:9092 --describe | grep -E "Replicas:.*\b6\b"
# 输出为空才安全
5.3 第二步:优雅停机
# server.properties(默认就是 true,确认别被改过)
controlled.shutdown.enable=true
controlled.shutdown.max.retries=3
controlled.shutdown.retry.backoff.ms=1000
# 用脚本停,不要 kill -9
kafka-server-stop.sh
# 或发送 SIGTERM,让 Broker 走完 controlled shutdown
kill -TERM <pid>
controlled shutdown 做的是:先把它作为 Leader 的分区领导权交给其他副本,再停止对外服务。这样客户端只会看到短暂的 Leader 切换,不会出现"分区无 Leader"。
直接 kill -9 的后果:Leader 无法交接,Controller 要等 replica.lag.time.max.ms(默认 30 秒)后才判定该 Broker 下线并重新选主。这 30 秒里,它领导的分区全部不可用——引言里 URP 冲到 300 的事故就是这么来的。
5.4 第三步:验证与观察
| 信号 | 正常 | 异常 |
|---|---|---|
| URP | 30 秒内回落到 0 | 持续 >0 超过 5 分钟 |
| OfflinePartitionsCount | 0 | >0 立即 P0 |
| LeaderCount | 其他 Broker 均匀接管 | 某台骤增,可能过载 |
| 客户端延迟 | 短暂抖动后恢复 | 持续升高 |
5.5 缩容后必须核对副本数
缩容最容易出的隐性问题是副本数被"缩"掉了。如果重分配方案里手动改过 replicas,或者下线过程中有副本没迁完,可能出现某些分区只剩 2 个副本。此时 min.insync.replicas=2 还能写,但容灾能力已经下降——再坏一台就拒写。
# 核对每个分区的副本数是否仍为 3
kafka-topics.sh --bootstrap-server kafka:9092 --describe | \
awk '/Replicas:/{n=split($0,a,"Replicas: "); split(a[2],b," "); split(b[1],c,","); if(length(c)<3) print $0}'
六、监控告警:整个过程的安全带
6.1 六个必须盯的指标
| 指标 | JMX(以你的版本为准) | 含义 | 异常阈值 |
|---|---|---|---|
| URP | kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions | 副本数不足的分区数 | >0 持续 5 分钟 |
| 离线分区 | kafka.controller:type=KafkaController,name=OfflinePartitionsCount | 没有 Leader 的分区数 | >0 立即告警 |
| ActiveController | kafka.controller:type=KafkaController,name=ActiveControllerCount | ZK 模式下应恒为 1 | ≠1 |
| ISR 收缩 | kafka.server:type=ReplicaManager,name=IsrShrinksPerSec | 副本掉出 ISR 的速率 | 持续 >0 |
| 请求处理空闲率 | kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent | Broker 是否过载 | <30% |
| 磁盘水位 | node_exporter | 数据盘使用率 | >80% |
KRaft 模式下没有 ActiveControllerCount 这个 ZK 时代的指标,改用:
kafka-metadata-quorum.sh --bootstrap-server kafka:9092 describe --status
# 看 LeaderId、CurrentVoters、MaxFollowerLag 是否正常
6.2 告警分级与收敛
按团队告警规范落地成表:
| 告警 | 表达式/条件 | 级别 | 通知策略 |
|---|---|---|---|
| OfflinePartitions > 0 | sum(...offlinepartitionscount) > 0 | P0 | 立即电话 |
| Broker 掉线 | absent(...partitioncount{instance=...}) | P0 | 立即电话 |
| URP 持续 >0 | sum(...underreplicatedpartitions) > 0,for: 5m | P1 | 工作时间 |
| URP 持续上升 | 5 分钟内增长 >20% | P0 | 立即电话 |
| ISR 持续收缩 | rate(...isrshrinks) > 0 持续 10m | P1 | 工作时间 |
| 请求处理空闲率低 | < 0.3 持续 5m | P1 | 工作时间 |
| 磁盘 >80% | node_exporter | P2 | 早晨汇总 |
| 磁盘 >90% | node_exporter | P1 | 工作时间 |
三条纪律(和《Kafka 监控告警实战》里的一致):
- P0 立即通知,P1 工作时间处理,P2 凌晨延迟到早晨
- 同一告警 5 分钟内只通知一次,靠
repeat_interval: 5m收敛 - Alertmanager 的时间区间默认按 UTC 解析,必须显式配置时区,否则"凌晨延迟"策略会整体错位 8 小时
6.3 Prometheus + kafka-exporter 配置
# prometheus.yml
scrape_configs:
- job_name: kafka
scrape_interval: 15s
static_configs:
- targets:
- '10.0.0.11:9308'
- '10.0.0.12:9308'
- '10.0.0.13:9308'
- '10.0.0.16:9308'
# alert-rules.yml
groups:
- name: kafka-ha
rules:
- alert: KafkaOfflinePartitions
expr: sum(kafka_controller_kafkacontroller_offlinepartitionscount) > 0
for: 1m
labels:
severity: P0
annotations:
summary: "Kafka 存在无 Leader 分区,服务已部分不可用"
- alert: KafkaUnderReplicated
expr: sum(kafka_server_replicamanager_underreplicatedpartitions) > 0
for: 5m
labels:
severity: P1
annotations:
summary: "Kafka 副本不足分区持续 5 分钟,检查 Broker 与磁盘"
- alert: KafkaBrokerDown
expr: absent(kafka_server_replicamanager_partitioncount{instance="10.0.0.16:9308"})
for: 2m
labels:
severity: P0
annotations:
summary: "Kafka Broker 10.0.0.16 无指标,疑似掉线"
指标名以 kafka-exporter 版本为准,上线前先用 /metrics 页面核对一遍。
6.4 重分配期间的专项看板
重分配时普通告警会"合理地"变多,值班同学容易误判。建议在 Grafana 上临时开一组面板:
| 面板 | 指标 | 正常形态 |
|---|---|---|
| 迁移流量 | rate(kafka_server_brokertopicmetrics_bytesin_total[5m]) | 稳定在 throttle 附近 |
| URP 趋势 | 上述 URP 指标 | 先升后降,最终归 0 |
| 各 Broker 分区数 | kafka_server_replicamanager_partitioncount | 逐步收敛到均值 |
| Leader 分布 | kafka_server_replicamanager_leadercount | 执行后趋于均匀 |
| 在线延迟 | 业务侧 P99 | 无显著变化 |
判断重分配是否健康,只看一条:URP 的斜率。 斜率为负,在追;斜率为零且大于 0,卡住了;斜率为正,在恶化,准备调大 throttle 或排查磁盘。
七、一次 3→6 扩容的完整时间线
把上面所有动作串起来,这是我们复盘后写进 Runbook 的标准流程:
| 时间 | 动作 | 验证点 | 失败回退 |
|---|---|---|---|
| T-1 天 | 容量/带宽/分区数三笔账;确认版本与顺序性 | 磁盘余量 >30%,迁移窗口足够 | 调整方案或延期 |
| T-0 | 新 Broker 配置、格式化、启动 | 三信号:进程、成员列表、Controller 注册 | 检查 node.id / cluster.id / quorum |
| T+5min | 生成重分配方案 | 当前 vs 建议两段 JSON 已保存 | 重新生成 |
| T+10min | 审阅:副本数、机架、单机分区数、Leader 倾斜 | 单机副本数偏离均值 <50% | 手改 JSON 或换方案 |
| T+15min | --execute --throttle 100MB/s | 迁移流量稳定、在线 P99 无变化 | 立即停命令,已迁移部分不回滚 |
| T+20min | 盯 URP 斜率 | 斜率为负 | 调大 throttle 或排查磁盘 |
| T+45min | --verify 全部 completed | 新 Broker 出现在 Replicas;URP=0 | 用同一 JSON 重跑 |
| T+50min | 检查分区/Leader 均衡 | 各 Broker 偏差 <20% | 微调方案再搬一轮 |
| T+55min | 更新监控 target、Runbook、容量台账 | 新 Broker 指标可见 | 补抓取配置 |
整场操作里最危险的两步是 T+15 的 --execute 和 缩容时的停机:前者可能打满带宽,后者可能触发 Leader 风暴。这两步必须有人在看板前盯着,不能脚本一跑就去干别的。
八、常见问题
8.1 新 Broker 加进来了,为什么一个分区都没有?
这是 Kafka 的正常行为:已有分区不会因为新节点加入自动迁移,否则每次扩容都要全量搬数据。必须手动执行 kafka-reassign-partitions.sh,把分区"请"过去。
8.2 --generate 的方案能直接执行吗?
不建议。它只做"尽量均衡",不保证副本跨机架、不保证单 Broker 分区数合理,也可能产生大量不必要的迁移。执行前至少核对 3.3 的四项,尤其是副本数和机架分布。
8.3 重分配期间 URP 上升,要不要立刻停?
不用。新副本正在追 Leader,URP 短时上升是预期行为。看趋势:下降就继续,15 分钟不降再排查。真正要立刻停的信号是在线业务 P99 显著恶化,或者 Broker 磁盘 IO util 持续 100%。
8.4 --throttle 的单位是什么?设多少合适?
字节/秒,按 Broker 计。100MB/s 写 104857600。同机房万兆可给 200–500MB/s;跨机房给 50–100MB/s;业务高峰给 20–50MB/s。核心原则是给在线流量留出至少 50% 的带宽余量。
8.5 重分配跑到一半断了,数据会不会坏?
不会。重分配是幂等的:已完成的副本切换不回滚,未完成的继续复制。重新执行同一份 JSON 即可,别重新 --generate。
8.6 扩分区为什么会导致顺序问题?
分区数变了,hash(key) % 分区数 的结果就变了。同一个 key 的历史消息留在老分区,新消息可能进新分区,两个分区由不同消费者并行处理,顺序就乱了。顺序敏感 Topic 要么停写扩容,要么一步规划到位。
8.7 缩容能直接停进程吗?
不能。直接 kill -9 会让它领导的分区在 replica.lag.time.max.ms(默认 30 秒)内无 Leader。正确做法是先把副本迁走,再用 kafka-server-stop.sh / SIGTERM 走 controlled shutdown,让它先交接领导权。
8.8 重分配能不能顺便改副本数?
可以,改 JSON 里每条 replicas 的长度即可。但有两条红线:不能低于 min.insync.replicas(否则直接拒写),不能把副本全放在同一机架(容灾失效)。改完记得同步更新容量台账。
8.9 KRaft 模式下还能看 ActiveControllerCount 吗?
这个 JMX 指标是 ZK 时代的。KRaft 用 kafka-metadata-quorum.sh describe --status 看 LeaderId、Voters、Follower 落后情况;监控上可以采集该命令输出,或用支持 KRaft 的 exporter 版本。
8.10 集群多大需要上 Cruise Control?
经验线是 10 台 Broker 以上、Topic 数超过 200、或者每周都要做重分配。低于这个规模,手工流程 + 本文的检查表足够;超过之后,人工审阅 JSON 的边际成本会快速超过引入 Cruise Control 的成本。
九、总结
决策树
集群要动,先问一句:缺什么?
├── 吞吐/容量不够
│ ├── 分区数还有余量、顺序不敏感 → 扩分区
│ └── 分区数到顶或顺序敏感 → 加 Broker + 重分配
├── 容灾不够(副本同机架)
│ └── 加新机架 Broker + 重分配时审机架分布
├── 单机热点(分区/Leader 倾斜)
│ └── 只做重分配,不加机器
└── 要下线机器
├── 先重分配把副本搬空
├── verify 确认 Replicas 无目标 Broker
└── controlled shutdown,绝不 kill -9
全流程速查表
| 步骤 | 命令/动作 | 必查项 |
|---|---|---|
| 加 Broker | 配置 → kafka-storage.sh format -t → 启动 | node.id 唯一、cluster.id 一致、quorum 正确 |
| 生成 | --generate --broker-list | 保存 current 与 proposed 两段 |
| 审阅 | 统计副本数/机架/Leader | 副本数=RF、跨机架、单机偏差<50% |
| 执行 | --execute --throttle | 带宽留 50% 余量 |
| 验证 | --verify + URP + describe | completed、URP 归零、分布均衡 |
| 缩容 | 重分配排除 → verify → 优雅停机 | Replicas 无目标 Broker、controlled shutdown |
| 监控 | URP/Offline/ISR/磁盘/Lag | P0 即时、P1 工作时段、5 分钟去重 |
一句话
Kafka 的高可用运维是一条有顺序的流水线:扩容先加节点、再搬分区、最后验证,少一步新机器就空转;缩容先搬空、再停机,反一步就是 Leader 风暴;重分配必须限流,扩分区必须考虑顺序,监控必须盯 URP 的斜率——命令谁都会敲,值钱的是"先做什么、做完看什么、出事退哪里"。
关键数据
- 新 Broker 注册成功 ≠ 承担分区,重分配才是扩容的最后一公里
--throttle单位是字节/秒/ Broker,跨机房建议 50–100MB/s- URP 短时上升是重分配的正常现象,看斜率不看绝对值
replica.lag.time.max.ms默认 30 秒,直接 kill Broker 会造成最长 30 秒的分区不可用- 扩分区只增不减,且不迁移存量数据,hash 漂移会破坏同 key 顺序
- 告警收敛:P0 即时、P1 工作时段、P2 延迟到早晨,同一告警 5 分钟内只发一次
给团队的建议
| 项目状态 | 建议 |
|---|---|
| 计划扩容 | 先按本文三笔账算一遍,再确认顺序性约束 |
| 扩容后新机器空转 | 不是故障,是漏了重分配,按第三章补 |
| 重分配经常影响业务 | 把 --throttle 写进 Runbook 模板,默认值 100MB/s |
| 缩容靠 kill | 立刻改成"迁移 + controlled shutdown"流程 |
| 监控只有 Lag | 先把 URP、OfflinePartitions、磁盘三条加上 |
| 集群超过 10 台 | 评估 Cruise Control,别再手工审 JSON |
| 值班总被同一告警吵 | 检查 Alertmanager 的 repeat_interval 与时区配置 |
互动话题:你们扩容 Kafka 时,新 Broker 空转过多久?重分配有没有因为没限流把在线业务拖慢过?评论区聊聊你的翻车现场,点赞最高的案例我拆成下一篇完整复盘。
参考资料
- Kafka 官方文档:Basic Operations(扩分区、重分配、下线)
- Kafka 官方文档:KRaft 模式与 Broker 配置
- Kafka 官方文档:Monitoring(JMX 指标清单)
- KIP-455:Replica Reassignment 管理 API
- KIP-853:KRaft 动态 Quorum
- Cruise Control:Kafka 自动均衡与异常检测
- kafka-exporter:Prometheus 指标采集
- 《Kafka 为什么这么快?——顺序写 + 页缓存 + 零拷贝 + 批量,四板斧拆解》
- 《Kafka 消息不丢失实战:生产者 ack + 副本 ISR + 消费者手动提交的完整配置》
- 《Kafka 顺序消息实战:同一个订单的消息必须按序消费——分区键设计 + 单分区单消费者》
- 《Kafka 消费积压实战:Lag 从 0 飙到百万——扩容、批量、多线程消费的正确姿势》
- 《Kafka 运维必备工具清单:这 5 个工具让排障快 10 倍》
- 《消息队列三巨头:Kafka vs RocketMQ vs Pulsar——百万级消息吞吐实测》
标题:Kafka 扩容后新 Broker 空转 3 小时:加机器不是扩容,重分配才是
作者:jiangyi
地址:http://jiangyi.space/articles/2026/11/01/1793462611279.html
公众号:服务端技术精选
- 引言
- 一、动手之前:先算三笔账
- 1.1 第一笔账:你到底缺什么
- 1.2 第二笔账:容量与带宽
- 1.3 第三笔账:顺序性与版本
- 二、加 Broker:从配置到注册成功
- 2.1 配置清单
- 2.2 格式化:必须用现有集群的 cluster.id
- 2.3 启动与验证:三个信号缺一不可
- 2.4 新 Broker 此时仍然是"空转"的
- 三、分区重分配:扩缩容的核心动作
- 3.1 重分配到底在做什么
- 3.2 生成方案
- 3.3 审阅:工具给的方案不能直接用
- 3.4 执行:throttle 是必选项,不是可选项
- 3.5 验证:看命令输出,也看监控
- 3.6 中途断了怎么办
- 3.7 自动化选项:Cruise Control
- 四、扩分区:加分区不等于扩容
- 4.1 两个铁律
- 4.2 什么时候该扩分区
- 4.3 扩 Broker 和扩分区,别搞混
- 五、缩容:先搬家,再拆房
- 5.1 正确顺序
- 5.2 第一步:把分区搬走
- 5.3 第二步:优雅停机
- 5.4 第三步:验证与观察
- 5.5 缩容后必须核对副本数
- 六、监控告警:整个过程的安全带
- 6.1 六个必须盯的指标
- 6.2 告警分级与收敛
- 6.3 Prometheus + kafka-exporter 配置
- 6.4 重分配期间的专项看板
- 七、一次 3→6 扩容的完整时间线
- 八、常见问题
- 8.1 新 Broker 加进来了,为什么一个分区都没有?
- 8.2 --generate 的方案能直接执行吗?
- 8.3 重分配期间 URP 上升,要不要立刻停?
- 8.4 --throttle 的单位是什么?设多少合适?
- 8.5 重分配跑到一半断了,数据会不会坏?
- 8.6 扩分区为什么会导致顺序问题?
- 8.7 缩容能直接停进程吗?
- 8.8 重分配能不能顺便改副本数?
- 8.9 KRaft 模式下还能看 ActiveControllerCount 吗?
- 8.10 集群多大需要上 Cruise Control?
- 九、总结
- 决策树
- 全流程速查表
- 一句话
- 关键数据
- 给团队的建议
- 参考资料
评论