Kafka KRaft 模式 Kubernetes 部署手册(StatefulSet + apache/kafka 官方镜像)——筑梦之路

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。


二、前置条件

  1. Kubernetes 集群 ≥ 1.24 (StatefulSet、policy/v1 PDB 均可用);能正常执行 kubectl。
  2. 集群中已有可用的 StorageClass (kubectl get sc),且支持 ReadWriteOnce。
  3. 节点有足够资源:Combined 3 节点建议 ≥ 4C8G/节点;Isolated 建议 controller 1C2G、broker 4C8G 起。
  4. 节点能拉取 apache/kafka:4.3.1(内网环境请先同步到私有镜像仓库,并替换清单中的 image 地址)。
  5. 时区/时钟同步正常(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。
sh 复制代码
export KAFKA_NODE_ID="${POD_NAME##*-}"
export KAFKA_ADVERTISED_LISTENERS="PLAINTEXT://${POD_NAME}.kafka-headless.kafka.svc.cluster.local:9092"
exec /etc/kafka/docker/run

POD_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。清单中固定:

yaml 复制代码
securityContext:
  runAsUser: 1000
  runAsGroup: 1000
  runAsNonRoot: true
  fsGroup: 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

bash 复制代码
kubectl create namespace kafka
kubectl -n kafka 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 步:部署

bash 复制代码
# 主方案(Combined 3 节点)
kubectl apply -f kafka-kraft-combined.yaml

# 或者:角色分离(Isolated 3 controller + 3 broker)
# kubectl apply -f kafka-kraft-isolated.yaml

# 可选:需要集群外访问时再应用
# kubectl apply -f kafka-kraft-external-access.yaml

第 4 步:等待就绪

bash 复制代码
kubectl -n kafka get pods -o wide -w
kubectl -n kafka get pvc

预期:Combined 模式下 kafka-0/1/2 全部 Running 且 READY 1/1;每个 Pod 各有一块 Bound 的 PVC。


五、验证

1. 看日志确认进程启动

bash 复制代码
kubectl -n kafka logs kafka-0 | tail -n 40

应能看到 Kafka Server started;首次启动还会有格式化数据目录的相关输出,重启后则显示 already formatted 并跳过。

2. 起一个客户端 Pod 做功能验证

bash 复制代码
kubectl -n kafka run kafka-client --image=apache/kafka:4.3.1 --restart=Never -- tail -f /dev/null
kubectl -n kafka exec -it kafka-client -- bash

容器内依次执行(Isolated 模式同样可用,kafka-bootstrap 指向 broker):

bash 复制代码
# 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 --topic demo --partitions 3 --replication-factor 3

# 3) 确认分区分布:3 个分区应分别以 kafka-0/1/2 为 leader
/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka-bootstrap:9092 --describe --topic demo

# 4) 生产
/opt/kafka/bin/kafka-console-producer.sh --bootstrap-server kafka-bootstrap:9092 --topic demo
# 输入几行文本后 Ctrl+C 退出

# 5) 消费
/opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka-bootstrap:9092 \
  --topic demo --from-beginning --max-messages 5

3. 故障演练(可选)

删掉一个 broker Pod,观察 topic 仍可读写、quorum 仍正常:

bash 复制代码
kubectl -n kafka delete pod kafka-1
kubectl -n kafka 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 必须同步更新所有节点看到的成员列表:

  1. 把新节点的 nodeId@Pod FQDN:9093 加进 KAFKA_CONTROLLER_QUORUM_VOTERS(所有节点,包括新节点自己);
  2. replicas 相应调整(controller 数量建议保持奇数,3 → 5 更合适;4 个 controller 的容错能力并不优于 3 个);
  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:

bash 复制代码
/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 层可以直接扩:

bash 复制代码
kubectl -n kafka scale statefulset kafka-broker --replicas=5

因为 broker 的 node.id 由 Pod 名推导(kafka-broker-N → N+3),新增 Pod 会自动获得不冲突的 ID,且 broker 不在 quorum voters 列表中,无需改动 controller 配置。controller 数量建议保持奇数(3 或 5)。

缩容与下线

  1. 先用 kafka-reassign-partitions.sh 把待下线 broker 上的分区副本迁移走;
  2. controller 节点:动态 quorum 下先用 kafka-metadata-quorum.sh ... remove-controller 移出 quorum 再停机;静态 quorum 下则要从所有节点的 KAFKA_CONTROLLER_QUORUM_VOTERS 中移除该节点并滚动重启;
  3. 再 scale 或删除 Pod;
  4. PVC 不会随 Pod 删除而释放,需要手动清理:kubectl -n kafka delete pvc data-kafka-3(数据会丢失)。

滚动重启 / 升级

改镜像版本或环境变量会触发 StatefulSet 滚动更新。配合 PDB(maxUnavailable: 1)可保证一次只动一个 Pod;生产建议在低峰期执行,并先确认 topic 副本因子 ≥ 3、min.insync.replicas=2。

磁盘扩容

StorageClass 支持在线扩容时:改 volumeClaimTemplates 的 storage 后需逐个 kubectl -n kafka edit pvc data-kafka-0 手动扩,或重建 StatefulSet(StatefulSet 的 volumeClaimTemplates 变更不会自动应用到已有 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 后重新 apply
Pod 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 一直 Pending StorageClass 不存在、无可用 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 修复

常用排查命令:

bash 复制代码
kubectl -n kafka get pods,pvc,svc
kubectl -n kafka describe pod kafka-0
kubectl -n kafka logs kafka-0 --previous
kubectl -n kafka exec -it kafka-0 -- cat /opt/kafka/config/server.properties
kubectl -n kafka exec -it kafka-0 -- ls -l /var/lib/kafka/data

十、参考来源


附:清单参数速查

环境变量 作用 本手册取值
CLUSTER_ID KRaft 集群 ID 需自行生成
KAFKA_NODE_ID 节点 ID(由 Pod 名推导) 0/1/2(broker 从 3 起)
KAFKA_PROCESS_ROLES 角色 broker,controller
KAFKA_CONTROLLER_QUORUM_VOTERS 控制器 quorum 成员 id@Pod FQDN:9093
KAFKA_LISTENERS 监听地址 PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS 对外通告地址(每个 Pod 不同) Pod FQDN:9092
KAFKA_LOG_DIRS 数据目录 /var/lib/kafka/data
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR 内部 topic 副本数 3(单机测试改 1)
相关推荐
A.说学逗唱的Coke2 小时前
【云原生专题】Kubernetes 备份完全实战:用 Velero 搞定集群备份、恢复与跨集群迁移(附完整命令与踩坑记录)
云原生·容器·kubernetes
wzq11_66619 小时前
Kubernetes集群——基础篇(基础知识与搭建步骤一遍过!!!)
java·容器·kubernetes
余槐i1 天前
将AI Agent嵌入现有Java系统时,Spring Boot 3.2的异步冲突与内存泄漏排查
人工智能·spring boot·性能优化·kubernetes·ai agent
旋生万物1 天前
用螺旋数重写 Transformer Attention:让大模型自带“相位记忆“的 PyTorch 实现
docker·云原生·kubernetes·螺旋生成论·螺旋相位
编码如写诗1 天前
【k8s】全新Ubuntu 26.04 使用kt 超简单安装 k8s 最新1.37.1+KubeSphere4.1.3
ubuntu·容器·kubernetes
小匠石钧知1 天前
05_在k8s集群中安装NFS实现ReadWriteMany存储
java·容器·kubernetes·nfs·readwritemany·rwx
智码看视界1 天前
技术选型指南:Pulsar vs Kafka:消息队列选型终极对比与压测
kafka·消息队列·pulsar·消息中间件·流处理·高吞吐·架构选型
俊哥大数据1 天前
Flink1.20.3 实时消费 Kafka 数据并解析入湖 Paimon1.4.2 全流程实战
分布式·flink·kafka·数据湖·paimon
万博智云OneProCloud1 天前
GPU 资源池化最佳实践:规划、调度、监控与运营的五步方法
kubernetes·算力调度·ai基础设施·gpu池化·agione·powerone