资讯动态

Kafka迁移K8s实践:StatefulSet部署有状态应用全记录

发布时间:2026/10/19 2:59:06 来源:尧图企业网站定制
最近接到一个内部消息中间件平台的迁移任务要把一套Kafka集群连同它依赖的ZooKeeper集群从机房虚拟机挪到k8s里。最初我以为只是“写几个YAML、把镜像跑起来”的事真正动手才发现难点根本不在于容器化而在于怎么让这两个“认死理”的有状态应用在Pod随时会被重建、IP随时会漂移的k8s环境里稳住自己的身份和存储。ZooKeeper要保证选主一致性Kafka的每个Broker要维护分区副本和位移它们都不像无状态服务那样可以容忍“无痕重生”。这篇文章不是简单地贴一份部署清单而是把我从方案选型、部署前准备、StatefulSet编排、网络暴露到最终压测排障的完整过程整理出来。文章会围绕StatefulSet、Headless Service、持久化存储、advertised.listeners这几个核心话题展开同时把我在实际操作中踩过的坑也一并交代清楚。无论你是刚学k8s的新手还是已经维护Kafka多年的老人只要准备在k8s上跑消息中间件这篇内容都有参考价值。1. 为什么非要在k8s里跑Kafka从虚拟机迁到容器编排后算清的几笔账在开始写YAML之前得先把“为什么”讲清楚。ZooKeeper和Kafka都是有状态应用对自身标识、数据持久化和节点间通信的要求比一般无状态服务高出一个量级。如果只是把原来虚拟机里的启动命令包成镜像然后随便用Deployment去跑Pod一重建Broker IP变了节点间互相找不到数据目录可能被重新初始化整个集群很容易进入“看起来活着但实际不可用”的状态。这也是为什么很多人第一次在k8s里部署Kafka时明明镜像能起来集群却隔三差五报警。1.1 有状态应用在k8s里的“身份焦虑”先说我犯过的第一个认知错误。最早我想用Deployment跑ZooKeeperPod重启后IP变了虽然客户端通过Service访问没有问题但ZooKeeper集群节点之间通信依赖配置文件里的server列表列表里写的是临时IP结果三个节点互相连不上日志里全是Connection refused选主一直失败。后来才意识到这不是配置写错而是没有理解k8s中Pod的生命周期Pod是“可替换的牲畜”而ZooKeeper和Kafka需要的是“有名字的宠物”。StatefulSet就是为了解决这种“身份焦虑”而生的。它给每个Pod一个固定且有序的标识比如zookeeper-0、zookeeper-1、kafka-0、kafka-1。Pod重建后名字不变对应DNS名不变PVC也不变。这个固定身份非常关键因为应用可以把主机名直接写进配置节点重启后依然能通过同一个名字找回彼此。如果换用DeploymentPod名是一串随机字符串每次都不固定状态类应用根本没法做这种关联。1.2 方案选型StatefulSet手写、Operator还是Helm当时我面前有三条路一是手写StatefulSet和Service二是用Operator三是直接用Helm chart部署。我最后选择了手写不是因为我天生爱造轮子而是团队对这个集群有定制需求——网络环境特殊持久化存储用的是自建的存储类。Operator虽然省事但排查问题时会多一层抽象而且Operator自身的版本和Kafka版本耦合得很紧升级时机不一定受控。Helm chart的好处是社区维护得不错但坏处是“开箱即用”往往意味着“开箱之后要改很多东西”。我当时用社区chart做过测试配置项非常多想调一个advertised.listeners的表达方式翻文档翻了很久。倒不如自己写几份YAML把每个参数都掌握清楚。这里不是想劝大家全部手写而是想说明如果目标是深入理解组件行为手写一遍是最好的学习路径如果只是赶时间上线直接用成熟的Operator也未尝不可。关键在于你必须知道自己的配置在这两种模式里分别落到哪出了问题才知道去哪里看。2. 部署前的硬性准备存储类、命名空间与镜像版本匹配很多人一上来就写StatefulSet结果后面被存储权限、命名空间隔离、镜像启动问题反复折磨。部署有状态应用至少有三件事应该在写YAML前就确认好存储性能、命名空间边界、镜像启动机制。这三件事没搞清后面遇到问题会非常被动。2.1 存储类选型别让磁盘拖垮事务性能ZooKeeper的dataDir目录存放事务日志和快照Kafka的log.dirs存放消息日志这些盘都极其忌讳延迟抖动。如果你用云厂商的网络存储除非是额外配置的高性能云盘否则性能会很难看。如果你用的是自建集群建议提前用fio之类工具测试一下存储的随机读写IOPS和延迟分布。Kafka是典型的顺序读写为主、随机写为辅的负载但ZooKeeper对fsync延迟非常敏感负责投票和事务提交的节点如果磁盘慢整个集群的可用性都会受影响。我个人的建议是ZooKeeper的事务日志单独用小容量高性能盘数据快照可以放在另一块盘Kafka的log.dirs用大容量高吞吐盘容量按日写入量、保留时间和副本因子去估算。比如业务一天写入1TB保留7天副本因子为3总存储需求大约21TB如果3个Broker每个Broker至少7TB。这种估算最好在部署前就做掉否则等到磁盘报警再扩容在k8s里可能要麻烦得多。2.2 命名空间、配额与标签规划ZooKeeper和Kafka建议放在独立命名空间里不要和业务应用混在一起。这样方便用NetworkPolicy限制哪些命名空间可以访问消息中间件也方便用ResourceQuota控制资源上限。我用的命名空间名是kafka-cluster所有相关的ConfigMap、Service、StatefulSet都打在同一个namespace下避免不同环境互相干扰。资源请求和限制也要提前规划。Kafka的JVM堆内内存通常不需要很大反而需要大量堆外内存做PageCache因此如果Pod的内存limit设置得太低内核PageCache会被压缩Kafka读写性能会严重下降。我当时的经验是容器内存limit至少要比JVM堆大小高出1.5到2倍给操作系统的PageCache留出余量。CPU请求可以低一点但limit要留足突发空间避免流量高峰时被CPU限流影响。2.3 镜像选择与安全上下文镜像版本一定要固定不要用latest。ZooKeeper和Kafka的版本之间是有兼容关系的比如Kafka 3.x对ZooKeeper版本有最低要求随便升级ZooKeeper小版本虽然一般没事但大版本跳跃前一定要看官方兼容性说明。另外所有Pod最建议用非root用户运行并设置securityContext里的runAsNonRoot、fsGroup。我之前遇到过持久卷目录权限属于root而容器内进程是非root用户结果根本写不了数据日志里一直是Permission denied。这个问题可以在部署前通过initContainer调整目录权限或者确认存储类自动处理了fsGroup。3. ZooKeeper集群先解决“选主”应用的有状态部署难题Kafka broker启动时会连接ZooKeeper做集群元数据管理。虽然新版Kafka正在逐步去除ZooKeeper依赖但很多存量集群还是依赖它。ZooKeeper本身是一个必须保持强一致性的服务部署在k8s里的核心就是让它的节点身份稳定、数据持久、选主通信地址固定。只要这一步做好了Kafka的部署就会顺畅很多。3.1 ZK的“选主”原理和部署需求ZooKeeper的配置核心是几个数字myid、dataDir、dataLogDir、clientPort以及server.Xhost:port:port。每个节点都有一个唯一id配置文件里列出所有节点的主机名和两个端口其中一个是跟随者与领导者之间通信的quorum端口另一个是用于leader选举的端口。节点之间通过这个列表互相发现如果host写的是会变的Pod IP节点重启后可能永远找不到彼此。在k8s里server.X后面应该接稳定DNS名。这就要用到Headless Service。Headless Service不分配ClusterIP但会为每个后端Pod生成一个形如podName.serviceName.namespace.svc.cluster.local的DNS记录。比如zookeeper-0.zookeeper-hs.kafka-cluster.svc.cluster.local。这样ZooKeeper配置里就可以放心地写server.1zookeeper-0.zookeeper-hs.kafka-cluster.svc.cluster.local:2888:3888 server.2zookeeper-1.zookeeper-hs.kafka-cluster.svc.cluster.local:2888:3888 server.3zookeeper-2.zookeeper-hs.kafka-cluster.svc.cluster.local:2888:3888myid的生成也有讲究。StatefulSet创建的三个Pod名称后缀分别是0、1、2我们可以通过启动脚本动态生成myid不需要手工为每个副本写单独配置。具体做法是在initContainer里读取Pod名称取最后一个连字符后的数字写入dataDir/myid文件。3.2 三节点ZK的StatefulSet与Headless Service先看Headless ServiceapiVersion: v1 kind: Service metadata: name: zookeeper-hs namespace: kafka-cluster spec: clusterIP: None selector: app: zookeeper ports: - name: client port: 2181 targetPort: 2181 - name: quorum port: 2888 targetPort: 2888 - name: election port: 3888 targetPort: 3888然后是用ConfigMap保存zoo.cfgapiVersion: v1 kind: ConfigMap metadata: name: zookeeper-config namespace: kafka-cluster data: zoo.cfg: | tickTime2000 initLimit5 syncLimit2 dataDir/data dataLogDir/datalog clientPort2181 maxClientCnxns60 autopurge.snapRetainCount3 autopurge.purgeInterval1 server.1zookeeper-0.zookeeper-hs.kafka-cluster.svc.cluster.local:2888:3888 server.2zookeeper-1.zookeeper-hs.kafka-cluster.svc.cluster.local:2888:3888 server.3zookeeper-2.zookeeper-hs.kafka-cluster.svc.cluster.local:2888:3888StatefulSet里需要挂载ConfigMap、两个PVC并用initContainer生成myid。我们假设ZooKeeper数据目录是/data事务日志目录是/datalogyaml大概长这样apiVersion: apps/v1 kind: StatefulSet metadata: name: zookeeper namespace: kafka-cluster spec: serviceName: zookeeper-hs replicas: 3 podManagementPolicy: OrderedReady selector: matchLabels: app: zookeeper template: metadata: labels: app: zookeeper spec: securityContext: fsGroup: 2000 runAsNonRoot: true initContainers: - name: zk-init image: busybox:1.36 command: - /bin/sh - -c - | myid$(echo $POD_NAME | awk -F- {print $NF}) echo $myid /data/myid env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name volumeMounts: - name: data mountPath: /data containers: - name: zk image: zookeeper:3.7.1 ports: - name: client containerPort: 2181 - name: quorum containerPort: 2888 - name: election containerPort: 3888 volumeMounts: - name: data mountPath: /data - name: datalog mountPath: /datalog - name: config mountPath: /conf readinessProbe: exec: command: - sh - -c - zkServer.sh status volumeClaimTemplates: - metadata: name: data spec: accessModes: [ ReadWriteOnce ] storageClassName: fast resources: requests: storage: 50Gi - metadata: name: datalog spec: accessModes: [ ReadWriteOnce ] storageClassName: fast resources: requests: storage: 20Gi volumes: - name: config configMap: name: zookeeper-config注意我这里是按照官方ZooKeeper镜像的目录习惯写的如果你用的是别的镜像路径要做相应调整。initContainer里用busybox只是为了生成myid实际业务容器跑起来后这个临时容器就退出了不影响安全。3.3 ZK初始化与健康检查的正确姿势部署完成后先看一下Pod是否都Readykubectl -n kafka-cluster get pods -o wide然后逐个进入容器查看节点状态kubectl -n kafka-cluster exec zookeeper-0 -- zkServer.sh status正常会输出类似Mode: follower或Mode: leader。如果三个节点都能看到状态说明集群内部通信正常。还可以通过ZooKeeper自带的命令行客户端看根节点列表kubectl -n kafka-cluster exec zookeeper-0 -- zkCli.sh -server 127.0.0.1:2181 ls /输出里一般有/zookeeper说明客户端通道没问题。健康检查这里我见过很多人只用TCP 2181端口做readiness这并不够因为ZooKeeper进程可能还在但已经不能响应客户端请求。用zkServer.sh status做exec探针虽然稍重但更接近真实可用性。3.4 针对会话超时的PodDisruptionBudget与优雅停机Kafka broker和ZooKeeper之间有一个会话超时时间通常只有几秒到几十秒不等。你控制不了机器故障但能控制“自愿性中断”比如节点维护、驱逐Pod这类运维操作。如果多个ZooKeeper节点同一时间被drain集群就可能选不出leader。k8s里可以用PodDisruptionBudget来限制这种自愿中断确保至少2个副本可用apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: zookeeper-pdb namespace: kafka-cluster spec: minAvailable: 2 selector: matchLabels: app: zookeeper另外要给ZooKeeper足够的优雅终止时间。虽然StatefulSet默认就是逐个更新Pod但如果terminationGracePeriodSeconds太短Kubelet可能会在ZooKeeper还没把事务日志刷完时就强制杀掉容器。我当时给ZooKeeper和Kafka都设置了不少于60秒的优雅退出时间并且没有在preStop里做额外重操作因为这两个应用本身已经能正确处理SIGTERM信号。4. Kafka集群让每个Broker拿到自己稳定的“身份证”ZooKeeper稳定之后Kafka侧的核心任务就是把每个Broker的broker.id、advertised.listeners和数据目录都固定下来。这三个参数里任何一个出错轻则客户端连不上重则数据分区重新分配甚至丢副本。4.1 broker核心参数broker.id、advertised.listeners与log.dirsbroker.id是Broker在Kafka集群里的唯一编号直接决定了它接管哪些分区副本。StatefulSet的Pod名称后缀天然可以作为broker.id所以启动脚本里可以这么取broker_id${HOSTNAME##*-}advertised.listeners是连接链路的重中之重。在容器里Kafka监听0.0.0.0:9092但它会把当前Pod的IP地址广播给客户端。如果客户端无法直接访问Pod IP连接就会失败。内部访问可以用kafka-0.kafka-hs.kafka-cluster.svc.cluster.local:9092这种DNS如果业务在集群外就必须把advertised.listeners配置成外部可达的地址和端口。log.dirs是消息日志目录必须挂到持久卷。如果没挂Pod每次重启数据就没了即使broker.id没变分区也会变成空白。另外一个很容易漏的配置是内部topic的副本因子。Kafka在运行中会创建__consumer_offsets、__transaction_state这些内部topic如果副本因子还是默认的1Broker一宕机这些内部topic就可能不可用。建议显式设置offsets.topic.replication.factor3 transaction.state.log.replication.factor3 transaction.state.log.min.isr2 min.insync.replicas2 default.replication.factor34.2 Kafka的StatefulSet与暴露方式Kafka也需要Headless Service。客户端通过kafka-0.kafka-hs.kafka-cluster.svc.cluster.local:9092访问指定BrokerapiVersion: v1 kind: Service metadata: name: kafka-hs namespace: kafka-cluster spec: clusterIP: None selector: app: kafka ports: - name: broker port: 9092 targetPort: 9092StatefulSet里我使用了一个ConfigMap保存固定配置然后在启动命令里动态拼接advertised.listeners和broker.id。这里的关键点是命令里要拿到Pod的hostname和namespace并通过${POD_NAMESPACE}把完整域名拼出来apiVersion: apps/v1 kind: StatefulSet metadata: name: kafka namespace: kafka-cluster spec: serviceName: kafka-hs replicas: 3 podManagementPolicy: Parallel selector: matchLabels: app: kafka template: metadata: labels: app: kafka spec: securityContext: fsGroup: 2000 runAsNonRoot: true containers: - name: broker image: kafka:3.4.0 ports: - name: broker containerPort: 9092 env: - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace volumeMounts: - name: datadir mountPath: /var/lib/kafka/data - name: kafka-config mountPath: /opt/kafka/config-custom command: - /bin/bash - -c - | broker_id${HOSTNAME##*-} cat /tmp/server.properties EOF broker.id${broker_id} listenersPLAINTEXT://0.0.0.0:9092 advertised.listenersPLAINTEXT://${HOSTNAME}.kafka-hs.${POD_NAMESPACE}.svc.cluster.local:9092 log.dirs/var/lib/kafka/data zookeeper.connectzookeeper-hs.kafka-cluster.svc.cluster.local:2181 offsets.topic.replication.factor3 transaction.state.log.replication.factor3 transaction.state.log.min.isr2 min.insync.replicas2 default.replication.factor3 EOF exec /opt/kafka/bin/kafka-server-start.sh /tmp/server.properties volumes: - name: kafka-config configMap: name: kafka-config volumeClaimTemplates: - metadata: name: datadir spec: accessModes: [ ReadWriteOnce ] storageClassName: high-throughput resources: requests: storage: 7Ti注意这里把Kafka的配置放在/tmp/server.properties是刻意为之。因为容器文件系统本来就是临时的而真正的数据在PVC上所以把动态配置放在临时目录没有任何问题。如果ConfigMap里有固定的server.properties也可以通过subPath挂载只是不要把它挂在会被命令覆盖的路径上。4.3 外部客户端如何访问Kafka集群这是最容易被低估的一步。Kafka的客户端第一次连接只需要找到一个Broker但拿到元数据后它会根据返回的advertised.listeners去分别连接每个Broker。如果你给三个Broker用一个共用的NodePort Service流量会被随机打到任意一个Broker客户端拿到的元数据又会指向别的地址来回几次就乱了。我当时的做法是给每个Broker单独建一个NodePort Service并且让advertised.listeners里带上对应该Broker的NodePort。比如kafka-0对应31090kafka-1对应31091kafka-2对应31092。Service用statefulset.kubernetes.io/pod-name这个标签精确选择PodapiVersion: v1 kind: Service metadata: name: kafka-0-external namespace: kafka-cluster spec: type: NodePort selector: app: kafka statefulset.kubernetes.io/pod-name: kafka-0 ports: - name: external port: 9092 targetPort: 9092 nodePort: 31090然后在kafka-0的启动脚本里把advertised.listeners写成PLAINTEXT://某节点IP:31090。其他两个Broker以此类推。如果k8s集群支持LoadBalancer也可以每个Broker单独创建一个LoadBalancer Service原理相同。Ingress对Kafka意义不大因为它不是HTTP协议没必要硬塞进去。4.4 Topic与分区副本规划Kafka集群跑起来以后创建topic时也要注意规格。一般建议分区数至少是Broker数的整数倍副本因子设为3。一条命令示例kubectl -n kafka-cluster exec kafka-0 -- \ kafka-topics.sh --bootstrap-server kafka-0.kafka-hs.kafka-cluster.svc.cluster.local:9092 \ --create --topic orders --partitions 3 --replication-factor 3有人会问分区数不是越多越好吗其实分区越多意味着各个Broker上的副本集合越大Leader切换和ISR同步的开销也会增加。对小团队来说3个分区配上3个副本已经足够覆盖大多数业务。生产端建议设置acksall并且配合min.insync.replicas2这样才能保证消息至少写入两个副本才返回成功。5. 验证闭环与常见踩坑从连接超时到僵死节点部署完成后真正的考验才开始。Kafka集群在k8s里最常见的几类问题表面现象都是“连接不上”“数据没了”“Broker掉线”但根因往往藏在配置、DNS、存储和更新策略这些细节里。我把自己遇到过的几个典型故障整理出来方便你遇到类似问题时按链路排查。5.1 完整自测链路先做最基础的自测。查看Pod和Service状态kubectl -n kafka-cluster get pods -o wide kubectl -n kafka-cluster get pvc然后确认ZooKeeper状态kubectl -n kafka-cluster exec zookeeper-0 -- zkServer.sh status kubectl -n kafka-cluster exec zookeeper-1 -- zkServer.sh status kubectl -n kafka-cluster exec zookeeper-2 -- zkServer.sh status再确认Kafka的Broker有没有注册到ZooKeeper。进入ZooKeeper后查看/brokers/ids目录应该能看到三个idkubectl -n kafka-cluster exec zookeeper-0 -- zkCli.sh -server 127.0.0.1:2181 ls /brokers/ids接着用Kafka自带命令创建topic启动一个console consumer再开一个console producer发消息确认端到端能通。测试Broker故障恢复时模拟删除一个Pod观察分区Leader是否会重新选举以及新的Pod重新加入后副本是否恢复正常。生产环境我还会用kafka-producer-perf-test.sh做一次压测但注意压测前先把副本状态调正常否则压测结果会被ISR不同步干扰。5.2 故障一ZK连接超时Kafka broker日志里经常出现Unable to connect to zookeeper或者Client session ended unexpectedly。我最开始遇到过这种问题第一个念头怀疑是镜像网络问题排查了一圈才发现是zookeeper.connect写错了。排查链路是这样的先看Kafka Pod里能不能解析ZooKeeper的Headless Service域名。如果Pod内没有解析工具可以用nslookup或者直接看/etc/resolv.conf。如果DNS解析正常再telnet 2181端口确认客户端端口可达。我当时的问题出在把zookeeper.connect写成了localhost:2181导致每个Broker都试图连自己的Pod端口当然连不上。后来改成zookeeper-hs.kafka-cluster.svc.cluster.local:2181才恢复。还有一个隐蔽问题如果同一个ZooKeeper集群被多个Kafka集群复用不同集群的broker.id可能冲突。解决办法是为每个Kafka集群分配独立的chroot比如zookeeper-hs.kafka-cluster.svc.cluster.local:2181/kafka-a。Kafka会把所有znode都放在这个前缀路径下两个集群就不会互相干扰。5.3 故障二滚动更新后Broker掉线或数据不一致做版本升级时StatefulSet默认是滚动更新每个Broker会依次被替换。理想情况下Kafka应该逐个优雅停机、替换、再加入集群。但如果你在原始配置里把Pod IP写死了滚动更新后Pod IP一变Broker广播的还是旧IP客户端就会连不上。我遇到过不止一次这样的问题新Pod启动后日志里显示新IP但客户端拿到的元数据里还是旧IP。排查之后发现问题出在配置里用了环境变量注入的Pod IP而不是DNS名。正确的做法是所有涉及集群内通信的地址都用Headless Service的稳定域名。还有一个更隐蔽的坑如果你把多个Broker的PVC共享到同一个目录比如用同一个NFS共享盘Kafka会认为自己接管了磁盘上所有分区最后出现数据损坏甚至无法启动。Kafka的每个数据卷必须是独享的访问模式用ReadWriteOnce就好千万不要图方便共享。5.4 故障三磁盘满触发“僵死节点”Kafka日志里出现Log directory is offline的时候通常不是进程挂了而是broker对应的分区目录无法继续写入。容器化之后这个问题更容易被忽视因为kubectl logs里不会直接显示磁盘占用而且日志文件和消息数据目录往往不在同一个文件系统下。我当时遇到一次Pod一直在Running但Kafka执行kafka-topics.sh超时查看指标才发现kafka_disk_used_bytes已经接近PVC上限。解决方法是先清理旧消息或者扩容PVC。需要注意的是不是所有StorageClass都允许在线扩容你需要提前确认allowVolumeExpansion: true。如果可以扩容修改PVC的spec.resources.requests.storage字段等待底层存储自动扩展。如果不支持在线扩容就只能备份后重建成本很高。所以容量规划一定要做在前面。6. 后续演进监控、备份与KRaft模式集群稳定跑起来之后真正考验运维能力的是运营期的监控、备份和版本演进。k8s环境下服务发现是动态的监控对象的标识也需要靠标签和DNS来定位不能再用写死IP的方式来配告警。6.1 监控层面要盯哪些指标ZooKeeper重点看znode数量、watch数量、leader选举次数、fsync耗时。Kafka重点看OfflinePartitionsCount、UnderReplicatedPartitions、ActiveControllerCount、IsrShrinksPerSec、请求队列大小、网络线程空闲率以及磁盘使用率。社区有一些现成的exporter和Grafana dashboard部署起来不复杂但要注意exporter的bootstrap.servers一定要写成kafka-0.kafka-hs.kafka-cluster.svc.cluster.local:9092这样的DNS不能用临时Pod IP。告警阈值也不要拍脑袋。比如UnderReplicatedPartitions如果长期大于0说明有副本没有跟上要立刻排查是网络、存储还是Broker负载问题。如果只是偶尔尖峰可以结合IsrShrinksPerSec看持续时间。磁盘使用率建议75%就预警80%以上就要处理因为Kafka的日志清理动作通常发生在85%之后如果清理跟不上Broker会很快“僵死”。6.2 备份与容灾策略ZooKeeper的数据虽然只有几十MB到几GB但损坏了可能导致整个Kafka集群失去元数据所以还是要备份。备份ZooKeeper事务日志和快照文件时最好在集群相对空闲时进行避免把最新的事务日志截断。Kafka的消息数据备份最稳妥的方式不是直接拷贝文件而是依靠副本机制本身。如果整个集群的副本都损坏那就只能做磁盘级快照。在k8s里用PVC快照要看底层存储能力但要注意一致性快照不能简单地在Broker运行时拍一张块设备快照就完事最好先暂停该Broker的写入否则快照恢复后可能出现日志和索引不一致。6.3 考虑未来KRaft模式去掉ZK依赖Kafka 3.x之后正在逐步进入KRaft模式新集群理论上可以不再需要ZooKeeper。如果你是从零开始我建议先测试KRaft版本。在k8s上部署KRaft的话核心难点变成了controller quorum的固定身份问题同样需要StatefulSet和Headless Service。好处是整个集群少了一个故障域不再需要同时维护两套分布式系统。我这次因为迁移和兼容性原因仍然用了ZooKeeper模式但如果再给我一次机会我会优先考虑KRaft毕竟少一个依赖就是少一类深夜告警。最后分享一点个人体会k8s里跑有状态应用真正难的不是把YAML写出来而是你要能预判“Pod被重新调度之后会怎样”。所有配置里只要出现临时IP就是一个定时炸弹。所有数据目录只要没有挂持久卷就是一个空壳集群。部署完成后一定要把故障演练做一遍删Pod、断网、压满磁盘都试一次确认集群能在各种意外里自己恢复才敢放心把流量切进去。这套集群上线后我经历过几次深夜告警最后定位下来都不是玄学问题都出在当初配置时没有遵循“固定身份 独立存储 可解析DNS”这三条基本原则。希望这篇文章能帮你少走我走过的弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