Kafka 集群部署
本文记录一个生产可用的 Kafka KRaft 集群部署流程:3 节点 Controller + Broker 合一(Combined 模式),以及独立 Controller 和 Broker 的 Separated 模式对比。
架构规划
Combined 模式(小集群推荐)
3 节点兼具 Controller 和 Broker 角色,适合中小规模集群:
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ Kafka Node 1 │ │ Kafka Node 2 │ │ Kafka Node 3 │
│ controller + broker │ │ controller + broker │ │ controller + broker │
│ :9092 │ │ :9093 │ │ :9094 │
│ CONTROLLER::9092 │ │ CONTROLLER::9093 │ │ CONTROLLER::9094 │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘
▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲
│ │ │ │ │ │ │ │ │
Partition-0 Leader Partition-1 Leader Partition-2 Leader
(rf=3, ISR: 1,2,3) (rf=3, ISR: 1,2,3) (rf=3, ISR: 1,2,3)
KRaft Quorum: Node 1 (Leader) + Node 2 + Node 3 → 元数据管理
Broker Cluster: Node 1 + Node 2 + Node 3 → 消息存储Separated 模式(大集群推荐)
Controller 和 Broker 分离部署,职责清晰,适合大规模集群:
Controller Layer (KRaft Quorum) Broker Layer (Data Plane)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Controller 1 │ │ Controller 2 │ │ Controller 3 │ 元数据 + Leader 选举
│ :9093 │ │ :9093 │ │ :9093 │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└───────────────┼───────────────┘
│ 管理元数据
┌───────────────┼───────────────┐
│ │ │
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Broker 1 │ │ Broker 2 │ │ Broker 3 │ 数据存储 + 客户端读写
│ :9092 │ │ :9092 │ │ :9092 │
└──────────────┘ └──────────────┘ └──────────────┘| 对比 | Combined 模式 | Separated 模式 |
|---|---|---|
| 节点数 | 3 | ≥ 6(3 Controller + 3 Broker) |
| 资源隔离 | 无,Controller/Broker 共享 CPU/内存 | 有,互不影响 |
| 故障隔离 | Controller 故障影响所在 Broker | 完全隔离 |
| 适用规模 | < 10 Broker,< 100K Partition | > 10 Broker,> 100K Partition |
| 滚动重启 | Broker 重启不影响 Controller 稳定性 | 独立滚动,风险更低 |
生产环境绝大多数场景 Combined 模式足够。Partition 超过 10 万或物理机部署时再考虑 Separated。
节点角色
| 角色 | 说明 |
|---|---|
| Controller | KRaft Quorum 成员,负责元数据管理(Topic/Partition 创建、Leader 选举、ISR 管理) |
| Broker | 数据面,负责消息存储和客户端请求处理 |
| Observer | KRaft 中的只读副本,不参与选举(v3.7+) |
Controller|元数据管理
Controller 负责管理集群所有元数据,通过 KRaft Raft 协议在 Quorum 之间同步:
| 元数据 | 说明 |
|---|---|
| Cluster metadata | Broker 注册、Topic/Partition 信息、配置 |
| Leader 选举 | 检测 Broker 故障,从 ISR 中选举新 Leader |
| ISR 管理 | 决定 Follower 是否在 ISR 中 |
| Partition 分配 | Topic 创建和扩容时分配 Partition |
KRaft Controller 将元数据存储在
__cluster_metadataTopic 中(而非外部的 ZooKeeper),数据与 Broker 使用相同的日志存储机制。
Broker|数据存储
Broker 负责实际的消息存储和客户端请求处理:
| 职责 | 说明 |
|---|---|
| 消息存储 | 接收 Producer 消息,写入本地日志文件 |
| 请求处理 | 处理 Produce、Fetch、Commit 等请求 |
| 副本同步 | Leader 负责读写,Follower 从 Leader 同步数据 |
| 数据删除 | 按 retention.ms / retention.bytes 清理过期数据 |
Partition Leader 分布
Topic "orders" (3 Partitions, replication-factor=3)
Node 1 Node 2 Node 3
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ P0 (Leader) │ │ P1 (Leader) │ │ P2 (Leader) │
│ P1 (Follower) │ │ P2 (Follower) │ │ P0 (Follower) │
│ P2 (Follower) │ │ P0 (Follower) │ │ P1 (Follower) │
└──────────────────┘ └──────────────────┘ └──────────────────┘
写入 P0 在这里 写入 P1 在这里 写入 P2 在这里Controller 负责将不同 Partition 的 Leader 均匀分布到各 Broker,实现负载均衡。
组件端口规划
| 组件 | 端口 | 用途 | 数量 |
|---|---|---|---|
| Broker Listener | 9092 | 客户端(Producer/Consumer)连接 | ≥ 3 |
| Controller Listener | 9093 | KRaft Quorum 间通信 | 3(Combined 模式与 Broker 共存) |
部署前提
网络连通性
每个节点都需要能够访问所有其他节点的 Controller 端口和 Broker 端口。确保防火墙/安全组放行相关端口。
主机名
生产环境使用 DNS 主机名而非 IP。所有节点的 advertised.listeners 应配置为客户端可达的地址。
环境准备
# 创建数据目录
sudo mkdir -p /data/kafka/{node1,node2,node3}
sudo chown -R $USER:$USER /data/kafka
# 创建日志目录
sudo mkdir -p /var/log/kafka
sudo chown -R $USER:$USER /var/log/kafka
# 确保 JDK 17+
java -versionCombined 模式部署(3 节点)
生成 Cluster UUID
KAFKA_CLUSTER_ID=$(kafka-storage.sh random-uuid)
echo $KAFKA_CLUSTER_ID
# 示例: 7XOkR-LDRj6i0QVkYXJzLgNode 1 配置
# /opt/kafka/config/kraft/node1.properties
process.roles=broker,controller
node.id=1
controller.quorum.voters=1@node1:9093,2@node2:9093,3@node3:9093
# Broker 监听
listeners=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
advertised.listeners=PLAINTEXT://node1:9092
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
controller.listener.names=CONTROLLER
# 日志目录
log.dirs=/data/kafka/node1
# 集群默认配置
num.partitions=3
default.replication.factor=3
min.insync.replicas=2
offsets.topic.replication.factor=3
transaction.state.log.replication.factor=3
transaction.state.log.min.isr=2Node 2、Node 3 只需修改 node.id、advertised.listeners 和 log.dirs。
格式化并启动
# 每个节点执行
kafka-storage.sh format -t $KAFKA_CLUSTER_ID \
-c /opt/kafka/config/kraft/node1.properties
# 启动
kafka-server-start.sh -daemon /opt/kafka/config/kraft/node1.properties# 检查 KRaft 元数据状态
kafka-metadata-quorum.sh --describe --status \
--bootstrap-server node1:9092,node2:9092,node3:9092关键配置解读
| 配置 | 推荐值 | 说明 |
|---|---|---|
process.roles | broker,controller | Combined 模式 |
controller.quorum.voters | 全部 Controller 节点列表 | 格式: nodeId@host:port |
default.replication.factor | 3 | 默认 3 副本 |
min.insync.replicas | 2 | 至少 2 个 ISR 才写入(配合 acks=all) |
offsets.topic.replication.factor | 3 | Consumer Offset Topic 副本数 |
transaction.state.log.replication.factor | 3 | 事务状态日志副本数 |
auto.create.topics.enable | false | 禁止自动创建 Topic |
num.network.threads | CPU 核数 × 0.5 | 网络线程数 |
num.io.threads | CPU 核数 × 2 | I/O 线程数 |
log.retention.hours | 168(7 天) | 消息保留时间 |
log.segment.bytes | 1073741824(1GB) | 日志段大小 |
compression.type | producer | 保留 Producer 指定的压缩格式 |
Docker Compose 部署
version: '3.8'
services:
kafka1:
image: apache/kafka:4.0.0
container_name: kafka1
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: "broker,controller"
KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka1:9093,2@kafka2:9093,3@kafka3:9093"
KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093"
KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092"
KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT"
KAFKA_NUM_PARTITIONS: 3
KAFKA_DEFAULT_REPLICATION_FACTOR: 3
KAFKA_MIN_INSYNC_REPLICAS: 2
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
KAFKA_CLUSTER_ID: "7XOkR-LDRj6i0QVkYXJzLg"
volumes:
- kafka1-data:/var/lib/kafka/data
kafka2:
image: apache/kafka:4.0.0
container_name: kafka2
ports:
- "9093:9092"
environment:
KAFKA_NODE_ID: 2
KAFKA_PROCESS_ROLES: "broker,controller"
KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka1:9093,2@kafka2:9093,3@kafka3:9093"
KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093"
KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9093"
KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT"
KAFKA_NUM_PARTITIONS: 3
KAFKA_DEFAULT_REPLICATION_FACTOR: 3
KAFKA_MIN_INSYNC_REPLICAS: 2
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
KAFKA_CLUSTER_ID: "7XOkR-LDRj6i0QVkYXJzLg"
volumes:
- kafka2-data:/var/lib/kafka/data
kafka3:
image: apache/kafka:4.0.0
container_name: kafka3
ports:
- "9094:9092"
environment:
KAFKA_NODE_ID: 3
KAFKA_PROCESS_ROLES: "broker,controller"
KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka1:9093,2@kafka2:9093,3@kafka3:9093"
KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093"
KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9094"
KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT"
KAFKA_NUM_PARTITIONS: 3
KAFKA_DEFAULT_REPLICATION_FACTOR: 3
KAFKA_MIN_INSYNC_REPLICAS: 2
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2
KAFKA_CLUSTER_ID: "7XOkR-LDRj6i0QVkYXJzLg"
volumes:
- kafka3-data:/var/lib/kafka/data
volumes:
kafka1-data:
kafka2-data:
kafka3-data:Docker Compose 适合本地开发和功能验证。生产环境推荐使用物理机/虚拟机或 Kubernetes StatefulSet。
Kubernetes 部署
Kafka 在 K8s 上有两种方式:Operator(推荐)和手动 StatefulSet。
StatefulSet(手动)
apiVersion: v1
kind: Service
metadata:
name: kafka-broker
namespace: kafka
spec:
clusterIP: None # Headless Service — 必须,提供 Pod 直连 DNS
selector:
app: kafka
ports:
- name: plaintext
port: 9092
- name: controller
port: 9093
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
namespace: kafka
spec:
serviceName: kafka-broker
replicas: 3
podManagementPolicy: Parallel
selector:
matchLabels:
app: kafka
template:
metadata:
labels:
app: kafka
spec:
terminationGracePeriodSeconds: 30
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: kafka
containers:
- name: kafka
image: apache/kafka:4.0.0
env:
- name: KAFKA_NODE_ID
valueFrom:
fieldRef:
fieldPath: metadata.name # 不可用于 ID(需为整数),用环境变量计算
- name: KAFKA_PROCESS_ROLES
value: "broker,controller"
- name: KAFKA_CONTROLLER_QUORUM_VOTERS
value: "1@kafka-0.kafka-broker.kafka:9093,2@kafka-1.kafka-broker.kafka:9093,3@kafka-2.kafka-broker.kafka:9093"
- name: KAFKA_LISTENERS
value: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093"
- name: KAFKA_CONTROLLER_LISTENER_NAMES
value: "CONTROLLER"
- name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP
value: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT"
resources:
requests:
cpu: "2"
memory: 4Gi
limits:
memory: 8Gi
volumeMounts:
- name: data
mountPath: /var/lib/kafka/data
livenessProbe:
tcpSocket:
port: 9092
initialDelaySeconds: 30
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-high-iops
resources:
requests:
storage: 100GiStatefulSet 的 Pod 名称为
kafka-0,kafka-1,kafka-2。需在启动前通过 InitContainer 将 Pod 序号写入node.id文件。
使用 InitContainer 写入 node.id
initContainers:
- name: init-node-id
image: busybox:1.36
command:
- sh
- -c
- |
# 从 Pod 名称提取序号: kafka-0 → 1, kafka-1 → 2, kafka-2 → 3
NODE_ID=$((${HOSTNAME##*-} + 1))
mkdir -p /var/lib/kafka/data
# 写入 KRaft meta.properties
cat > /var/lib/kafka/data/meta.properties <<EOF
node.id=$NODE_ID
version=1
cluster.id=7XOkR-LDRj6i0QVkYXJzLg
EOF
volumeMounts:
- name: data
mountPath: /var/lib/kafka/dataPod 启动后验证:
# Pod DNS:kafka-0.kafka-broker.kafka.svc.cluster.local
kubectl exec -n kafka kafka-0 -- kafka-metadata-quorum.sh \
--describe --status -c /opt/kafka/config/kraft/server.propertiesStrimzi Operator(推荐)
Strimzi 是 CNCF 项目,提供完整的 Kafka on K8s 声明式管理:
# 安装 Operator
kubectl create namespace kafka
kubectl apply -f https://strimzi.io/install/latest?namespace=kafka -n kafka# kafka-cluster.yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: my-cluster
namespace: kafka
spec:
kafka:
version: 4.0.0
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
tls: false
config:
offsets.topic.replication.factor: 3
transaction.state.log.replication.factor: 3
transaction.state.log.min.isr: 2
default.replication.factor: 3
min.insync.replicas: 2
storage:
type: persistent-claim
size: 100Gi
class: ssd-high-iops
entityOperator:
topicOperator: {}
userOperator: {}kubectl apply -f kafka-cluster.yaml -n kafka
# Strimzi 自动创建 StatefulSet、Service、ConfigMap,管理证书和滚动更新| Strimzi 特性 | 说明 |
|---|---|
| 自动滚动更新 | 配置变更后自动滚动重启 Broker |
| TLS/证书管理 | 自动生成和管理内部 TLS 证书 |
| Topic/User CRD | 通过 KafkaTopic、KafkaUser CRD 声明式管理 |
| Kafka Connect | 内置 Connect 集群管理 |
| 监控集成 | 内置 JMX Exporter + Prometheus 支持 |
K8s 部署要点
| 要点 | 说明 |
|---|---|
| Headless Service | clusterIP: None,提供 Pod 直连 DNS |
| podAntiAffinity | 确保 Broker 分散到不同物理节点 |
| volumeClaimTemplates | 每个 Pod 独立 PVC |
| Parallel podManagementPolicy | 并行启动 Pod,加速初始化 |
| InitContainer | 自动计算 node.id(基于 Pod 序号) |
| IOPS 要求 | Kafka 依赖顺序 I/O,使用 SSD + 高 IOPS 存储类 |
| 预热 | StatefulSet 首次启动较慢,建议先预热 PVC |
验证集群
# 1. 查看集群元数据
kafka-metadata-quorum.sh --describe --status \
--bootstrap-server node1:9092,node2:9092,node3:9092
# 2. 创建测试 Topic
kafka-topics.sh --create --topic test --partitions 6 --replication-factor 3 \
--bootstrap-server node1:9092,node2:9092,node3:9092
# 3. 查看 Topic ISR 分布
kafka-topics.sh --describe --topic test \
--bootstrap-server node1:9092,node2:9092,node3:9092
# 4. 生产/消费验证
echo "hello cluster" | kafka-console-producer.sh --topic test \
--bootstrap-server node1:9092,node2:9092,node3:9092
kafka-console-consumer.sh --topic test --from-beginning --max-messages 1 \
--bootstrap-server node1:9092,node2:9092,node3:9092
# 5. 查看 Broker 列表
kafka-broker-api-versions.sh --bootstrap-server node1:9092,node2:9092,node3:9092监控
JMX 指标导出
# 启动时附加 JMX Exporter
export KAFKA_OPTS="-javaagent:/opt/jmx_exporter/jmx_prometheus_javaagent.jar=9090:/opt/jmx_exporter/kafka.yml"
kafka-server-start.sh /opt/kafka/config/kraft/node1.properties关键指标
| 类别 | 指标 | 说明 |
|---|---|---|
| Broker | kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec | 消息流入速率 |
| Broker | kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec | 字节流入速率 |
| Broker | kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce | Produce 请求延迟 |
| Replica | kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions | 未完全同步的分区数 |
| Partition | kafka.server:type=ReplicaManager,name=PartitionCount | 总分区数 |
| Controller | kafka.controller:type=ControllerStats,name=ActiveControllerCount | 活跃 Controller 数(应为 1) |
| OS | kafka.server:type=OperatingSystem,name=FreePhysicalMemorySize | 空闲内存 |
| JVM | java.lang:type=GarbageCollector,name=G1 Young Generation | GC 耗时 |
推荐监控栈
| 方案 | 适用 |
|---|---|
| Prometheus + JMX Exporter + Grafana | 开源自建 |
| Datadog Kafka Integration | 商业平台 |
| Confluent Control Center | Confluent Platform |
| Strimzi + Prometheus Operator | K8s 部署 |
常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| Under-Replicated Partitions | Follower 不同步 | 检查 replica.lag.time.max.ms,增加 num.replica.fetchers |
| Consumer Lag 持续增长 | Consumer 处理慢 | 增加 Consumer 实例或 Partition 数 |
| Controller 频繁切换 | 网络不稳定或 GC 停顿 | 检查网络延迟、调整 JVM 堆大小 |
| 磁盘写满 | retention.ms 过长或 retention.bytes 无限制 | 调整保留策略,增加 Broker |
| Quorum 不可用 | Controller Quorum 节点数不足 | 确保 ≥ 2 个节点存活(3 节点集群) |
| Topic 创建失败 | replication.factor > Broker 数 | 减少副本因子或增加 Broker |
| OOM (Out of Memory) | 堆大小过低或分区太多 | 调大 KAFKA_HEAP_OPTS=-Xmx4G,减少分区 |
生产检查清单
- [ ] 至少 3 个 Broker 节点(Combined 模式兼具 Controller 和 Broker)
- [ ]
default.replication.factor=3,min.insync.replicas=2 - [ ]
offsets.topic.replication.factor=3 - [ ]
transaction.state.log.replication.factor=3 - [ ]
auto.create.topics.enable=false - [ ]
unclean.leader.election.enable=false - [ ] 所有节点使用 DNS 主机名,
advertised.listeners配置正确 - [ ] 数据目录使用 SSD 磁盘,独立于系统盘
- [ ] 操作系统 vm.swappiness=1,关闭 swap
- [ ] JVM 堆内存 ≤ 32GB,预留 50% 物理内存给 Page Cache
- [ ] Prometheus + Grafana 监控已接入
- [ ] 网络延迟 < 10ms(Controller 间)
- [ ] 制定 Partition 扩容和 Broker 扩容预案
安全加固
启用认证与加密
# 每个 Broker 的 server.properties
# SASL/PLAIN 认证
listeners=SASL_PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
advertised.listeners=SASL_PLAINTEXT://node1:9092
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT
sasl.enabled.mechanisms=PLAIN
# Super User
super.users=User:admin创建 JAAS 配置文件并设置 KAFKA_OPTS=-Djava.security.auth.login.config=/opt/kafka/config/kafka.jaas:
// kafka.jaas
KafkaServer {
org.apache.kafka.common.security.plain.PlainLoginModule required
username="admin"
password="admin-secret"
user_admin="admin-secret"
user_producer="producer-secret"
user_consumer="consumer-secret";
};ACL 授权
# 创建 ACL:允许 producer-user 对 orders Topic 写入
kafka-acls.sh --bootstrap-server node1:9092 \
--add --allow-principal User:producer-user \
--operation Write --topic orders \
--command-config admin-client.conf
# 允许 consumer-group "order-service" 读取
kafka-acls.sh --bootstrap-server node1:9092 \
--add --allow-principal User:consumer-user \
--operation Read --topic orders --group order-service \
--command-config admin-client.conf