Kafka KRaft 模式 Kubernetes 部署手册(StatefulSet + apache/kafka 官方镜像)
https://blog.csdn.net/qq_34777982/article/details/166831220?sharetype=blogdetail&sharerId=166831220&sharerefer=PC&sharesource=qq_34777982&spm=1011.2480.3001.8118
目标:用Apache 官方镜像
apache/kafka:4.3.1在 Kubernetes 上以StatefulSet方式部署KRaft(无 ZooKeeper)集群。
随本手册交付 3 个可直接kubectl apply的清单文件:kafka-kraft-combined.yaml(主方案)、kafka-kraft-isolated.yaml(角色分离)、kafka-kraft-external-access.yaml(可选外部访问)。
一、方案概览
| 项目 | 选择 | 说明 |
|---|---|---|
| 镜像 | apache/kafka:4.3.1 | Apache 官方 JVM 镜像,自 3.7.0 起提供;另有实验性的apache/kafka-native(GraalVM),官方明确仅建议本地开发测试使用 |
| 元数据 | KRaft | 4.x 已完全移除 ZooKeeper 依赖,不需要部署 ZK |
| 工作负载 | StatefulSet | 需要稳定的 Pod 名称、DNS 与独占存储 |
| 服务发现 | Headless Service | kafka-0.kafka-headless.kafka.svc.cluster.local等稳定域名 |
| 存储 | volumeClaimTemplates | 每个 Pod 一块独立 PVC |
| 模式 | Combined(默认)/ Isolated(备选) | Combined = broker+controller 合一,3 节点;Isolated = 3 controller + 3 broker |
文件清单
| 文件 | 内容 | 适用 |
|---|---|---|
kafka-kraft-combined.yaml | Namespace + Headless/ClusterIP Service + 3 节点 StatefulSet + PDB | 中小规模生产、测试/预发 |
kafka-kraft-isolated.yaml | Namespace + controller/broker 两套 StatefulSet(各 3 副本)+ Service + PDB | 生产环境,角色分离 |
kafka-kraft-external-access.yaml | 每 Pod 一个 NodePort Service | 需要集群外客户端接入时 |
两个部署文件二选一,不要同时 apply。
二、前置条件
- Kubernetes 集群≥ 1.24(StatefulSet、
policy/v1PDB 均可用);能正常执行kubectl。 - 集群中已有可用的StorageClass(
kubectl get sc),且支持ReadWriteOnce。 - 节点有足够资源:Combined 3 节点建议 ≥ 4C8G/节点;Isolated 建议 controller 1C2G、broker 4C8G 起。
- 节点能拉取
apache/kafka:4.3.1(内网环境请先同步到私有镜像仓库,并替换清单中的 image 地址)。 - 时区/时钟同步正常(KRaft 对时钟敏感,节点需有 NTP)。
三、关键设计说明(为什么这么写)
1. 为什么必须podManagementPolicy: Parallel
KRaft 的 controller quorum 需要 3 个成员同时在线才能选举出 leader。若用默认的OrderedReady,kafka-0会一直等 quorum 而不 Ready,kafka-1、kafka-2就永远不会被创建 —— 直接死锁。
2. 为什么 Headless Service 要开publishNotReadyAddresses: true
同理:Pod 之间要靠 DNS 互相解析才能组 quorum。若 DNS 记录只在 Pod Ready 之后才注册,就会陷入"要 Ready 先要互相解析、要互相解析先要 Ready"的循环。
3. 为什么用 bash 包一层启动命令
官方镜像的启动命令是/etc/kafka/docker/run(在 Dockerfile 中由CMD指定,不是ENTRYPOINT,因此可以被覆盖)。而这两个值每个 Pod 都不同,无法用静态环境变量写死:
node.id(KAFKA_NODE_ID):由 Pod 名后缀推导,kafka-0→ 0;advertised.listeners(KAFKA_ADVERTISED_LISTENERS):必须是客户端能连到本 Pod的地址,用 Pod 的稳定 DNS。
exportKAFKA_NODE_ID="${POD_NAME##*-}"exportKAFKA_ADVERTISED_LISTENERS="PLAINTEXT://${POD_NAME}.kafka-headless.kafka.svc.cluster.local:9092"exec/etc/kafka/docker/runPOD_NAME来自 Downward API(fieldRef: metadata.name),比依赖容器内HOSTNAME变量更稳。
4. 官方镜像的配置注入规则
镜像支持三种配置方式,优先级从低到高:内置默认配置 → 挂载文件(/mnt/shared/config/*.properties)→ 环境变量。
环境变量命名规则(.→_、_→__、-→___,再加前缀KAFKA_):
| 配置项 | 环境变量 |
|---|---|
node.id | KAFKA_NODE_ID |
log.dirs | KAFKA_LOG_DIRS |
offsets.topic.replication.factor | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR |
abc-def | KAFKA_ABC___DEF |
abc_def | KAFKA_ABC__DEF |
KAFKA_HEAP_OPTS、KAFKA_OPTS、KAFKA_LOG4J_*等属于脚本特殊处理变量,不遵循上述映射。
5. 集群 ID 与自动格式化
镜像内置了一个默认CLUSTER_ID(未设置时自动使用),启动时会调用kafka.docker.KafkaDockerWrapper setup把默认配置 + 挂载配置 +KAFKA_*环境变量合并写入/opt/kafka/config/server.properties,并在数据目录未格式化时自动格式化(已格式化则跳过并打印already formatted)。
实践建议:显式设置CLUSTER_ID(用kafka-storage.sh random-uuid生成),同一集群所有节点保持一致。集群 ID 只在首次格式化时写入meta.properties,后期更换必须清空数据卷重新格式化。
6. 权限
官方镜像内建用户appuser(uid=1000 / gid=1000),数据目录/var/lib/kafka/data。清单中固定:
securityContext:runAsUser:1000runAsGroup:1000runAsNonRoot:truefsGroup:1000# 关键:让 PVC 对 appuser 可写注意:镜像运行过程中需要写/opt/kafka/config(生成最终配置文件),因此不要开启readOnlyRootFilesystem: true,否则启动会报/opt/kafka/config/ file not writable。
7. 探针
- 只用
startupProbe+readinessProbe,不配livenessProbe:Kafka 启动慢,且绝大多数启动失败(quorum 不完整、存储权限、集群 ID 不一致)重启无法自愈,配了 liveness 只会陷入反复重启。 - Broker 用
kafka-broker-api-versions.sh --bootstrap-server localhost:9092探测(能响应 API 请求才算真就绪),controller 用 9093 端口 TCP 探测。
四、部署步骤
第 1 步:生成集群 ID
kubectl create namespace kafka kubectl-nkafka run kafka-cluster-id--rm-it--restart=Never\--image=apache/kafka:4.3.1 -- /opt/kafka/bin/kafka-storage.sh random-uuid输出形如4L6g3nShT-eMCtK--X86sw。把它填进清单里的CLUSTER_ID(Combined 清单只有一处;Isolated 清单有 controller、broker 两处,必须一致)。
第 2 步:按环境修改清单
storageClassName:改成集群实际存在的 StorageClass;storage:数据盘容量(生产建议 ≥ 50Gi,并预留扩容能力);resources:CPU/内存按实际调整,KAFKA_HEAP_OPTS的堆大小取容器内存 limits 的约 1/2;image:内网环境改成私有仓库地址。
第 3 步:部署
# 主方案(Combined 3 节点)kubectl apply-fkafka-kraft-combined.yaml# 或者:角色分离(Isolated 3 controller + 3 broker)# kubectl apply -f kafka-kraft-isolated.yaml# 可选:需要集群外访问时再应用# kubectl apply -f kafka-kraft-external-access.yaml第 4 步:等待就绪
kubectl-nkafka get pods-owide-wkubectl-nkafka get pvc预期:Combined 模式下kafka-0/1/2全部Running且READY 1/1;每个 Pod 各有一块Bound的 PVC。
五、验证
1. 看日志确认进程启动
kubectl-nkafka logs kafka-0|tail-n40应能看到Kafka Server started;首次启动还会有格式化数据目录的相关输出,重启后则显示already formatted并跳过。
2. 起一个客户端 Pod 做功能验证
kubectl-nkafka run kafka-client--image=apache/kafka:4.3.1--restart=Never --tail-f/dev/null kubectl-nkafkaexec-itkafka-client --bash容器内依次执行(Isolated 模式同样可用,kafka-bootstrap指向 broker):
# 1) 查看 KRaft 元数据 quorum 状态:应看到 3 个 voter,其中一个为 Leader/opt/kafka/bin/kafka-metadata-quorum.sh --bootstrap-server kafka-bootstrap:9092 describe--status# 2) 建 topic(副本因子 3)/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-bootstrap:9092\--create--topicdemo--partitions3--replication-factor3# 3) 确认分区分布:3 个分区应分别以 kafka-0/1/2 为 leader/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-bootstrap:9092--describe--topicdemo# 4) 生产/opt/kafka/bin/kafka-console-producer.sh --bootstrap-server kafka-bootstrap:9092--topicdemo# 输入几行文本后 Ctrl+C 退出# 5) 消费/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka-bootstrap:9092\--topicdemo --from-beginning --max-messages53. 故障演练(可选)
删掉一个 broker Pod,观察 topic 仍可读写、quorum 仍正常:
kubectl-nkafka delete pod kafka-1 kubectl-nkafka get pods-w六、客户端接入
集群内应用
bootstrap 地址(所有 Pod 都是入口,任填一个或全部):
| 地址 | 用途 |
|---|---|
kafka-bootstrap.kafka.svc.cluster.local:9092 | 常规入口(ClusterIP,负载到任一 broker) |
kafka-0.kafka-headless.kafka.svc.cluster.local:9092(1、2 同理) | 指定 Pod 直连,排查问题时用 |
跨命名空间访问时用 FQDN 即可。注意:客户端拿到的是advertised.listeners中的 Pod DNS,因此客户端必须能解析集群内的 Pod 域名,把应用部署在同一集群内最省事。
集群外应用
见kafka-kraft-external-access.yaml头部注释:需要额外加一个EXTERNAL监听(9094),并把每个 Pod 的对外地址写进advertised.listeners,再为每个 Pod 配一个固定nodePort的 Service。
安全提醒:PLAINTEXT暴露到公网等于无认证无加密,生产必须改用SASL_SSL/SSL(镜像支持把证书与 JAAS 配置挂到/etc/kafka/secrets,再用KAFKA_OPTS指向 JAAS 文件)。
七、扩缩容与日常运维
Combined 模式扩容(注意:本清单是静态 quorum)
broker 与 controller 是同一批 Pod,增加副本数会同时增加一个 controller,因此不是改一下replicas就行。本手册的清单显式配置了KAFKA_CONTROLLER_QUORUM_VOTERS,属于静态 quorum,扩容 controller 必须同步更新所有节点看到的成员列表:
- 把新节点的
nodeId@Pod FQDN:9093加进KAFKA_CONTROLLER_QUORUM_VOTERS(所有节点,包括新节点自己); replicas相应调整(controller 数量建议保持奇数,3 → 5 更合适;4 个 controller 的容错能力并不优于 3 个);apply后 StatefulSet 会滚动重启全部 Pod,期间集群有短暂不可用 —— 安排在维护窗口执行,并先确认 topic 副本因子 ≥ 3、min.insync.replicas=2。
想避免"改 voters 就要全量滚动重启",需要在集群首次格式化时就建立动态 quorum:不配
controller.quorum.voters,改配controller.quorum.bootstrap.servers,并在格式化时带--initial-controllers。此后即可用kafka-metadata-quorum.sh ... add-controller/remove-controller在线增删 controller。判断当前属于哪种 quorum:/opt/kafka/bin/kafka-features.sh --bootstrap-controller\kafka-0.kafka-headless.kafka.svc.cluster.local:9093 describe
kraft.version为0或字段不存在 = 静态 quorum;≥ 1= 动态 quorum。注意:官方镜像的自动格式化默认得到静态 quorum,要动态 quorum 需自行控制格式化流程。
Isolated 模式扩容
broker 层可以直接扩:
kubectl-nkafka scale statefulset kafka-broker--replicas=5因为 broker 的node.id由 Pod 名推导(kafka-broker-N→N+3),新增 Pod 会自动获得不冲突的 ID,且 broker 不在 quorum voters 列表中,无需改动 controller 配置。controller 数量建议保持奇数(3 或 5)。
缩容与下线
- 先用
kafka-reassign-partitions.sh把待下线 broker 上的分区副本迁移走; - controller 节点:动态 quorum 下先用
kafka-metadata-quorum.sh ... remove-controller移出 quorum 再停机;静态 quorum 下则要从所有节点的KAFKA_CONTROLLER_QUORUM_VOTERS中移除该节点并滚动重启; - 再
scale或删除 Pod; - PVC 不会随 Pod 删除而释放,需要手动清理:
kubectl -n kafka delete pvc>滚动重启 / 升级改镜像版本或环境变量会触发 StatefulSet 滚动更新。配合 PDB(
maxUnavailable: 1)可保证一次只动一个 Pod;生产建议在低峰期执行,并先确认 topic 副本因子 ≥ 3、min.insync.replicas=2。磁盘扩容
StorageClass 支持在线扩容时:改
volumeClaimTemplates的storage后需逐个kubectl -n kafka edit pvc>八、生产加固清单- 副本与一致性:
default.replication.factor=3、min.insync.replicas=2、unclean.leader.election.enable=false;生产者的acks=all。 - 自动建 Topic 关闭:
auto.create.topics.enable=false(清单已设置),避免误建单副本 topic。 - 资源与 JVM:
KAFKA_HEAP_OPTS取容器内存 limits 的约 1/2,其余留给页缓存;limits 过小会被 OOMKill。 - 存储:使用 SSD/本地盘类高性能 StorageClass;磁盘写满会导致 broker 不可用,务必配置磁盘使用率告警。
- 反亲和:把
preferredDuringSchedulingIgnoredDuringExecution改成requiredDuringSchedulingIgnoredDuringExecution,强制 3 副本分散到不同节点;跨可用区用topologySpreadConstraints。 - 监控:开启 JMX(
KAFKA_JMX_PORT)并配 JMX Exporter sidecar,或部署 kafka-exporter;重点指标:UnderReplicatedPartitions、OfflinePartitionsCount、ActiveControllerCount、请求延迟、磁盘使用率。 - 安全:启用 SASL/SSL,
/etc/kafka/secrets挂载证书与 JAAS;用 NetworkPolicy 限制 9092/9093 的来源。 - 日志与保留:
log.retention.hours、log.segment.bytes按业务量调整,避免磁盘无限增长。 - 备份与容灾:跨集群用 MirrorMaker 2 复制;关键 topic 单独设置保留策略。
- 时钟同步:节点必须 NTP 同步。
九、常见问题排查
现象 常见原因 处理 kafka-1/kafka-2迟迟不被创建podManagementPolicy不是Parallel改回 Parallel后重新 applyPod Running 但不 Ready,日志反复解析失败 Headless Service 未开 publishNotReadyAddresses开启后重建 Service 日志 KAFKA_ADVERTISED_LISTENERS is not supported on a KRaft controller.后退出controller-only 节点设置了该变量 从 controller 的 env 中删除(Isolated 清单已规避) 日志 /opt/kafka/config/ file not writable开启了只读根文件系统,或 Docker < 20.10.4 关闭 readOnlyRootFilesystem;升级容器运行时Permission denied写/var/lib/kafka/data缺 fsGroup: 1000补上 securityContext 更换 CLUSTER_ID后启动失败 / 集群 ID 不一致数据目录已格式化, meta.properties里是旧 ID清空对应 PVC 后重新启动 PVC 一直 PendingStorageClass 不存在、无可用 PV、容量超限 检查 kubectl get sc/kubectl describe pvc客户端连上后超时或反复重连 advertised.listeners地址客户端不可达(跨集群、NAT、NodePort 配错)核对客户端网络能否解析/直连 Pod DNS 或节点 IP:nodePort broker 起不来、日志报 quorum 相关错误 KAFKA_CONTROLLER_QUORUM_VOTERS中 node.id 与各 Pod 实际KAFKA_NODE_ID不匹配,或 DNS 写错逐项核对 voters 列表与 Pod 名 升级/重启后部分分区副本不同步 单 Pod 停机时间过长、副本因子不足 用 kafka-topics.sh --describe看 ISR,必要时kafka-reassign-partitions.sh修复常用排查命令:
kubectl-nkafka get pods,pvc,svc kubectl-nkafka describe pod kafka-0 kubectl-nkafka logs kafka-0--previouskubectl-nkafkaexec-itkafka-0 --cat/opt/kafka/config/server.properties kubectl-nkafkaexec-itkafka-0 --ls-l/var/lib/kafka/data十、参考来源
- Apache Kafka 官方 Docker 页面(镜像版本与拉取方式):https://kafka.apache.org/43/getting-started/docker/
- Apache Kafka 官方 Docker 镜像使用指南(环境变量命名规则、集群 ID、挂载配置、SASL/SSL):https://github.com/apache/kafka/blob/trunk/docker/examples/README.md
- 官方多节点示例(Combined / Isolated 的完整环境变量清单):https://github.com/apache/kafka/tree/trunk/docker/examples/docker-compose-files/cluster
- Apache Kafka 4.3.1 发布公告:https://kafka.apache.org/blog/2026/06/25/apache-kafka-4.3.1-release-announcement/
- KRaft 运维文档(动态调整 controller quorum、add/remove-controller):https://kafka.apache.org/43/operations/kraft/
- Docker Hub
apache/kafka:https://hub.docker.com/r/apache/kafka
附:清单参数速查
环境变量 作用 本手册取值 CLUSTER_IDKRaft 集群 ID 需自行生成 KAFKA_NODE_ID节点 ID(由 Pod 名推导) 0/1/2(broker 从 3 起) KAFKA_PROCESS_ROLES角色 broker,controllerKAFKA_CONTROLLER_QUORUM_VOTERS控制器 quorum 成员 id@Pod FQDN:9093KAFKA_LISTENERS监听地址 PLAINTEXT://:9092,CONTROLLER://:9093KAFKA_ADVERTISED_LISTENERS对外通告地址(每个 Pod 不同) Pod FQDN:9092 KAFKA_LOG_DIRS数据目录 /var/lib/kafka/dataKAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR内部 topic 副本数 3(单机测试改 1) - 副本与一致性: