使用Docker单机部署Kafka:KRaft模式完全替代ZooKeeper实操指南
2026/9/13 2:12:30 网站建设 项目流程

以前在本地折腾Kafka,最烦的不是Kafka本身,而是旁边那个ZooKeeper。明明只是想跑个消息队列做验证,却要同时伺候两个 Java 进程:版本要匹配、启动有先后,稍微哪里不对就是一连串莫名其妙的报错。Kafka 从 3.3 开始引入的 KRaft 模式,把 ZooKeeper 彻底换掉了——元数据由 Kafka 自己通过 Raft 协议管理,不再需要独立的协调组件。单机场景下,我们甚至可以把 controller 角色和 broker 角色放进同一个进程,一个容器、一份配置就能跑起来。这篇文章我会把"使用 Docker 单机部署 Kafka,以 KRaft 模式运行、完全不依赖 ZooKeeper"的完整过程整理出来,包括镜像选型、监听器配置、生产消费验证,以及我实际部署中踩过的几个坑,适合想在本地快速搭一套 Kafka 做开发测试、或在 CI 里做集成验证的读者。

1. KRaft模式到底解决了什么问题:先弄明白为什么要换掉ZooKeeper

1.1 ZooKeeper时代的三个痛点

早期Kafka的架构中,ZooKeeper扮演的角色是集群元数据存储和协调者:broker注册、topic配置、分区副本分配、消费者组位移,全都要靠ZooKeeper来记录和同步。听起来很合理,但实际用起来有三类痛点。

第一个痛点是部署复杂度。想跑Kafka,得先单独搭ZooKeeper。哪怕只是本地单机测试,也要下载两个包、起两个进程、配两套参数。容器化之后稍微好一点,但依然要编排两个容器,还要处理启动顺序——ZooKeeper必须先Ready,Kafka再启动,否则broker起不来。docker-compose只能靠healthcheck写依赖,多一层维护成本。

第二个痛点是版本兼容。Kafka版本和ZooKeeper版本之间有明确的兼容要求,升级Kafka时经常要同步升级ZooKeeper,一旦版本对不上,可能连启动都过不去。这个问题在社区里被吐槽了很久,因为它的排错方向往往不是Kafka本身,而是你"外面的那套东西"是不是装对了。

第三个痛点,也是更隐蔽的痛点是资源与运维。ZooKeeper本身是个Java应用,内存占用不小;生产环境还要单独监控、调参数、告警。一个消息中间件,部署时却相当于多引入了一套分布式协调系统,无论从学习成本还是运维成本看都很重。大概就是这个原因,越来越多人在新项目里想绕开它。以前在团队里带新人,光解释"为什么会有ZooKeeper"就要花掉半天,KRaft出现以后,这门课可以直接跳过了。

1.2 KRaft的核心变化:Kafka自己管好自己的元数据

KRaft说白了就是把原来交给ZooKeeper的活,交给了Kafka自己。Kafka内部引入了一个元数据日志,所有的集群元数据都写入一个叫__cluster_metadata的内部主题,集群里有一个或多个 controller 角色节点,它们通过 Raft 协议对这个日志做投票和同步。集群里的 broker 只需要和 controller 通信,就能拿到最新元数据。

对单机部署来说,KRaft带来的最大好处是可以通过组合模式(combined mode)在一个进程里同时跑 controller 和 broker 角色。也就是说,不需要多余的协调组件,只要一个容器就是一套完整的Kafka。Kafka 3.3.1开始把KRaft标记为生产可用;到了Kafka 4.x,ZooKeeper模式已经被彻底移除了,继续学老的部署方式,路只会越走越窄。

我用一个粗糙的类比来理解这件事:以前公司里财务和审计分两个办公室,审计(ZooKeeper)要先到场,财务(Kafka)才能开工,两套班子定期对账;KRaft相当于把审计职能收进财务部,一个人把所有账管了。流程简化了,出了问题也容易排查。

2. 部署前的关键选择:镜像、端口与目录规划

2.1 镜像选型:为什么我推荐官方apache/kafka

先看镜像。现在社区里常见的Kafka镜像有这么几类:

  • apache/kafka官方镜像,从3.7版本开始由Apache官方维护。它直接用Apache Kafka发行版打底,环境变量和Kafka原生配置项一一对应,透明、好排查,我推荐这一类。
  • bitnami/kafka,封装程度高,许多默认配置帮你做了。但它默认走ZooKeeper模式,要切到KRaft需要额外设一串KAFKA_CFG_*KAFKA_ENABLE_KRAFT=yes,一套自己的配置体系,反而不利于理解底层原理。
  • wurstmeister/kafka这类老镜像,基本停更多年,不建议新项目使用。

版本方面,至少要选3.3.1以上的版本。我这里以apache/kafka:3.9.0为例。生产环境或长期项目建议固定到小版本tag,不要追latest,否则镜像内容变了、配置可能不兼容,排错成本很高。我刚踩过一次类似的事:同事用了latest,第二天镜像更新,兼容性变化导致topic元数据异常,查了半天才发现是tag漂移。

2.2 端口与监听器:把LISTENER和ADVERTISED_LISTENER搞清楚

KRaft模式下,Kafka进程要承担两个角色,所以需要监听不同的端口:

端口监听器名称用途
9092PLAINTEXTbroker对外服务,供生产者和消费者连接
9093CONTROLLERcontroller之间通信的Raft端口

这两个端口对应两个监听器,默认都不需要加密(PLAINTEXT协议)。单机部署时,9093虽然只有一个节点在用,但controller的Raft通信端口仍然要监听,quorum配置里必须写上,否则controller自己无法形成法定人数。

这里最容易搞混的是listenersadvertised.listeners的区别。listeners是Kafka进程实际绑定的网卡和端口,好比家里电话机的接口;advertised.listeners是Kafka告诉客户端的对外连接地址,好比印在名片上的电话号码。客户端一定拿着名片上的地址去找你。如果你的容器映射了宿主机端口,但advertised里写的是容器内部地址,外部客户端就无论如何连不上。

所以我在下面的配置里使用:

  • KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093,让两个listener都绑定容器内所有网卡地址。
  • KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092,告诉客户端"你在宿主机上连127.0.0.1:9092就能找到我"。
  • KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,把监听器名映射到安全协议。

如果你需要让局域网里其他机器访问,只要把advertised里的地址改成宿主机在局域网中的IP即可,listeners不用动。这个理解到位以后,绝大多数"容器里能连、容器外连不上"的问题,都能自己定位。

2.3 数据目录与持久化

Kafka默认把消息日志和元数据日志都写在log.dirs目录里。官方镜像下通常是/var/lib/kafka/data。跑容器时一定要挂载volume,否则容器一旦被删,所有topic和数据直接消失。

挂载方式有两种:

  • 命名卷:-v kafka-kraft-data:/var/lib/kafka/data,由Docker管理,方便迁移和备份。
  • 绑定挂载:-v $PWD/kafka-data:/var/lib/kafka/data,调试时能直接看宿主机目录里的文件,更直观。

我更推荐绑定挂载,尤其在本地调试阶段。因为一旦发生"集群ID不匹配"之类的问题,你直接在宿主机上查看meta.properties__cluster_metadata-0目录,排查效率会高很多。后面第5章我会专门讲这个问题。

3. 单机KRaft部署实操:两种方式从零跑通

3.1 方式一:docker run快速验证

如果你只是想最快速度起一个Kafka来验证代码,一条docker run就够了。以下命令我逐段说明,建议不要直接抄完就跑,至少把参数看明白。

docker run -d \ --name kafka-kraft \ -p 9092:9092 \ -e KAFKA_NODE_ID=1 \ -e KAFKA_PROCESS_ROLES=broker,controller \ -e KAFKA_CONTROLLER_QUORUM_VOTERS=1@localhost:9093 \ -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 \ -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \ -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \ -e KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER \ -e KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT \ -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \ -e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 \ -e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1 \ -e KAFKA_AUTO_CREATE_TOPICS_ENABLE=true \ -e KAFKA_HEAP_OPTS="-Xmx512M -Xms512M" \ -v kafka-kraft-data:/var/lib/kafka/data \ apache/kafka:3.9.0

逐个说关键参数:

  • KAFKA_NODE_ID=1:当前节点的ID,单机没有特殊要求,但必须有一个唯一数字。
  • KAFKA_PROCESS_ROLES=broker,controller:这是KRaft组合模式的核心开关。如果两个角色分离部署,这里就要分开写,但单机合体就写这一行。
  • KAFKA_CONTROLLER_QUORUM_VOTERS=1@localhost:9093:controller quorum的投票者列表。格式是节点ID@host:端口。因为单机只有一个controller节点,且监听地址是容器内的9093,所以写1@localhost:9093就可以了。这里有个容易忽略的点:voters里的地址是Kafka内部Raft节点之间互相连接用的,不用经过宿主机端口映射,所以写localhost完全没问题。
  • KAFKA_CONTROLLER_LISTENER_NAMES=CONTROLLER:告诉Kafka,名为CONTROLLER的监听器是用于controller通信的。
  • KAFKA_INTER_BROKER_LISTENER_NAME=PLAINTEXT:broker之间内部通信也走PLAINTEXT监听器。单机时可能无所谓,但显式写出来可以避免多监听器场景下的歧义。
  • KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1:Kafka在创建消费者位移内部主题__consumer_offsets时默认副本数是3,单机只有一个broker,必须改成1,否则创建失败。
  • KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1:同理,事务状态日志的副本和最小ISR,单机都改成1。
  • KAFKA_HEAP_OPTS="-Xmx512M -Xms512M":限制堆内存。Kafka默认堆上限通常不小,在小内存机器上容易直接OOM,本地测试压到512M足够。

启动以后,先看一眼日志确认没有异常:

docker logs -f kafka-kraft

看到类似Kafka Server started的日志,就已经起来了。

提示:apache/kafka官方镜像在检测到process.roles包含 controller 且存储目录为空时,会自动执行存储格式化逻辑,不需要手动kafka-storage.sh format。稍后我会提到,某些情况下自动格式化不生效,需要手动处理。

3.2 方式二:docker-compose推荐

实际开发环境我更推荐用docker-compose,配置可以版本化管理,团队里其他人clone下来一条命令就能起环境。

services: kafka: image: apache/kafka:3.9.0 container_name: kafka-kraft ports: - "9092:9092" environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://127.0.0.1:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" KAFKA_NUM_PARTITIONS: 3 KAFKA_HEAP_OPTS: "-Xmx512M -Xms512M" volumes: - kafka-data:/var/lib/kafka/data volumes: kafka-data:

启动:

docker compose up -d

有几个和docker run场景不同的点要注意:

  • compose里KAFKA_CONTROLLER_QUORUM_VOTERS我仍然写的是1@localhost:9093,而不是service名。原因上面说过:controller需要在容器内部连接自己的9093端口参与投票,localhost在容器网络命名空间内部可以正确解析。
  • 我把KAFKA_NUM_PARTITIONS=3加上了。这设置自动创建topic时的默认分区数,本地调试时比较方便。
  • KAFKA_AUTO_CREATE_TOPICS_ENABLE="true"用字符串true,是因为YAML布尔值在某些解析情况下会变成True这种写法,Kafka配置解析时可能会出错。这是个小坑,常常被忽略。

3.3 启动后的三项基础验证

跑起来之后别急着写代码,先做三个检查:

  1. 容器状态和端口。容器必须是runningdocker port kafka-kraft能输出9092/tcp -> 0.0.0.0:9092
  2. 日志里找启动标志。docker logs kafka-kraft 2>&1 | grep "Kafka Server started",确认出现了这条日志。
  3. 容器内的端口监听。进入容器执行docker exec kafka-kraft sh -c "netstat -lntp | grep -E '9092|9093'"或者ss -lntp,应该看到9092和9093都在监听。

如果上面三项都通过,说明一个不依赖ZooKeeper的单机Kafka已经跑起来了。

4. 真实场景实测:生产、消费与主题管理

4.1 容器内命令行工具完整测试

直接用容器自带工具做一轮完整测试。先进入容器:

docker exec -it kafka-kraft /bin/bash

Kafka的命令行工具都在/opt/kafka/bin目录下。

创建topic:

/opt/kafka/bin/kafka-topics.sh \ --create \ --topic demo-topic \ --partitions 1 \ --replication-factor 1 \ --bootstrap-server localhost:9092

列出topic:

/opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092

然后开一个终端跑消费者:

/opt/kafka/bin/kafka-console-consumer.sh \ --topic demo-topic \ --from-beginning \ --bootstrap-server localhost:9092

再开一个终端跑生产者:

/opt/kafka/bin/kafka-console-producer.sh \ --topic demo-topic \ --bootstrap-server localhost:9092

输入几行文本,比如hello kraftdummy message,消费者终端里应该马上能收到。这一步验证的是broker内部的生产消费链路是否正常。如果通了,说明基本的消息通道没问题,Kafka的核心功能已经可用。

4.2 从宿主机连接:验证advertised.listeners配置

容器内没问题,不代表宿主机也没问题。想要从宿主机直接连接,验证的是端口映射和advertised.listeners是否匹配。

如果你宿主机装了kcat,可以这样快速验证:

echo "hello from host" | kcat -P -b localhost:9092 -t demo-topic kcat -C -b localhost:9092 -t demo-topic -o beginning

没有kcat也可以用Python,装个confluent-kafka或者kafka-python。以kafka-python为例:

from kafka import KafkaProducer, KafkaConsumer # 生产 producer = KafkaProducer(bootstrap_servers='localhost:9092') producer.send('demo-topic', b'hello from host') producer.flush() # 消费 consumer = KafkaConsumer( 'demo-topic', bootstrap_servers='localhost:9092', auto_offset_reset='earliest' ) msg = next(iter(consumer)) print(msg.value.decode())

能连续打出消息,就可以确认宿主机到容器、客户端到broker的连接是通的。这个测试同时也是对advertised.listeners=127.0.0.1:9092这一配置的直接验证。

4.3 如何确认当前真的工作在KRaft模式

一个很直接的证据是检查数据目录:

docker exec kafka-kraft sh -c "ls /var/lib/kafka/data"

如果当前是KRaft模式,你会看到类似__cluster_metadata-0的目录,这是存储集群元数据的内部主题;旧ZooKeeper模式下不会出现这个目录。

另一个证据是端口:确认2181端口没有被监听。ZooKeeper默认端口2181应该完全没有进程。你也可以在容器日志里搜索ZooKeeper相关字样,KRaft模式下不会出现ZooKeeper连接信息。

还可以用kafka-metadata.sh查看元数据。不过对一般验证来说,__cluster_metadata-0目录加2181无监听,已经足够直接了。如果你想更严谨,可以启动时候观察日志里出现的KafkaRaftServer nodeId=1字样,这个类名也直接指向KRaft实现。

5. 踩坑记录:单机KRaft部署中我遇到的5个问题

5.1 advertised.listeners配置错误,宿主机始终连不上

我在第一次部署时就栽在这里。容器内生产消费一切正常,但从宿主机用客户端连接就一直报Connection to node -1 could not be established. Broker may not be available.

排查思路可以分享一下:

  1. 先确认容器端口映射没有丢:docker port kafka-kraft
  2. 再进容器看listener是否正常监听:netstat -lntp | grep 9092
  3. 最后用kafka-log-dirs.sh --bootstrap-server localhost:9092等命令,看broker告诉客户端的连接地址到底是什么。

问题基本锁定在advertised.listeners。我当时写成了PLAINTEXT://kafka-kraft:9092,容器内部能通,但宿主机解析不了这个容器名。改成127.0.0.1:9092后立刻恢复。这个坑最隐蔽的点在于:客户端报错信息不一定直接说"address is not reachable",反而经常是超时或broker不可用,容易让人误判为网络问题。

5.2 小内存机器上直接被OOM杀掉

Docker Desktop在Windows/Mac上跑,宿主机可用内存本身就紧张。Kafka默认堆内存配置在某些镜像里会到1GB以上,我在一台8GB的笔记本上经常遇到容器启动后过几分钟就消失,docker logs一看就是OutOfMemory。

处理方式是在启动参数里显式限制堆内存:

