跳到主要内容

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、清日志段、做迁移。下一篇《运维与监控》进入运维日常。