
老实说看到还有人在照着两三年前的教程在 Docker 里先起一个 ZooKeeper 容器、再起一个 Kafka 容器我是有点着急的。Kafka 从 3.3 版本开始就已经正式支持 KRaft 模式也就是用 Kafka 自己的 Raft 协议来管理元数据彻底摆脱 ZooKeeper到了 Kafka 4.0ZooKeeper 更是被直接移除了。这篇文章我从一个经常搭开发环境的人的角度把 Docker 单机部署 Kafka、以 KRaft 模式运行、全程不碰 ZooKeeper 的完整过程写一遍。适合正在搭 Kafka 学习环境、做 Spring Boot 本地联调或者想快速评估 Kafka 功能但不想折腾两个中间件的人。1. 为什么丢掉 ZooKeeperKRaft 模式到底改变了什么1.1 从 ZooKeeper 到 KRaftKafka 元数据管理演进早期 Kafka 的架构里ZooKeeper 承担的是“元数据存储 集群协调”的职责比如 broker 节点注册、topic 分区信息、controller 选举、消费者 offset旧版本等等全都放在 ZooKeeper 里。很多第一次接触 Kafka 的人都会一脸懵我明明只是想用消息队列为什么还要多伺候一个 ZooKeeperZooKeeper 本身也是一个高可用协调服务部署时至少三节点才能达到生产可用的标准。维护它并不轻松机器多了之后Kafka 和 ZooKeeper 之间的网络通信、ZooKeeper 的性能瓶颈、ZooKeeper 集群本身的故障恢复都是运维上非常头疼的问题。尤其当 Kafka topic 数量暴涨时ZooKeeper 上的元数据节点会迅速膨胀频繁地读写很容易把它拖垮。KRaft 模式的核心变化是不再把元数据放到外部组件而是由 Kafka 自己通过 Raft 协议将元数据日志复制到多个 controller 节点。元数据变更会形成一份追加写入的日志存储在 Kafka 内部的__cluster_metadata主题中。Controller 节点之间相互投票选举出 leader由 leader 接收和广播元数据变更。用大白话解释以前 Kafka 的“档案室管理员”是外聘的 ZooKeeper流程烦琐、沟通成本高现在 Kafka 自己内部设立了“档案室”几个 controller 自己开会投票少数服从多数档案记录全都内部消化。整个系统少了一个外部依赖链路更简单出问题的概率也低了很多。1.2 单机部署去掉 ZooKeeper 后你感受到的四个变化单机部署是 KRaft 模式收益最直观的场景。第一部署组件少一个。以前一条 Kafka 要起两个容器现在只起一个docker-compose 文件短了至少一半。对本地开发和测试来说少一个进程意味着更少的内存占用和更快的冷启动。第二启动时间大幅缩短。ZooKeeper 模式下Kafka 启动前要等 ZooKeeper 集群就绪Kafka 启动后又要做大量的元数据同步KRaft 模式单节点只有一个 controller 进程日志回放和加载快很多基本几秒钟就能完成启动。我实测在普通笔记本上从docker compose up到 Kafka 完全可用10 秒左右。第三没有额外的 ZooKeeper 端口和目录要管。以前要操心 zk 的 2181 端口、zoo.cfg、myid 文件现在只需要关心 Kafka 自己的端口和日志目录。第四单机模式和数据目录之间的迁移更加自然。KRaft 模式下所有元数据都作为日志文件存在本地备份、迁移、恢复本质上就是拷贝目录不用再考虑两套系统之间数据怎么对应。1.3 版本选择KRaft 发展历程与你的最优解如果你在网上搜 Kafka 部署教程还会搜到很多2.8、2.9、3.0时代的文章里面全是 ZooKeeper 的配置。这里简单过一下 KRaft 的版本节点Kafka 2.8KRaft 早期预览版本只能用于测试不建议生产。Kafka 3.3KRaft 正式宣布生产可用主流环境开始支持。Kafka 3.5 ~ 3.9KRaft 持续完善社区主流部署都在这个区间。Kafka 4.0直接移除 ZooKeeper 模式ZooKeeper 相关配置和脚本彻底退役。我现在用的版本是3.7.1这个版本已经很成熟同时4.x也已经出现在 Docker Hub 上。如果你只是本地学习用选3.7.x或者3.8.x都行不用追最新如果公司生产环境已经跑在 3.x 上本地环境建议保持和线上大版本一致避免行为差异。注意尽量别再用kafka 2.x的旧镜像和旧教程。旧教程里大量--zookeeper参数在新版本 CLI 中已经彻底失效照着敲只会得到报错。2. 部署前的自我提问镜像、端口、目录和三个关键配置2.1 镜像选型不同 Kafka 镜像的差异和坑Docker Hub 上常见的 Kafka 镜像有三类它们的差异不小选错镜像会让后续配置思路完全跑偏。第一类是 Apache 官方镜像apache/kafka。这是我现在最推荐给本地开发者的。它从 3.7 开始在 Docker Hub 上活跃默认就是 KRaft 模式启动脚本会自动生成集群 ID、自动格式化存储目录环境变量使用KAFKA_前缀覆盖配置。配置直观和 Kafka 原生命名方式几乎一致踩坑成本低。第二类是 Bitnami 镜像bitnami/kafka。它也很流行但它使用了KAFKA_CFG_前缀比如KAFKA_CFG_PROCESS_ROLES、KAFKA_CFG_ADVERTISED_LISTENERS。如果你照着官方文档写习惯了很容易把前缀写错。这个镜像也支持 KRaft但对新手不够友好。第三类是 Confluent 镜像confluentinc/cp-kafka。这是 Confluent 平台的一部分里面带了一些平台化的包装和扩展镜像体积大很多配置是为他们的控制台服务的。除非你在用 Confluent 生态否则本地单机部署没必要选它。所以本文所有配置都基于apache/kafka:3.7.1。2.2 端口与目录规划9092 和 9093 谁给谁用KRaft 单机模式下我们至少需要两个 listenerPLAINTEXT监听9092给客户端生产者、消费者、可视化工具连接使用。CONTROLLER监听9093给 controller 节点之间通信使用外部不需要暴露。单机单容器时9093只在容器内部使用不需要映射到宿主机。我们把宿主机9092映射到容器9092宿主机程序就能直接通过localhost:9092连接 Kafka。数据目录我会映射到当前目录下的./kafka-data对应容器内/var/lib/kafka/data。这样即使容器删掉重来Kafka 的元数据和消息日志都还在不会出现“重启一次topic 全没”的尴尬。如果你不挂数据卷容器一删消息和 topic 配置全部蒸发本地学习可能无所谓但做联调测试时真的会哭。2.3 必须先搞懂的三个 KRaft 关键配置项第一个是process.roles。单机模式下我们让同一个节点同时扮演 broker负责消息读写和 controller负责元数据管理所以填broker,controller。第二个是controller.quorum.voters。它声明了控制器集群都有哪些节点。格式是nodeIdhost:port多个节点用逗号分隔。单机单节点就写1localhost:9093。这一段以后扩展成多节点时最重要节点 ID 和地址必须真实可解析。第三个是advertised.listeners。很多新手第一次部署 Kafka 报错90% 都是因为这个配置。它是 broker 对外公布的“客户端应该来连接我的地址”。如果这台 Kafka 运行在 Docker 容器里而客户端跑在宿主机上那这个地址必须填宿主机能够访问的地址最简单就是PLAINTEXT://localhost:9092。注意不要把listeners和advertised.listeners混淆。listeners是 Kafka 进程实际绑定的监听地址在容器里可以写0.0.0.0advertised.listeners是 Kafka 告诉客户端“你来连我”的地址。如果后者填了容器内网 IP 或容器 ID 主机名宿主机客户端能连上 broker但后续 fetch metadata 时会被 broker 引导到一个谁也连不上的地址于是出现经典的Error while fetching metadata报错。3. 实操Docker Compose 一键启动 KRaft 单机 Kafka3.1 编写 docker-compose.yml 并逐行解读先创建一个项目目录比如kafka-kraft-single里面放一个docker-compose.yml内容如下services: kafka: image: apache/kafka:3.7.1 container_name: kafka restart: unless-stopped ports: - 9092:9092 environment: CLUSTER_ID: 4L6g3nShT-eMCtK--X86sw KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_CONTROLLER_QUORUM_VOTERS: 1localhost:9093 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LOG_DIRS: /var/lib/kafka/data KAFKA_AUTO_CREATE_TOPICS_ENABLE: true KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 volumes: - ./kafka-data:/var/lib/kafka/data逐行说几个重点CLUSTER_ID是整个 Kafka 集群的全局标识。同一个集群内的节点必须使用相同值官方镜像允许你手写不写的话它也会自动生成。我建议显式写死因为以后你从单机扩展到三节点集群或者把数据目录备份到其他机器恢复都需要这个 ID 保持一致。这个值是一个 22 字符的 Base64 URL 安全字符串符合规则即可比如MkU3OEVBNTcwNTJENDM2Qk这种。KAFKA_NODE_ID是当前节点的唯一编号单节点写 1。以后加节点就得按1、2、3分配不能重复。KAFKA_LISTENERS里有两个 listenerPLAINTEXT绑定0.0.0.0:9092允许外部访问CONTROLLER绑定0.0.0.0:9093监听所有网卡但只有集群内部会用到。之所以不映射 9093 到宿主机是没必要也避免端口占用问题。KAFKA_AUTO_CREATE_TOPICS_ENABLE我专门设置成开启。本地开发环境开启自动创建 topic省得每条消息之前都手动建 topic。生产环境建议关掉防止手误写错 topic 名导致一堆自动创建的空主题。KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR、KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR、KAFKA_TRANSACTION_STATE_LOG_MIN_ISR这三个单机环境必须设为 1。否则 Kafka 内部创建__consumer_offsets等内部主题时要求的副本因子默认是 3单机只有 1 个 broker分区副本根本凑不齐系统会一直报错或拒绝创建。KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS设为 0目的是让消费者组在测试时立即进入 rebalance不用傻等 3 秒默认延迟。本地联调体验会爽很多。3.2 启动容器并确认服务状态在docker-compose.yml所在目录执行docker compose up -d首次启动会拉取镜像取决于网络情况可能需要一点时间。启动完成后执行docker ps应该能看到kafka容器处于Up状态。然后看日志确认 Kafka 真正启动完成docker logs -f kafka日志里出现类似Kafka Server started或者started (kafka.server.KafkaRaftServer)的字样就说明没问题。如果只是容器 Up但日志里一直在刷错误那等于没起来别急着下一步。顺手验证端口是否起来nc -vz localhost 9092如果提示 connected端口通了。这一步简单但很有效可以先排除“容器起来了但 listener 绑定失败”的情况。提示执行docker compose命令时注意你的 Docker 版本。老版本需要docker-compose中间带横线新版本推荐docker compose空格形式。如果提示命令不存在就换成横线写法。3.3 从创建 Topic 到收发消息完整走一遍现在进入容器用 Kafka 自带的命令行工具做一次完整验证。创建 topicdocker exec -it kafka /opt/kafka/bin/kafka-topics.sh \ --bootstrap-server localhost:9092 \ --create --topic orders --partitions 3 --replication-factor 1看到Created topic orders就成功了。注意参数用的是--bootstrap-server不是旧版的--zookeeper。新版工具连接 Kafka 本身就能完成元数据操作这也是 KRaft 模式带来的简化。查看 topic 详情docker exec -it kafka /opt/kafka/bin/kafka-topics.sh \ --bootstrap-server localhost:9092 \ --describe --topic orders输出里能看到Topic: orders、PartitionCount: 3、每个分区的Leader、Replicas和Isr信息。因为只有一个 broker所以每个分区 Leader 都是 1ISR 也只有 1 个节点这是单机环境的正常情况。打开一个终端启动控制台生产者docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \ --bootstrap-server localhost:9092 \ --topic orders输入几条消息比如10001,create_order 10002,pay_order 10003,cancel_order然后按CtrlC退出。再开另一个终端启动控制台消费者docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \ --bootstrap-server localhost:9092 \ --topic orders \ --from-beginning如果能看到刚才输入的三条消息恭喜整个 Kafka 单机链路已经通了。4. 客户端连接与可视化别只会用容器命令行4.1 容器内 CLI 的使用姿势进入容器内执行命令有很多种方式我建议写成带完整路径的docker exec形式不要每次docker exec -it kafka bash进入交互 shell再手敲命令。因为脚本化、历史记录、复制粘贴都更脆而且-it在 CI 环境或某些终端工具里会出问题。Kafka 安装目录在镜像里的默认位置是/opt/kafka命令行工具全在/opt/kafka/bin下。常用工具包括kafka-topics.sh管理 topic。kafka-console-producer.sh控制台生产者。kafka-console-consumer.sh控制台消费者。kafka-configs.sh查看和修改配置。kafka-consumer-groups.sh查看消费者组状态和 offset。想省事的话可以在宿主机.bashrc或者.zshrc里加几个 alias比如alias kafka-topicsdocker exec -it kafka /opt/kafka/bin/kafka-topics.sh alias kafka-console-producerdocker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh alias kafka-console-consumerdocker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh之后在宿主机上直接敲kafka-topics --bootstrap-server localhost:9092 --list体验几乎和本地装了 Kafka 一样。4.2 Spring Boot 连接本地 Kafka 的配置如果你主要用 Java 开发Spring Boot 项目连接这个本地 Kafka 非常简单。先在pom.xml引入依赖dependency groupIdorg.springframework.kafka/groupId artifactIdspring-kafka/artifactId /dependency然后在application.yml里写spring: kafka: bootstrap-servers: localhost:9092 producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer consumer: group-id: demo-group auto-offset-reset: earliest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer为什么这里bootstrap-servers可以直接写localhost:9092因为我们在 compose 里已经把宿主机 9092 映射到了容器内 Kafka 的PLAINTEXTlistener同时advertised.listeners也声明为PLAINTEXT://localhost:9092客户端从 metadata 里拿到的地址就是localhost:9092所以双向都通。写一个简单的发送接口Service public class KafkaSendService { private final KafkaTemplateString, String kafkaTemplate; public KafkaSendService(KafkaTemplateString, String kafkaTemplate) { this.kafkaTemplate kafkaTemplate; } public void send(String topic, String message) { kafkaTemplate.send(topic, message); } }配一个KafkaListener就能消费Component public class KafkaConsumer { KafkaListener(topics orders, groupId demo-group) public void onMessage(String message) { System.out.println(received: message); } }这套组合在我本地跑得非常顺Spring Boot 的 KafkaAutoConfiguration 会自动识别配置基本零额外代码。4.3 Kafdrop一行配置给你一个消息控制台命令行验证没问题后建议加一个可视化工具日常查看 topic、看分区偏移量会舒服很多。我用得比较多的是 Kafdrop它是一个轻量级 Web 页面直接在 Docker Compose 里加一个服务即可。kafdrop: image: obsidiandynamics/kafdrop:latest container_name: kafdrop restart: unless-stopped ports: - 9000:9000 environment: KAFKA_BROKERCONNECT: kafka:9092 JVM_OPTS: -Xms32M -Xmx64M depends_on: - kafka启动后浏览器访问http://localhost:9000就能看到所有 topic 列表。点进orders可以看到分区数、副本情况、每分区当前 offset 等。Kafdrop 自己也提供创建 topic、查看消息等操作本地调试非常好用。提示Kafdrop 容器和 Kafka 容器在同一个 compose 网络里所以KAFKA_BROKERCONNECT填服务名kafka:9092。如果 Kafdrop 跑在宿主机上那就要填localhost:9092。别用错连接地址这是新手最容易犯的错误。5. 常见问题与排查实录解决你 80% 的部署报错5.1 fetch metadata 报错的根因与修复这个报错几乎每个用 Docker 跑 Kafka 的人都会遇到完整信息类似Error while fetching metadata with correlation id 3 : {ordersLEADER_NOT_AVAILABLE}或者客户端直接报Connection to node -1 could not be established. Broker may not be available.先说结论大多数情况下不是你 Kafka 没启动而是advertised.listeners配置让客户端找不到正确的 broker 地址。举个例子如果你只设置了KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092没有显式设置KAFKA_ADVERTISED_LISTENERS镜像可能会把 broker 的 hostname比如容器 ID 生成的字符串作为 advertised 地址广播给客户端。客户端拿到xxx:9092这个地址在自己的网络里根本解析不了于是连接失败。修复方法就是像我前面 compose 里那样显式声明KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092如果客户端不在宿主机上而是在另外一台机器这里的localhost也要换成宿主机的局域网 IP例如KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.1.100:9092简单判断方法客户端连上后立刻看 Kafka 日志如果看到类似Metadata request from ...的日志且客户端最终报找不到节点就是 advertised 地址不可达。在宿主机执行ping一下客户端拿到的 hostname感受更直观。5.2 Docker 镜像拉取慢怎么办这个问题在本地开发时很常见因为官方镜像比较大。解决方案不复杂就是给 Docker 配置镜像加速源。Linux 下编辑/etc/docker/daemon.jsonWindows/Mac 在 Docker Desktop 的 Settings - Docker Engine 里编辑加入{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com ] }然后重启 Docker。需要注意的是这类公共加速源的稳定性经常变化如果你有云服务商提供的专属加速地址优先用自己的。拉取失败时多换几个源或者重试几次。如果你在公司内网可能有内部的 Docker Registry 或者镜像仓库也可以把镜像搬运到内部仓库后手动pull。总之先确认是网络问题再动手改 Docker 配置别一上来就怀疑 Kafka 镜像有问题。5.3 容器重启后 Kafka 数据去哪里了这是 Docker 使用率最高的话题。如果你没有挂载数据卷容器一旦docker rmKafka 的所有数据就没了。之前创建的 topic 全消失所有消息也没了。所以我从一开始就建议挂载目录volumes: - ./kafka-data:/var/lib/kafka/data这样即使执行以下命令docker compose down docker compose up -d容器重建后Kafka 会自动从/var/lib/kafka/data读取元数据和日志之前的 topic 和消息都还在。如果你想把数据彻底清空重新来就把宿主机上kafka-data目录删掉再启动容器。注意删除前确认你不需要里面的数据因为 Kafka 的日志文件可不像 MySQL 的 binlog 那么好恢复。另外提醒一句如果你手动改过KAFKA_NODE_ID或者CLUSTER_ID而数据目录里保存的元数据还是旧的 ID启动时可能会报存储不匹配的错误。遇到这种情况要么把CLUSTER_ID改回原来的值要么清空数据卷重新初始化。5.4 几个容易被忽略的细节第一个是端口占用。本地经常遇到 9092 被占用因为可能有另一个 Kafka或者别的服务占了这个端口。启动前先用lsof -i :9092或netstat -ano | grep 9092检查一下。占用了就把宿主机端口改成其他值比如19092:9092但注意advertised.listeners也要同步改成PLAINTEXT://localhost:19092。第二个是内存不足。Kafka 本身是 Java 进程默认堆内存可能给得比较大本地机器如果只有 4G 内存跑起 Kafka 再开 IDEA、Docker Desktop很容易卡顿甚至 OOM。如果你觉得内存紧张可以在 compose 里加上KAFKA_HEAP_OPTS: -Xmx512M -Xms256M本地测试 512M 堆足够甚至 256M 也能跑简单 demo。生产环境当然不这么抠但本地验证问题不大。第三个是重启策略。compose 里我写了restart: unless-stopped这样 Docker 重启后 Kafka 容器会自动起来不用每次手动启动本地开发很省心。如果你希望手动控制生命周期也可以不写看个人习惯。第四个是防火墙。有些 Linux 发行版默认防火墙开着宿主机上的客户端连接不到 Kafka。如果在本机验证没问题但局域网其他机器连不上检查一下 9092 端口是否被防火墙拦截。这种情况不算 Kafka 配置问题但排查时很容易卡住。6. 从单机到折腾KRaft 模式还能怎么扩展6.1 单机三容器用一个 Compose 模拟真实集群单机跑通后下一步很多人会好奇KRaft 模式的多节点集群要怎么搭在 Docker 里模拟三节点并不难思路和单机非常接近只是每个节点都有自己的NODE_ID并且controller.quorum.voters要把三个节点都写进去。下面是一个极简的示例端口映射做了区分避免冲突services: kafka1: image: apache/kafka:3.7.1 container_name: kafka1 ports: - 19092:9092 environment: CLUSTER_ID: 4L6g3nShT-eMCtK--X86sw KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: controller,broker KAFKA_CONTROLLER_QUORUM_VOTERS: 1kafka1:9093,2kafka2:9093,3kafka3:9093 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:19092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LOG_DIRS: /var/lib/kafka/data KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2 volumes: - ./kafka1-data:/var/lib/kafka/data kafka2: image: apache/kafka:3.7.1 container_name: kafka2 ports: - 29092:9092 environment: CLUSTER_ID: 4L6g3nShT-eMCtK--X86sw KAFKA_NODE_ID: 2 KAFKA_PROCESS_ROLES: controller,broker KAFKA_CONTROLLER_QUORUM_VOTERS: 1kafka1:9093,2kafka2:9093,3kafka3:9093 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:29092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LOG_DIRS: /var/lib/kafka/data KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2 volumes: - ./kafka2-data:/var/lib/kafka/data kafka3: image: apache/kafka:3.7.1 container_name: kafka3 ports: - 39092:9092 environment: CLUSTER_ID: 4L6g3nShT-eMCtK--X86sw KAFKA_NODE_ID: 3 KAFKA_PROCESS_ROLES: controller,broker KAFKA_CONTROLLER_QUORUM_VOTERS: 1kafka1:9093,2kafka2:9093,3kafka3:9093 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:39092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LOG_DIRS: /var/lib/kafka/data KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 3 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2 volumes: - ./kafka3-data:/var/lib/kafka/data这个配置里三个节点都同时承担 controller 和 broker 角色形成一个 3 节点混合集群。因为副本因子设成了 3所以单机单 broker 情况下没法自动创建 topic 的REPLICATION_FACTOR问题在这里也不存在了。你可以创建--replication-factor 3的 topic然后手动停掉一个容器观察 topic 分区的 Leader 切换这个操作对理解 Kafka 高可用非常有帮助。当然真要用这种模式做生产建议 controller 和 broker 角色分离节点职责更清楚故障影响面也更小。本地模拟集群主要为了学习混合角色足够。6.2 部署之外一些我踩出来的使用习惯文章最后说说我自己用 Docker 跑 Kafka 的一些习惯都是一次次踩坑后留下来的。第一个习惯是尽量保持 Kafka 镜像版本和客户端依赖版本接近。比如 Spring Boot 自带的 spring-kafka 版本如果比较旧而 Kafka 服务端已经升到 4.x一些内部协议和兼容性会有差异。本地开发时镜像用 3.7客户端也用支持 3.7 的库能省掉很多莫名其妙的兼容性问题。第二个习惯是重视docker compose down和docker compose down -v的区别。down不会删数据卷down -v会连带删除所有命名的 volume。如果你用的是./kafka-data这种 bind mount-v也不会删宿主机目录。所以想清理环境时先想清楚你是要“停服务”还是“删数据”别被一条命令清掉所有调试进度。第三个习惯是遇到异常先看 Kafka 服务端日志而不是只盯客户端报错。很多 Kafka 客户端报错信息很泛比如 “TimeoutException”但真正的原因可能在 broker 日志里写得很明确比如 “Configuration does not match” 或者 “Log directory not found”。执行docker logs -f kafka是最快定位问题的方式之一。第四个习惯是本地模拟消息延迟场景时不需要改 Kafka 本身配置。像网上很火的“如何延迟 30 分钟消费”这类问题本质上是消费侧逻辑你可以给消息体带一个expectedProcessTime时间戳消费者拿到后判断如果没到时间就暂存到内存或数据库中到点再处理。也可以用 Redisson 的延迟队列做一层业务封装。这些都是应用层设计和 Kafka 部署没有直接关系别一开始就试图从 broker 侧做全局延迟那样会把架构搞得很复杂。回到部署这件事本身。KRaft 模式让 Kafka 的容器化部署变得异常清爽单机一条 compose 文件就能跑起来根本不用关心 ZooKeeper 怎么协调。你只要理解了advertised.listeners、process.roles和controller.quorum.voters这三个核心配置后续无论是本地开发、测试联调还是扩展成多节点集群都不会有太大障碍。如果你正准备开始学 Kafka直接用这套 KRaft 方案起步可以少走很多弯路。