Docker Compose多主机部署Kafka与Zookeeper集群实战 直接上结论docker-compose单机部署一整套 Kafka 环境非常简单很多人第一步都是这么干——一个 YAML 文件把 Zookeeper、Kafka 全塞进同一台机器端口映射出来测试完事。但一旦把同样的套路搬到多台服务器上你就会发现各种连不上、leader 选举失败、client 读写超时。这套东西坑非常多而且大多数坑不在 Kafka 本身而是在网络拓扑、hostname 解析、advertised.listeners 这些平时单机环境根本不会暴露的细节上。这篇文章我会把多主机跨物理机/虚拟机环境用 docker-compose 部署 Zookeeper 集群和 Kafka 集群的完整过程拆开讲包括架构设计、compose 文件编写、环境变量含义、启动验证顺序以及我在实际部署中踩过的坑和排查思路。适合已经会写单机 Kafka compose、但对多机集群还处于“大概知道要改 IP但不知道还要改什么”阶段的人。1. 部署前必须想清楚的四件事1.1 这不是“复制三份”那么简单单机 compose 里所有容器跑在同一个 docker bridge 网络容器之间通过服务名互相访问比如zookeeper:2181、kafka:9092Docker 自带的 DNS 自动完成解析。你根本不需要关心容器 IP 是多少、对外暴露什么端口反正面试官也不会问。多主机环境完全不同。三台机器上的容器不在同一个网络命名空间Docker 默认的网络模型在这里是行不通的。你要么选择 overlay 网络docker swarm 或手动创建 attachable overlay network要么选择最简单直接的host 网络模式让容器直接使用宿主机网络栈。我在这篇文章里推荐的是 host 模式。原因很简单Kafka 和 Zookeeper 对网络延迟和端口映射的敏感度很高host 模式没有 NAT 转发性能损耗最小而且端口、IP 全部透明排查问题的时候心理压力小很多。代价是你要自己管理端口冲突以及手动维护各节点之间的 hostname 解析。1.2 Kafka 与 Zookeeper 的版本关联部署之前先确认版本组合。旧版 Kafka 依赖 Zookeeper 做元数据存储、Controller 选举、broker 注册。虽然 Kafka 3.x 开始引入 KRaft 模式可以去掉 Zookeeper但生产环境里大量存量集群还是基于 Zookeeper 架构而且很多组件比如某些消息中间件、数据同步工具、历史运维脚本仍然假设 Zookeeper 存在。所以这篇内容完全没说老黄历反而是很多团队迁移 KRaft 之前必须掌握的基础功。版本选型上我建议用稳定组合Zookeeper:zookeeper:3.6.3或3.8.xKafka:apache/kafka:3.4.0或3.5.x注意apache/kafka:3.x官方镜像和旧的wurstmeister/kafka镜像环境变量不完全一致用官方镜像而不是某个第三方封装镜像主要原因是可维护性。第三方镜像停更多年、环境变量和安装路径不同出了问题你连文档都不好找。后面所有配置我都基于官方镜像来写。1.3 主机规划与域名解析是大前提为了让三台机器上的容器互相用 hostname 访问你需要在每台宿主机上把主机名写到 hosts 文件里。这个操作如果不做后面 Kafka 的 broker 注册、ZK 的 quorum 通信全会出问题而且报错信息非常不直观。我习惯的规划是三台机器跑一个 ZK 节点加一个 Kafka broker一套 3 节点集群既能容错允许挂一台又不需要太多资源。当然你也可以把 ZK 和 Kafka 拆开部署到 6 台机器但那至少需要 6 台机器很多测试环境其实没这条件。ZK 单独 3 台 Kafka 单独 3 台更适合生产环境本文方案重点在讲清原理你完全可以根据资源调整。1.4 数据持久化不能全依赖容器层容器是一次性的这不用我说你也知道。但很多人部署集群时图省事不给 ZK 和 Kafka 挂数据卷结果容器重启一下元数据直接丢了Zookeeper 里的事务日志没了Kafka 里的消息全没了。你哪怕是在做测试也应该挂 volume。数据目录最好是放在宿主机独立磁盘或远程存储上配置层面我下面会给出卷映射方式。生产环境建议用 NFS 或多副本存储但那是基础设施的事不是这篇文章的重点。2. 整体架构设计与节点规划2.1 推荐的三节点拓扑我这次部署的规划如下宿主机器hostname 对应名称角色内网 IP机器 Azk1 / kafka1Zookeeper Kafka192.168.1.11机器 Bzk2 / kafka2Zookeeper Kafka192.168.1.12机器 Czk3 / kafka3Zookeeper Kafka192.168.1.13注意这里zk1、kafka1不是宿主机 hostname而是将来容器启动时指定的 hostname。因为 Kafka 和 ZK 都要监听外网advertised 地址会用到它。每台机器的 hosts 文件都要加上三行192.168.1.11 zk1 kafka1 192.168.1.12 zk2 kafka2 192.168.1.13 zk3 kafka3为什么要写两个名字因为 ZK 集群内部通信用的是zk1、zk2、zk3Kafka broker 之间以及客户端访问用的是kafka1、kafka2、kafka3。虽然在这里它们指向同一台机器但职责不同分开命名方便你后续某一天把 Kafka 横向扩展成独立节点而不用大改。2.2 端口规划host 模式下容器的监听端口就是宿主机的监听端口所以端口不能冲突。需要开放的端口如下组件端口用途Zookeeper client2181客户端连接 ZKZookeeper 选举通信2888follower 与 leader 通信Zookeeper 投票端口3888leader 选举投票Kafka client9092客户端生产/消费本质上只需要开放 9092 和 2181 给外部客户端2888 和 3888 只需要在三台机器之间互通。如果中间有防火墙自己心里要有数。2.3 关于“到底是 Kafka 连 ZK还是客户端先连 ZK”这是一个常见混淆点。客户端连的是 Kafka 的 9092不直接连 ZK除了老式 admin 工具偶尔会直接查 ZK。Kafka broker 自己启动时会连接 ZK 完成注册和元数据同步。所以你在 compose 里配置 ZK 地址是给 Kafka broker 用的不是给外部客户端用的。3. compose 文件编写与逐项拆解3.1 目录结构建议每台机器上使用相同的目录结构方便维护/opt/cluster/ ├── zk/ │ ├── data/ # ZK 数据目录 │ └── datalog/ # ZK 事务日志目录 ├── kafka/ │ └── data/ # Kafka 日志目录 └── docker-compose.yml三台机器上的 compose 文件除节点编号、hostname 之外其余内容完全一致。3.2 Zookeeper 的 compose 配置以机器 A 为例zk服务的配置应该是这样的services: zk: image: zookeeper:3.8.1 container_name: zk hostname: zk1 restart: unless-stopped network_mode: host environment: ZOO_MY_ID: 1 ZOO_SERVERS: server.1zk1:2888:3888;2181 server.2zk2:2888:3888;2181 server.3zk3:2888:3888;2181 ZOO_4LW_COMMANDS_WHITELIST: * volumes: - /opt/cluster/zk/data:/data - /opt/cluster/zk/datalog:/datalog机器 B 只需要把ZOO_MY_ID改成 2hostname改成zk2机器 C 同理。这里有几个必须解释清楚的地方ZOO_SERVERS的格式空格分隔多个server.Xhost:2888:3888;2181host必须能解析成 IP。如果解析不了ZK 集群之间连不上日志会一直刷Cannot open channel to X at election address。ZOO_MY_ID为什么不能乱ZK 通过 myid 区分每个节点三台机器的 myid 必须分别是 1、2、3不能重复。写错了会出现两个节点认为自己是同一个成员日志疯狂报错。ZOO_4LW_COMMANDS_WHITELIST: *这个是给后续用zkCli.sh或运维监控四字命令ruok、mntr准备的。不开白名单echo ruok | nc localhost 2181会直接失败。3.3 Kafka 的 compose 配置Kafka 服务配置如下机器 Akafka: image: apache/kafka:3.4.0 container_name: kafka hostname: kafka1 restart: unless-stopped network_mode: host environment: KAFKA_BROKER_ID: 1 KAFKA_LISTENERS: PLAINTEXT://:9092 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:9092 KAFKA_ZOOKEEPER_CONNECT: zk1:2181,zk2:2181,zk3:2181 KAFKA_NUM_PARTITIONS: 3 KAFKA_DEFAULT_REPLICATION_FACTOR: 2 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 2 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 2 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 2 KAFKA_MIN_INSYNC_REPLICAS: 2 KAFKA_LOG_DIRS: /var/lib/kafka/data volumes: - /opt/cluster/kafka/data:/var/lib/kafka/data机器 B 改KAFKA_BROKER_ID: 2、hostname 为kafka2机器 C 改成 3。3.4 最重要的一行advertised.listeners如果你的集群只需要接收本机 localhost 请求那KAFKA_ADVERTISED_LISTENERS不配也没事。但只要你希望从其他机器无论客户端还是其他 broker访问这一行就决定生死。这里面的原理是listenersbroker 监听哪个网卡和端口。advertised.listenersbroker 对外宣告的地址客户端拿到这个地址之后才会去连接。单机部署时容器里经常被设成PLAINTEXT://localhost:9092你在本机测试完全正常。一旦换到多机环境别的机器按 localhost 来找 Kafka结果找到自己机器上去了当然连不上。所以必须把 advertised 地址写成其他机器能解析到的主机名或 IP。注意一个细节advertised.listeners里面写的是kafka1:9092不是192.168.1.11:9092。我在前面要求 hosts 文件里做了解析所以这里用主机名完全没问题。如果直接写 IP 也可以但一旦机器迁移或换网段配置就要全部跟着改主机名的可维护性好很多。3.5 镜像环境变量的命名习惯apache/kafka官方镜像内部会把KAFKA_前缀的环境变量自动转换成配置文件里的同名属性。所以KAFKA_BROKER_ID对应broker.idKAFKA_ADVERTISED_LISTENERS对应advertised.listenersKAFKA_ZOOKEEPER_CONNECT对应zookeeper.connect如果你看到网上有些文章用KAFKA_BROKER_ID1 BROKER_ID_COMMAND或者需要写-e KAFKA_offset_topic_replication_factor之类那多半是旧版第三方镜像的写法。官方镜像直接驼峰写就好省心很多。4. 启动顺序、验证流程与常见操作4.1 启动顺序先 ZK后 Kafka这个顺序是必须遵守的。Kafka broker 启动时要向 ZK 注册如果 ZK 还没就绪Kafka 启动会反复重试日志里报SessionExpiredException或连接超时虽然不一定崩但会让你误以为配置出问题。启动动作如下# 在三台机器上都执行先启动 zk docker compose up -d zk # 等 20 秒让 zk 完成 leader 选举 # 再启动 kafka docker compose up -d kafka怎么确认 ZK 集群起来了在任意一台机器上执行for i in 1 2 3; do echo server.${i}: $(echo mntr | nc zk${i} 2181) done如果输出里有zk_server_state leader或follower说明集群已经选主完成。如果三个节点全部显示standalone说明三台机器之间没连起来问题大概率出在 hosts 解析或防火墙。当然前提是你已经配置了ZOO_4LW_COMMANDS_WHITELIST: *否则四字命令会被拒绝。4.2 验证 Kafka broker 是否注册ZK 起来后检查 Kafka 是否成功注册。连到任意一台 ZK执行docker exec -it zk zkCli.sh -server 127.0.0.1:2181 ls /brokers/ids正常会输出[1, 2, 3]。如果只有部分 id说明那台 broker 没起来或没注册成功去对应机器的 Kafka 日志看报错。注意看日志位置。官方镜像的日志默认输出到 stdoutdocker logs kafka就能看到。报错信息里如果出现Connection to node -1 could not be established. Broker may not be available.通常不是 broker 之间连不上而是客户端或测试工具能解析到的 hostname 不对这个后面专门讲。4.3 创建 topic 并测试读写用一个能访问任意一台机器的环境最好不是你部署集群的三台机器而是另一台客户端机器安装kafka_2.13-3.4.0.tgz或者直接使用 docker 镜像里的命令行工具。这里用官方镜像跑命令是最快的# 创建 3 分区 3 副本的 topic docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --create \ --topic test-multi-host \ --partitions 3 \ --replication-factor 3 \ --bootstrap-server kafka1:9092注意--bootstrap-server只需要写一个 broker 地址客户端会通过 metadata 请求拿到所有 broker 的地址列表。但实际连接时它会依据每个 broker 的 advertised.listeners 地址去连所以你必须保证客户端机器能解析并访问kafka1、kafka2、kafka3这 3 个名字对应的 IP。验证 topic 状态docker exec -it kafka /opt/kafka/bin/kafka-topics.sh --describe \ --topic test-multi-host \ --bootstrap-server kafka1:9092理想情况是看到每个 partition 有 3 个副本且所有副本都处于Isr状态在 ISR 列表里。只要有一个分区显示Replicas: 1,2之类只有两个副本说明第三台机器的 broker 不可用或没注册上。生产消费验证我一般这样测# 终端 1消费 docker exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \ --topic test-multi-host \ --bootstrap-server kafka2:9092 \ --from-beginning # 终端 2生产 docker exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \ --topic test-multi-host \ --bootstrap-server kafka3:9092如果能正常收发客户端链路就通了。先把这条链路走通再谈生产环境优化。4.4 日常重启与滚动升级多主机集群维护时不要三台机器一起重启。推荐滚动方式先重启一台确认 ZK 重新选主正常、Kafka broker 重新注册、ISR 收缩又恢复之后再操作下一台。Kafka 支持滚动重启是因为分区副本在其它 broker 上有同步副本短暂下线会影响延迟但不会丢数据前提是min.insync.replicas设置合理。ZK 因为集群本身有 3 节点允许挂一个节点完成选主。5. 我踩过的几个坑与排查思路5.1 broker 启动超慢日志里一直刷 ZK 连接超时这个问题我出现过好多次。先说结论90% 是KAFKA_ZOOKEEPER_CONNECT里写的 hostname 解析不了。Kafka 容器使用 host 网络模式时它会在宿主机的/etc/hosts里查找zk1、zk2、zk3。如果你只在其中一台机器写了 hosts其他机器没写那么那台机器上的 Kafka 就会连不上其它机器上的 ZK。排查命令很简单docker exec -it kafka getent hosts zk1 docker exec -it kafka getent hosts zk2 docker exec -it kafka getent hosts zk3任何一个失败就说明 hosts 没配对。别再浪费时间看 Kafka 日志了先解决解析问题。5.2 客户端能连上 ZK但生产/消费时一直报 timeout典型报错org.apache.kafka.common.errors.TimeoutException: Topic test-multi-host not present in metadata after 60000 ms.或者Connection to node -1 could not be established. Broker may not be available.这不是 broker 挂了而是你的客户端拿到了 broker 的 advertised 地址但无法连接。最常见的原因是你把KAFKA_ADVERTISED_LISTENERS配成了PLAINTEXT://localhost:9092或者写成了容器内网 IP比如172.17.0.2。客户端解析后连接了一个它自己机器上的不存在的端口自然失败。正确写法是PLAINTEXT://kafka1:9092对应 kafka2、kafka3并确保客户端机器也能解析这三个 hostname。如果客户端机器不在同一个 DNS 环境直接在客户端机器的 hosts 文件里加同样的映射也行。5.3 ZK 三节点互相看不到用mntr全是 standalone别看程序单机要看节点间通信。三台机器全部显示 standalone 说明它们之间就没有建立 quorum 连接。检查思路按顺序走三台机器的/etc/hosts是否都写全了映射。三台机器防火墙是否对 2888、3888 端口放行。看 ZK 日志搜Cannot open channel to里面会显示它尝试连接哪个 IP、失败原因。确认ZOO_SERVERS里写的 host 不是localhost。这里有镜像默认值问题官方镜像默认ZOO_SERVERS指向zookeeper:2888:3888如果你没配这个环境变量它默认连接一个叫zookeeper的 host当然找不到。5.4 host 模式下容器内的 hostname 为什么老变有些版本下即使你在 compose 里指定了hostname: kafka1容器内/etc/hosts依然会附加容器 ID 和宿主机 hostname 的映射。这不影响主要功能只要 Kafka 进程解析PLAINTEXT://:9092时监听的是0.0.0.0就行。但有个隐性问题如果宿主机名也叫kafka1然后你又在另一台机器上把它的宿主机名也改成了kafka1那 hosts 解析会重叠出现逻辑混乱。所以我再三建议部署这种多机容器集群之前把宿主机命名为中性的名字不要和容器 hostname、service 名混用。例如宿主机器分别叫node1、node2、node3容器内部 hostname 才用集群角色名。5.5 Kafka 的offsets内部 topic 副本数不设置会怎样如果KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR不设单机默认就是 1你三节点的集群里__consumer_offsets只有 1 个副本。一旦存放 offset 的那个 broker 挂了整个消费组都会出问题。我习惯在三台机器的 compose 里统一设成 2 或 3。同理transaction.state.log.replication.factor也建议设置不然使用事务 API 时会出现NotEnoughReplicasException。这里有个小技巧这些参数虽然是 compose 里靠环境变量设置的但只能在broker 首次启动创建内部 topic 时生效。如果第一次启动没设置内部 topic 已经创建了你再改环境变量重启也不会重建需要手动删除内部 topic 并重启或者用kafka-configs.sh去改 topic 级别配置。所以我建议第一次启动的时候就全部配好省得后面还要做手术。5.6 误将单机 compose 的 network 配置复制到多机有些开发者在单机的时候上了自定义 bridge network并且给 Kafka 配了固定 IP比如 static IPs到多机部署时就想继续沿用。声明确实能配置但跨主机的 bridge 网络默认是不互通的要么你搭 overlay要么老老实实用 host。折腾 overlay 对测试环境意义不大我还是那句话能用 host 就 host。6. 生产环境增强建议6.1 增加健康检查compose 的healthcheck在容器编排场景里非常有用。ZK 和 Kafka 都可以通过命令检查存活状态。ZK 加healthcheck: test: [CMD, zkServer.sh, status] interval: 10s timeout: 5s retries: 3注意 ZK 容器里zkServer.sh status在没有写4lw.commands.whitelist时会失败所以前面提到的白名单一定不能漏。Kafka 健康检查稍微麻烦一点因为它没有直接暴露 health 端点通常用kafka-broker-api-versions.sh拉一下 broker 版本号来判断healthcheck: test: [CMD, /opt/kafka/bin/kafka-broker-api-versions.sh, --bootstrap-server, kafka1:9092] interval: 15s timeout: 10s retries: 5这个健康检查适合 compose 管理场景真实生产建议用更完善的监控接入有条件的直接上 JMX exporter Prometheus。6.2 资源限制Kafka 是吃内存大户尤其 page cache 用得狠。compose 里建议限制deploy: resources: limits: memory: 4g reservations: memory: 1gZK 相对轻量1g 限制足够。不过docker compose的deploy字段需要 compose 文件版本 3 以上才支持。没有跨主机的编排需求时可以加加了也就在单机维度限制容器内存至少比不管要好。6.3 存储选择Zookeeper 和 Kafka 都是极端依赖磁盘顺序读写的组件。Kafka 的 log 段文件如果落在普通磁盘上吞吐和延迟都会非常难看。建议Kafka 数据目录放到 SSD或者至少独立于系统盘。Zookeeper 的 data 和 datalog 目录也放到独立磁盘数据量虽小但事务日志的 fsync 性能直接影响整个集群稳定性。真实生产环境必须做数据备份和灾备这里容器化部署并不能帮你免掉这些功课。我自己测试时遇到过一个问题数据卷挂载的是机械硬盘Kafka 消费延迟忽高忽低ISR 频繁收缩又恢复。当时排查了半天的网络问题最后才发现是磁盘 IO 跟不上。所以如果你是做性能相关的压测先把磁盘性能基准测一下再上集群。7. 实际操作中我最想留给你的一句话如果你问我在多次多机部署里最深的体会是什么那就是多主机 Kafka 集群 90% 的问题都不是 Kafka 本身而是网络语义没有理清。单机环境隔离了太多细节让很多人误以为 compose 部署集群只是多复制几段 YAML。实际上你要处理的是机器间解析、端口开放、广播地址选择、副本因子设置这些都是分布式系统的基础功课只是平时单机环境帮你隐藏了。真正把三台机器的 compose 都写好、启动一次、看到 topic 正常同步之后这套架构带来的收益会非常直接以后任何节点挂了集群依然可以对外提供服务重新拉起一台也不会有太多心智负担因为配置是声明式的一遍写对终身受益。最后再分享一个我个人的小习惯每次改完 compose 先跑一遍docker compose config确认 YAML 语法和环境变量没有问题再执行docker compose up -d。这个习惯帮我拦截了无数次因为 YAML 缩进或变量名拼写错误导致的无效启动。多主机环境里改一个文件要在三台机器上同步如果你不借助配置管理工具手动复制粘贴的时候更要小心漏改一个 broker.id 可能就是一次集群脑裂事故。