尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

使用Docker单机部署Kafka:KRaft模式完全替代ZooKeeper实操指南

使用Docker单机部署Kafka:KRaft模式完全替代ZooKeeper实操指南 以前在本地折腾Kafka最烦的不是Kafka本身而是旁边那个ZooKeeper。明明只是想跑个消息队列做验证却要同时伺候两个 Java 进程版本要匹配、启动有先后稍微哪里不对就是一连串莫名其妙的报错。Kafka 从 3.3 开始引入的 KRaft 模式把 ZooKeeper 彻底换掉了——元数据由 Kafka 自己通过 Raft 协议管理不再需要独立的协调组件。单机场景下我们甚至可以把 controller 角色和 broker 角色放进同一个进程一个容器、一份配置就能跑起来。这篇文章我会把使用 Docker 单机部署 Kafka以 KRaft 模式运行、完全不依赖 ZooKeeper的完整过程整理出来包括镜像选型、监听器配置、生产消费验证以及我实际部署中踩过的几个坑适合想在本地快速搭一套 Kafka 做开发测试、或在 CI 里做集成验证的读者。1. KRaft模式到底解决了什么问题先弄明白为什么要换掉ZooKeeper1.1 ZooKeeper时代的三个痛点早期Kafka的架构中ZooKeeper扮演的角色是集群元数据存储和协调者broker注册、topic配置、分区副本分配、消费者组位移全都要靠ZooKeeper来记录和同步。听起来很合理但实际用起来有三类痛点。第一个痛点是部署复杂度。想跑Kafka得先单独搭ZooKeeper。哪怕只是本地单机测试也要下载两个包、起两个进程、配两套参数。容器化之后稍微好一点但依然要编排两个容器还要处理启动顺序——ZooKeeper必须先ReadyKafka再启动否则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.xZooKeeper模式已经被彻底移除了继续学老的部署方式路只会越走越窄。我用一个粗糙的类比来理解这件事以前公司里财务和审计分两个办公室审计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_KRAFTyes一套自己的配置体系反而不利于理解底层原理。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自己无法形成法定人数。这里最容易搞混的是listeners和advertised.listeners的区别。listeners是Kafka进程实际绑定的网卡和端口好比家里电话机的接口advertised.listeners是Kafka告诉客户端的对外连接地址好比印在名片上的电话号码。客户端一定拿着名片上的地址去找你。如果你的容器映射了宿主机端口但advertised里写的是容器内部地址外部客户端就无论如何连不上。所以我在下面的配置里使用KAFKA_LISTENERSPLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093让两个listener都绑定容器内所有网卡地址。KAFKA_ADVERTISED_LISTENERSPLAINTEXT://127.0.0.1:9092告诉客户端你在宿主机上连127.0.0.1:9092就能找到我。KAFKA_LISTENER_SECURITY_PROTOCOL_MAPCONTROLLER: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_ID1 \ -e KAFKA_PROCESS_ROLESbroker,controller \ -e KAFKA_CONTROLLER_QUORUM_VOTERS1localhost:9093 \ -e KAFKA_LISTENERSPLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 \ -e KAFKA_ADVERTISED_LISTENERSPLAINTEXT://127.0.0.1:9092 \ -e KAFKA_LISTENER_SECURITY_PROTOCOL_MAPCONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \ -e KAFKA_CONTROLLER_LISTENER_NAMESCONTROLLER \ -e KAFKA_INTER_BROKER_LISTENER_NAMEPLAINTEXT \ -e KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR1 \ -e KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR1 \ -e KAFKA_TRANSACTION_STATE_LOG_MIN_ISR1 \ -e KAFKA_AUTO_CREATE_TOPICS_ENABLEtrue \ -e KAFKA_HEAP_OPTS-Xmx512M -Xms512M \ -v kafka-kraft-data:/var/lib/kafka/data \ apache/kafka:3.9.0逐个说关键参数KAFKA_NODE_ID1当前节点的ID单机没有特殊要求但必须有一个唯一数字。KAFKA_PROCESS_ROLESbroker,controller这是KRaft组合模式的核心开关。如果两个角色分离部署这里就要分开写但单机合体就写这一行。KAFKA_CONTROLLER_QUORUM_VOTERS1localhost:9093controller quorum的投票者列表。格式是节点IDhost:端口。因为单机只有一个controller节点且监听地址是容器内的9093所以写1localhost:9093就可以了。这里有个容易忽略的点voters里的地址是Kafka内部Raft节点之间互相连接用的不用经过宿主机端口映射所以写localhost完全没问题。KAFKA_CONTROLLER_LISTENER_NAMESCONTROLLER告诉Kafka名为CONTROLLER的监听器是用于controller通信的。KAFKA_INTER_BROKER_LISTENER_NAMEPLAINTEXTbroker之间内部通信也走PLAINTEXT监听器。单机时可能无所谓但显式写出来可以避免多监听器场景下的歧义。KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR1Kafka在创建消费者位移内部主题__consumer_offsets时默认副本数是3单机只有一个broker必须改成1否则创建失败。KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR1和KAFKA_TRANSACTION_STATE_LOG_MIN_ISR1同理事务状态日志的副本和最小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: 1localhost: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我仍然写的是1localhost:9093而不是service名。原因上面说过controller需要在容器内部连接自己的9093端口参与投票localhost在容器网络命名空间内部可以正确解析。我把KAFKA_NUM_PARTITIONS3加上了。这设置自动创建topic时的默认分区数本地调试时比较方便。KAFKA_AUTO_CREATE_TOPICS_ENABLEtrue用字符串true是因为YAML布尔值在某些解析情况下会变成True这种写法Kafka配置解析时可能会出错。这是个小坑常常被忽略。3.3 启动后的三项基础验证跑起来之后别急着写代码先做三个检查容器状态和端口。容器必须是runningdocker port kafka-kraft能输出9092/tcp - 0.0.0.0:9092。日志里找启动标志。docker logs kafka-kraft 21 | grep Kafka Server started确认出现了这条日志。容器内的端口监听。进入容器执行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/bashKafka的命令行工具都在/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 kraft、dummy 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_serverslocalhost:9092) producer.send(demo-topic, bhello from host) producer.flush() # 消费 consumer KafkaConsumer( demo-topic, bootstrap_serverslocalhost:9092, auto_offset_resetearliest ) msg next(iter(consumer)) print(msg.value.decode())能连续打出消息就可以确认宿主机到容器、客户端到broker的连接是通的。这个测试同时也是对advertised.listeners127.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 nodeId1字样这个类名也直接指向KRaft实现。5. 踩坑记录单机KRaft部署中我遇到的5个问题5.1 advertised.listeners配置错误宿主机始终连不上我在第一次部署时就栽在这里。容器内生产消费一切正常但从宿主机用客户端连接就一直报Connection to node -1 could not be established. Broker may not be available.。排查思路可以分享一下先确认容器端口映射没有丢docker port kafka-kraft。再进容器看listener是否正常监听netstat -lntp | grep 9092。最后用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 -Xms512M512M对单机测试的Kafka完全够用。如果你只是跑通流程256M也能勉强撑住但我不建议压得太低Kafka内部很多后台线程和数据缓存需要内存。这个问题在Docker Desktop上尤其明显因为宿主机内存还要分给虚拟机和本机系统。5.3 重新部署时集群ID不匹配容器一直重启局部开发时我经常删掉docker-compose里的容器再重建。某次重建后日志直接报类似The cluster id [xxxxxxxxxxxxxxxx] doesnt match the cluster id of the existing data [yyyyyyyy]原因是我删了容器但没有清掉命名卷里的旧数据。第二次启动时镜像检测到存储目录非空于是沿用了旧数据里的cluster ID而新容器在自动格式化时可能生成了新的cluster ID两边一对比就崩了。排查步骤查看宿主机挂载目录下的meta.properties里面记录了cluster.id。查看容器日志里生成的cluster ID。对比两个值是否一致。解决办法很简单如果数据不需要保留执行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认证和存储调优这些进阶内容。
返回列表