-e KAFKA_HEAP_OPTS="-Xmx512M -Xms512M"

512M对单机测试的Kafka完全够用。如果你只是跑通流程,256M也能勉强撑住,但我不建议压得太低,Kafka内部很多后台线程和数据缓存需要内存。这个问题在Docker Desktop上尤其明显,因为宿主机内存还要分给虚拟机和本机系统。

5.3 重新部署时集群ID不匹配,容器一直重启

局部开发时,我经常删掉docker-compose里的容器再重建。某次重建后日志直接报类似:

The cluster id [xxxxxxxxxxxxxxxx] doesn't match the cluster id of the existing data [yyyyyyyy]

原因是:我删了容器,但没有清掉命名卷里的旧数据。第二次启动时,镜像检测到存储目录非空,于是沿用了旧数据里的cluster ID;而新容器在自动格式化时可能生成了新的cluster ID,两边一对比就崩了。

排查步骤:

  1. 查看宿主机挂载目录下的meta.properties,里面记录了cluster.id
  2. 查看容器日志里生成的cluster ID。
  3. 对比两个值是否一致。

解决办法很简单,如果数据不需要保留,执行docker compose down -v把卷一起删掉,再重新启动。如果要保留数据,就不要清卷,每次启动的配置保持一致即可。

5.4 官方镜像自动格式化不生效,需要手动kafka-storage

大部分情况下,apache/kafka官方镜像启动时检测到KRaft模式会帮我们格式化存储目录。但我也遇到过一种情况:手动提前创建了挂载目录,并且在目录里放了文件,导致镜像认为目录非空、没有执行format,于是启动失败,日志提示存储目录里找不到meta.properties

此时手动执行format即可:

docker exec kafka-kraft \ /opt/kafka/bin/kafka-storage.sh format \ -t $(docker exec kafka-kraft /opt/kafka/bin/kafka-storage.sh random-uuid) \ -c /etc/kafka/server.properties

这个命令的含义是:用random-uuid生成一个新的集群ID,然后按镜像生成的server.properties配置格式化存储目录。执行完成后重启容器:

docker restart kafka-kraft

其实更省事的做法是在compose环境变量里塞一个固定的KAFKA_CLUSTER_ID(部分镜像支持),或者手动在数据目录里维护一份meta.properties,不过对单机调试来说,手动format一次就能解决问题。

5.5 Docker Desktop的网络转发与文件挂载坑

如果你在Windows或macOS上用Docker Desktop,还有两个容易忽略的问题。

第一个是网络转发。Docker Desktop通常会帮你把容器端口转发到宿主机的localhost,所以127.0.0.1:9092可以通。但如果你在WSL2后端下遇到localhost能通而127.0.0.1有时不通的情况,先检查Docker Desktop的端口映射,再检查WSL和Windows之间的网络转发规则,不要把问题直接归到Kafka配置上。

第二个是文件挂载性能。Docker Desktop在跨文件系统做bind mount时IO性能损失比较明显。Kafka的日志目录是高频写入目录,如果挂载到宿主机目录,本地测试时可能明显感觉吞吐上不去。只是验证功能没问题,但如果要跑压测,建议把volume挂载调整到Docker虚拟机内部,或者直接用命名卷。

在这两类平台上做Kafka开发,我的建议是:功能验证用Docker Desktop没问题,性能相关测试还是尽早换Linux环境,或者用命名卷减少文件系统转发开销。

最后补充一点个人感受:KRaft模式把Kafka的部署门槛拉低了一大截,尤其是在单机开发和CI场景,一个容器就能完整运行,再也不用维护两套服务的编排。对于想学Kafka内部原理的朋友,我建议在跑通这套单机环境之后,再抽时间把controller和broker拆到两个容器部署一遍,看元数据复制的过程,这样对Raft协议和KRaft的理解会更立体。

上文所有命令我都按3.9.0版本实际跑过,如果你的版本不同,注意看一下镜像是否已经支持KRaft,以及环境变量的命名是否有变化。限于篇幅,今天先讲到这里,后面有机会我再展开聊聊多节点集群、SASL认证和存储调优这些进阶内容。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询