《Java 消息中间件·Kafka 内核与客户端》13.1 集群拓扑与机架感知

三个 broker 配了两个机架,三副本的 topic 会怎么放?本节在三节点 KRaft 上实测:每个分区都是 2 个副本落在同一机架、1 个在另一机架,12 个分区无一例外——只要机架里只有一台 broker,机架感知就无法把 3 个副本分到 3 个故障域。文中还统计了跨机架复制链路的真实占比与 broker.rack 的配置边界。

describe 里看不到的东西

建一个三副本的 topic,然后看分区分布:

kafka-topics.sh --bootstrap-server mq-d1:9092 \
  --create --topic d1.rack-e3 --partitions 3 --replication-factor 3
kafka-topics.sh --bootstrap-server mq-d1:9092 --describe --topic d1.rack-e3

本机实测(Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 三节点 / 2026-10-29):

Topic: d1.rack-e3  PartitionCount: 3  ReplicationFactor: 3  Configs: min.insync.replicas=2
  Partition: 0  Leader: 2  Replicas: 2,3,1  Isr: 2,3,1  Elr:   LastKnownElr:
  Partition: 1  Leader: 3  Replicas: 3,1,2  Isr: 3,1,2  Elr:   LastKnownElr:
  Partition: 2  Leader: 2  Replicas: 2,3,1  Isr: 2,3,1  Elr:   LastKnownElr:

输出里只有 broker id:1、2、3。哪一个 broker 在哪个机架,describe 不告诉你。

要知道机架,得换一个命令:

kafka-broker-api-versions.sh --bootstrap-server mq-d1:9092 | grep -E "^mq-d"

本机输出:

mq-d3:9092 (id: 3 rack: rack-b isFenced: false) -> (
mq-d1:9092 (id: 1 rack: rack-a isFenced: false) -> (
mq-d2:9092 (id: 2 rack: rack-a isFenced: false) -> (

把两张表拼起来,才看得到真正的拓扑:

brokerrack该机架内的 broker 数
1rack-a2(1、2)
2rack-a2(1、2)
3rack-b1(3)

这是本节实验刻意做出来的形状:两个机架,2:1 分布,不是三个机架。只有这种不对称的拓扑,才能看出「机架感知」在约束不够时如何妥协。

broker.rack 是什么

broker.rack 是 broker 的一个静态配置项,值是一个任意字符串。Kafka 不理解它的语义,只做字符串比较:值相同就算同一个故障域。

它可以在 server.properties 里写:

broker.rack=rack-a

也可以在容器里用环境变量传入(本节实验就是这么做的):

environment:
  KAFKA_BROKER_RACK: rack-a

它不会自动发现。Kafka 不会去问云厂商「这台机器在哪个可用区」,也不会读机柜编号。你写什么它就是什么,写错了也没有任何报错。

这带来两个直接后果:

  1. 标签是运维契约,不是事实。 迁移机器、换可用区、重建集群时,如果忘了同步 broker.rack,副本分布会按错误的地图计算,而且从 describe 输出里完全看不出来。
  2. 不是所有 broker 都有 rack 时,机架感知会退化。 分配器需要知道目标 broker 的 rack 才能做机架维度的分散;缺失 rack 的 broker 无法参与这个约束。所以生产集群要么全配,要么全不配,不要一半一半。

改 broker.rack 需要重启 broker 才生效。它是一个静态配置,不在动态配置的命令清单里。

三副本落在两个机架上

回到上面的 describe。把 broker id 换成机架:

分区副本(顺序即 preferred leader 优先序)机架构成
02,3,1a, b, a
13,1,2b, a, a
22,3,1a, b, a

三个分区都是「两个副本在 rack-a、一个副本在 rack-b」。没有例外。

这不是偶然。把约束写出来就明白了:

  • 同一分区的多个副本必须落在不同的 broker 上(这是副本的定义);
  • rack-b 里只有 broker 3 一台机器,所以 rack-b 最多只能放 1 个副本;
  • 副本总数是 3,rack-b 放满 1 个之后,剩下 2 个只能放进 rack-a。

结论很硬:3 副本、2 机架(其中一 rack 只有 1 台 broker)时,必然有一对副本同机架。 机架感知能做的只是「尽量分散」,不是「保证分散」。

那如果两个机架各有两台以上 broker 呢?三副本仍然只能做到 2:1,因为机架只有两个。只有当机架数不少于副本数时,才可能每个副本各占一个机架。本机只有两个机架,这一点无法实测验证,只能给出预期形态:

# 预期(未在本机执行,需要 3 个机架的拓扑)
Partition: 0  Replicas: 1,2,3    # 1/2/3 分别在 rack-a/b/c

想做这个实验,得另起一个每个 broker 一个 rack 的三节点集群,再建 RF=3 的 topic。本书的三节点集群固定为 2:1,所以这一条只作说明。

二副本的对比:全部跨机架

同样三台机器,把副本数降到 2:

kafka-topics.sh --bootstrap-server mq-d1:9092 \
  --create --topic d1.rack-e2 --partitions 3 --replication-factor 2
kafka-topics.sh --bootstrap-server mq-d1:9092 --describe --topic d1.rack-e2

本机实测:

  Partition: 0  Leader: 1  Replicas: 1,3  Isr: 1,3
  Partition: 1  Leader: 3  Replicas: 3,2  Isr: 3,2
  Partition: 2  Leader: 1  Replicas: 1,3  Isr: 1,3

三个分区都是「一个 rack-a、一个 rack-b」。RF=2 时,两个机架刚好够用,机架感知达到了它的设计目标:任何一个机架整体断电,都还有一个副本活着。

把 RF=2 和 RF=3 放在一起看,机架感知的真实价值就清楚了:

副本数机架数最坏情况下单机架故障的影响能否承受一个机架全灭
22每个分区丢 1 个副本能,还剩 1 个
32坏 rack-a 时每个分区丢 2 个,坏 rack-b 时丢 1 个能,但最坏只剩 1 个
33每个分区丢 1 个副本能,还剩 2 个,仍可写

注意第三行「3 副本 3 机架」和「3 副本 2 机架」的区别:两者都能扛住一个机架故障,但前者故障后每个分区还剩 2 个副本,仍满足 min.insync.replicas=2,可以继续写;后者如果坏的是 rack-a,每个分区只剩 1 个副本,写入立刻被拒。

机架感知不增加副本数,它决定的是「故障时还剩几个」。

分区多了以后,分布均衡吗

三个分区看不出统计规律,加到 12 个:

kafka-topics.sh --bootstrap-server mq-d1:9092 \
  --create --topic d1.rack-many --partitions 12 --replication-factor 3

本机对 describe 输出做统计(把 broker 映射成机架后逐分区计数):

d1.rack-many  partitions=12
机架构成: {('a','a','b'): 12}
leader 分布: broker1=3, broker2=2, broker3=7
leader→follower 复制链路: 同机架 5 条, 跨机架 19 条

两件事值得注意。

第一,12 个分区的机架构成完全一致:12 个都是 2a+1b。 机架感知在分区之间没有「换边」——不会出现某些分区是 2b+1a。原因还是 rack-b 只有一台 broker:它最多贡献一个副本,剩下的两个只能来自 rack-a。所以这种不均衡不是随机波动,是拓扑决定的。

第二,leader 分布明显倾斜:broker 3 拿了 7 个分区的 preferred leader,broker 1 只有 3 个,broker 2 只有 2 个。 理想情况是 4/4/4。broker 3 是 rack-b 里唯一的机器,在副本列表里经常排在第一位,于是成了多数分区的 preferred leader。

这个倾斜会带来实际后果:

  • 写入压力集中在 broker 3 上,它的网卡和磁盘先饱和;
  • broker 3 重启时,7 个分区同时触发选举,比均匀分布时的影响面大得多;
  • 跨机架流量也随之变大,下一节算这笔账。

生产上如果发现这种倾斜,处理办法不是调机架标签,而是重新分配分区(kafka-reassign-partitions.sh)或者在机架内部增加 broker 数量,让每个机架的 broker 数接近。

跨机架流量的代价

副本分布决定了复制链路走不走跨机架网络。对每个分区,leader 要向 RF-1 个 follower 推送数据;链路两端在同一个机架就不出机架,否则就是跨机架流量。

用上面 12 个分区的实测数据算:

每个分区 2 条 follower 链路,共 24 条
其中 19 条跨机架,5 条同机架
跨机架比例 = 19 / 24 = 79.2%

也就是说,每写入 1 MB 数据,平均产生约 1.58 MB 的跨机架复制流量(19/12)。如果机架之间是可用区,这 1.58 MB 就是实打实的出网带宽和跨 AZ 费用。

这个比例之所以这么高,正是因为 leader 大多在 rack-b:leader 在 rack-b 时,两个 follower 都在 rack-a,两条链路全跨机架;leader 在 rack-a 时,只有 rack-b 的那条链路跨机架。7 个 leader 在 rack-b、5 个在 rack-a,于是 7×2 + 5×1 = 19。

如果三个机架各一台 broker、RF=3,每条链路都跨机架,比例是 100%,但每个分区只有 2 条链路,绝对量是 2 × 写入量;本机这种 2:1 拓扑是 1.58 × 写入量,绝对量反而小一点。跨机架比例高不等于跨机架流量一定大,要乘上链路数一起看。

除了带宽,跨机架还有延迟。同机架往返通常是几十微秒级,跨可用区是毫秒级。这个延迟直接进入 acks=all 的写入路径:leader 要等所有 ISR 副本确认,最慢的那条跨机架链路决定写入延迟的下限。

所以选机架粒度时有一个基本取舍:

  • 按物理机架划分:故障域小,跨机架延迟低,但一个机柜断电仍然可能带走多台机器;
  • 按可用区划分:故障域大(整个 AZ 断电),跨 AZ 延迟和费用高,但能扛住机房级故障。

多数云上部署选后者,因为真正要防的是 AZ 级故障。物理机架感知在自建机房更有意义。

生产环境里的「机架」到底是什么

broker.rack 的值是字符串,所以它可以表示任何故障域。常见的三种用法:

用法示例值防的是什么代价
可用区az-1、cn-hangzhou-h机房/可用区级故障跨 AZ 延迟与流量费
物理机架rack-07、row3-rack12机柜断电、交换机故障故障域较小,通常同机房
复合标签az-1-rack-07同时保留两级信息只能表达一层约束

第三种看起来最灵活,实际最危险:Kafka 只做字符串相等比较,az-1-rack-07 和 az-1-rack-08 是两个完全独立的故障域,Kafka 不知道它们同属 az-1。复合标签不会让 Kafka 理解层级,只会让每个机架变成一台机器。 结果就是副本被分散到每个机柜,但一个 AZ 断电仍然全灭。

如果确实要两级容错,正确做法是选一级作为 broker.rack(通常选 AZ),再用别的手段(跨集群复制、多活)处理更细的故障域。不要指望一个字符串表达两层语义。

还有一个容易忽略的点:机架数量要和副本数对齐。RF=3 配两个机架,必然出现同机架副本;RF=3 配三个机架,才能保证每个副本一个故障域。规划时先确定能接受多大的故障域,再定 RF 和机架数,顺序不能反。

一次可复现的检查流程

把本节的操作整理成一套检查流程,换到别的集群也能用:

# 1. 看每个 broker 的 rack
kafka-broker-api-versions.sh --bootstrap-server mq-d1:9092 | grep -E "^mq-d"

# 2. 看 topic 的副本分布
kafka-topics.sh --bootstrap-server mq-d1:9092 --describe --topic d1.rack-e3

# 3. 人工把 broker id 映射成 rack,逐分区检查
#    重点看:是否存在同机架的两个副本、leader 是否倾斜

# 4. 统计跨机架链路占比(分区多时才有意义)

判断标准只有两条:

  1. 每个分区的副本是否分布在尽可能多的机架上,上限是 min(RF, 机架数)。本机 min(3,2)=2,每个分区确实落在 2 个机架上,这一条是达标的。
  2. leader 是否均匀落在各 broker 上。 本机按机架看是 7:5,还算均衡;按 broker 看是 7:3:2,明显倾斜。

第 1 条达标不等于可用性达标。本机每个分区只覆盖 2 个机架,坏 rack-a 时每个分区只剩 1 个副本,不满足 min.insync.replicas=2,写入会被拒。想让「坏一个机架仍可写」,要么增加到 3 个机架,要么在每个机架放足够多的 broker 并提高 RF,让坏一个机架后仍剩 2 个副本。生产上遇到这种拓扑,要么加机架,要么接受「同机架有一对副本」并相应调整可用性预期。

三个实践判断

第一,机架数少于副本数时,机架感知只能做到「尽量分散」。 本机 12 个分区全部是 2a+1b,证明了这一点。不要把「配了 broker.rack」当成「副本一定跨机架」,要用 describe 输出实际核对。

第二,机架内的 broker 数量决定副本上限。 rack-b 只有一台 broker,就最多承载一个副本;想让三个副本真正分散到三个故障域,每个机架至少要有能放下一个副本的 broker。机架数量够、但某个机架只有一台机器,同样会退化成 2:1。

第三,跨机架流量的账要用链路数算,不是用比例算。 本机 79.2% 的链路跨机架,折算成每写入 1 MB 产生 1.58 MB 跨机架流量。按可用区计费时,这个系数直接乘在账单上。

小结

三句话收束这一节:

  1. broker.rack 是一个手工维护的字符串标签,Kafka 只做相等比较,不自动发现,也不理解层级。
  2. 机架感知的目标是「故障域分散」,不是「副本一定跨机架」。 机架数少于副本数、或某个机架 broker 太少时,必然出现同机架副本;本机 3 副本 2 机架,12 个分区全部是 2:1。
  3. 副本分布同时决定故障后的剩余副本数和跨机架流量。 前者影响可用性,后者影响延迟和账单,两个都要在规划时算清楚。

拓扑是静态的,运维动作是动态的。下一节把镜头对准最常见的一个动作:滚动重启。三个节点按什么顺序停、每次等多久、min.insync.replicas=2 的 topic 在重启窗口里能承受几台机器同时离线,都有实测数字。

阅读导航:上一节 12.3 容量规划 | 下一节 13.2 滚动重启与最小可用

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收