Kafka 部署与高可用
从单机到集群
单机那套跑通之后,上生产要解决的问题变成三个:Broker 挂了怎么办、元数据存哪、容量怎么估。
前两个靠副本和 KRaft 解决,第三个靠算。这一篇按「3 节点 KRaft 集群」展开,控制面与数据面分离部署,是目前比较推荐的形态。
副本因子 3 就需要至少 3 个 Broker,否则 min.insync.replicas=2 加一台机器故障就没有可用副本了。3 个 Controller 是 KRaft 的推荐配置,能容忍 1 台 Controller 挂掉(多数派 2/3)。总共 6 台机器起步,小环境可以 3 台机器混合部署(每台同时当 controller 和 broker),但角色分离在扩缩容和故障隔离上更清晰。
集群拓扑
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ controller1 │ │ controller2 │ │ controller3 │
│ node.id=101│ │ node.id=102│ │ node.id=103│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
└──────── quorum(9093)──────────┘
│ 元数据
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ broker1 │ │ broker2 │ │ broker3 │
│ node.id=1 │ │ node.id=2 │ │ node.id=3 │
└─────────────┘ └─────────────┘ └─────────────┘
客户端走 9092,数据副本三份
职责划分:
- Controller:只存元数据(Topic、分区、副本分布、ACL、配置),不存消息数据。它们之间用 Raft 协议选举 Active Controller 并同步元数据。
- Broker:存消息、响应读写,从 Controller 拿元数据。Broker 之间互相发起了副本同步,但不再有「谁是老大」的选举负担。
Controller 配置
三个 Controller 的配置只有 node.id 和 log.dirs 不同:
# 只当 controller,不存消息
process.roles=controller
node.id=101
# quorum 成员列表,三个节点写的必须完全一致
controller.quorum.voters=101@kafka-1:9093,102@kafka-2:9093,103@kafka-3:9093
# controller 只需要一个监听器
listeners=CONTROLLER://:9093
controller.listener.names=CONTROLLER
listener.security.protocol.map=CONTROLLER:PLAINTEXT
# 元数据日志目录,数据量很小但要和消息目录分开
log.dirs=/data/kafka/controller
几个要点:
controller.quorum.voters必须三台完全一致,写错一个端口整个集群起不来。- Controller 不需要
advertised.listeners,它不接受客户端连接。 log.dirs里的数据是元数据日志,体积通常只有几十 MB 到几百 MB,但绝对不要和消息目录共用:消息把盘写满会连带把元数据写坏,整个集群直接失控。
Broker 配置
process.roles=broker
node.id=1
# 三台 broker 写同一份 quorum 列表
controller.quorum.voters=101@kafka-1:9093,102@kafka-2:9093,103@kafka-3:9093
# 客户端监听器
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://kafka-1:9092
inter.broker.listener.name=PLAINTEXT
listener.security.protocol.map=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
controller.listener.names=CONTROLLER
log.dirs=/data/kafka/logs
# 新 Topic 的默认值:副本 3、最小同步 2
num.partitions=6
default.replication.factor=3
min.insync.replicas=2
# 内部主题也必须 3 副本,否则控制器挂了消费组会乱
offsets.topic.replication.factor=3
transaction.state.log.replication.factor=3
transaction.state.log.min.isr=2
# 关掉自动建 Topic,避免打错名字凭空多出一堆分区为 1 的 Topic
auto.create.topics.enable=false
# 禁止非 ISR 副本当选 leader,宁可不服务也不丢数据
unclean.leader.election.enable=false
# 保留策略
log.retention.hours=168
log.retention.bytes=-1
log.segment.bytes=1073741824
# 线程与网络
num.network.threads=8
num.io.threads=16
num.replica.fetchers=4
replica.fetch.max.bytes=10485760
num.io.threads 一般设成磁盘数的 2 到 3 倍,num.network.threads 设成 CPU 核数。这两个值不是越大越好,线程太多反而增加上下文切换和锁竞争。
格式化与启动顺序
第一步:生成一个集群 ID,所有节点共用。
bin/kafka-storage.sh random-uuid
# 输出类似:xtzWWN4bTjitpL3kfd9s5g
第二步:每个节点各自格式化(用同一个 ID,配置文件按角色区分):
# controller 节点
bin/kafka-storage.sh format -t xtzWWN4bTjitpL3kfd9s5g \
-c config/kraft/controller.properties
# broker 节点
bin/kafka-storage.sh format -t xtzWWN4bTjitpL3kfd9s5g \
-c config/kraft/broker.properties
第三步:先起 Controller,全部就绪后再起 Broker。
# 三台 controller 上分别执行
bin/kafka-server-start.sh -daemon config/kraft/controller.properties
# 确认 quorum 选出了 leader
bin/kafka-metadata-quorum.sh --bootstrap-server kafka-1:9092 describe --status
输出里重点看三项:
ClusterId: xtzWWN4bTjitpL3kfd9s5g
LeaderId: 101
CurrentVoters: [101,102,103]
CurrentObservers: []
LeaderId 有值说明选主成功,CurrentVoters 三个都在说明 quorum 完整。然后再起 Broker:
bin/kafka-server-start.sh -daemon config/kraft/broker.properties
最后验证集群:
# 三个 broker 都应该出现在列表里
bin/kafka-broker-api-versions.sh --bootstrap-server kafka-1:9092 | grep -c "kafka-"
# 建个 3 副本 Topic 验证副本分布
bin/kafka-topics.sh --bootstrap-server kafka-1:9092 \
--create --topic ha-test --partitions 3 --replication-factor 3
bin/kafka-topics.sh --bootstrap-server kafka-1:9092 --describe --topic ha-test
describe 里每个分区的 Replicas 应该是三个不同的 node.id,Isr 也是三个。如果 Isr 只有一两个,先别往下走,说明副本同步没建立起来,通常是 advertised.listeners 或网络策略的问题。
KRaft 的存储目录带上集群 ID 之后就是「已初始化」状态,重复 format 会报错。要重建集群必须停服务、清空所有节点的 log.dirs、重新生成 ID。生产上这一步等于清库,操作前务必确认目录路径。
ZooKeeper 模式:历史项目怎么认
4.0 之前的集群大量是 ZooKeeper 模式,配置长这样:
broker.id=1
zookeeper.connect=zk1:2181,zk2:2181,zk3:2181/kafka
listeners=PLAINTEXT://:9092
advertised.listeners=PLAINTEXT://kafka-1:9092
log.dirs=/data/kafka/logs
区别不只是「多装一个 ZooKeeper」:
| 对比项 | ZooKeeper 模式 | KRaft 模式 |
|---|---|---|
| 元数据存储 | 外部 ZooKeeper 集群 | Controller 内部的 Raft 日志 |
| 控制器选举 | ZooKeeper 临时节点抢锁 | Raft 投票选 Leader |
| 部署组件 | Kafka + ZooKeeper 两套 | 只有 Kafka |
| 元数据规模上限 | 分区数到十万级开始吃力 | 百万级分区可支撑 |
| Controller 故障恢复 | 需要从 ZK 全量加载元数据,分钟级 | 从本地日志恢复,秒级 |
| 运维复杂度 | 两套系统、两套监控、两套备份 | 一套 |
| 版本状态 | 3.x 可用,4.0 已移除 | 3.3 起生产可用,4.0 唯一选择 |
判断手上的集群是哪种模式,最直接的办法是看启动脚本的配置文件:config/kraft/server.properties 是 KRaft,config/server.properties + zookeeper.connect 是 ZooKeeper。
已上线的 ZooKeeper 集群不建议仓促迁移。官方提供了 kafka-storage.sh 的迁移路径(先以 ZK 模式跑,再切 KRaft 的 bridge release),但涉及停机窗口和元数据校验,方案要单独评审。新集群一律 KRaft。
分区数规划
分区数是最容易拍脑袋、又最难反悔的决定。三个约束同时起作用:
吞吐。单个分区能跑多少,取决于磁盘和网络,实践上按 10MB/s 估比较稳(SSD、万兆网可以更高,但别按理论峰值算)。目标写入峰值除以单分区能力,就是下限:
峰值写入 150MB/s ÷ 单分区 10MB/s = 15 个分区
考虑增长和消费者并行度 → 取 24(能被 2、3、4、6、8 整除,方便分配)
消费者并行度。分区数是一个消费组能用的消费者数量上限。如果业务预期峰值要 12 个实例同时消费,分区数就不能低于 12,否则多出来的实例全程空转。
顺序性。要求严格的场景,分区数就是「能并行的 key 组数」。订单按订单号有序,想并行就只能按订单号分片;如果业务要求全局有序,那只能单分区,吞吐也就被锁死了。
经验上限:
| 指标 | 建议值 | 超过之后的症状 |
|---|---|---|
| 单 Broker 分区数 | 2000 以内 | 文件句柄耗尽、日志刷盘变慢 |
| 单集群分区数 | 20 万以内 | Controller 故障恢复变慢、元数据膨胀 |
| 单分区副本数 | 3(最多 5) | 写入放大、跨机架流量翻倍 |
| 单 Topic 分区数 | 按业务算,别超过 200 | 客户端元数据请求变重 |
每个分区每个副本至少占 3 个文件(.log、.index、.timeindex),还有目录本身。2000 分区 × 3 副本 × 3 文件 ≈ 1.8 万个句柄,再算上网络连接和日志,单进程轻松过 3 万。所以 LimitNOFILE 一定要调到 10 万,否则会在半夜莫名其妙地报 Too many open files。
磁盘与日志保留
磁盘布局。两种常见做法:
- JBOD(
log.dirs写多个盘):Kafka 会把不同分区分散到不同盘,容量利用率高、成本低。缺点是单盘故障只影响该盘上的分区,需要走副本恢复,运维时要能准确定位是哪块盘。 - RAID10:单盘故障对 Kafka 透明,但成本翻倍、容量利用率低。
大数据量场景一般选 JBOD,配合副本机制兜底,把「盘坏了」当成常态来设计。
保留策略。删数据只有两种触发条件,先到先删:
# 按时间:默认 7 天
log.retention.hours=168
# 按大小:默认不限,磁盘紧张时才用
log.retention.bytes=-1
# 多久检查一次过期日志段,默认 5 分钟
log.retention.check.interval.ms=300000
# 单个日志段大小,1GB 是默认值
log.segment.bytes=1073741824
这里要理解日志段(segment):分区不是一个巨大文件,而是切成一个个段,只有「当前正在写的段」是活跃的。过期删除和 compaction 都以段为单位,所以 log.segment.bytes 决定了删除的粒度——段太大,保留时间到了也要等段写满才删,实际占用会超出预期。
容量估算,一个可用的公式:
所需磁盘 = 日均写入量 × 保留天数 × 副本数 × 1.3(索引与预留)
举例:日均 1.5TB 原始数据,压缩后按 0.6 折算,保留 7 天,副本 3:
1.5TB × 0.6 × 7 × 3 × 1.3 ≈ 24.6TB
分摊到 3 台 Broker,每台 8TB 出头。这个数字还要和磁盘水位线对齐:Kafka 没有「磁盘用到 90% 自动清理」的机制,写满就是写入失败。所以单盘使用率控制在 70% 到 80% 就得扩容或缩短保留时间。
小结
- 生产集群按 3 Controller + 3 Broker 起步;Controller 只存元数据,
log.dirs必须和消息目录分开。 controller.quorum.voters三台必须完全一致;format 时集群 ID 必须一致;先起 Controller 再起 Broker。- 关键校验:
kafka-metadata-quorum.sh describe --status看 quorum,kafka-topics.sh --describe看 ISR 是否为 3。 - ZooKeeper 模式与 KRaft 的核心差异在元数据存哪、控制器怎么选;4.0 起只剩 KRaft,新集群没有选择困难。
- 分区数由吞吐、消费者并行度、顺序性三者共同决定,单 Broker 控制在 2000 以内,
LimitNOFILE调到 10 万。 - 磁盘按「日均写入 × 保留天数 × 副本数 × 1.3」估算,用 JBOD 摊成本,单盘水位压到 80% 以下。
集群起来了,接下来是每天都要干的事:看指标、盯 lag、清日志段、做迁移。下一篇《运维与监控》进入运维日常。