ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker Compose多主机部署Kafka与ZooKeeper集群完整指南

Docker Compose多主机部署Kafka与ZooKeeper集群完整指南

说实话,docker-compose部署zookeeper集群和kafka集群这个标题我在网上见过不下十遍,但绝大多数教程都停留在“用docker-compose up -d把三个节点拉起来”这个层面,完全没回答多主机部署真正要命的那些问题:为什么单机模拟一切正常,换到三台机器就各种连接被拒?为什么leader选不出来?为什么producer能发消息,消费者却拉不到数据?

这篇文章不是从零教docker-compose基础语法,而是聚焦“多主机”这个关键词。我会直接给出一套经过验证的三节点部署方案,包括完整的compose配置、环境变量写法、验证步骤,以及我在实际部署中遇到的六个故障和排查思路。如果你已经会用compose跑单机服务,想在多台机器上搭一套能支撑开发或小规模生产验证的消息中间件环境,这篇文章就是给你准备的。

1. 多主机部署真正难在哪:容器网络与主机名认知是第一道坎

1.1 单机docker-compose能跑,不代表多主机能跑

很多人第一次搭Kafka集群,是在一台机器上用三个compose服务模拟的,比如zk1、zk2、zk3,kafka1、kafka2、kafka3,所有服务往同一个docker network里一丢,互相用服务名解析,跑得顺风顺水。然后信心满满地把同样的配置搬到三台服务器上,改一改IP,起服务,结果客户端一连接就超时,ZooKeeper内部选举也一直起不来。

问题出在哪?单机compose环境里,容器的网络是同一张bridge网络,Docker内置的DNS能解析服务名。但多主机部署时,每台机器上的容器属于各自的网络,容器名和compose服务名完全无法被其他机器解析。这就是多主机部署与单机模拟的第一个本质区别:跨主机的容器通信,唯一可靠的方式是“宿主机IP+映射端口”,而不是容器名或服务名。

1.2 集群通信的三个关键链路

在动手写配置之前,先把Kafka+ZooKeeper集群的数据流拆清楚。整个集群运行时有三条独立的通信链路:

  • 客户端到Kafka:客户端通过bootstrap-server连接Kafka broker,默认端口9092。
  • Kafka到ZooKeeper:Kafka broker需要向ZooKeeper注册自身信息、读写元数据(topic、分区、副本、ISR列表、消费者offset等),默认端口2181。
  • ZooKeeper内部通信:ZooKeeper集群节点之间需要相互通信,包括数据同步端口2888和选举投票端口3888。

这三条链路,任何一条配置错误,表现出的故障现象都不相同。客户端连不上Kafka,往往不是Kafka本身的问题,而是advertised.listeners告诉客户端的地址不对;Kafka连不上ZooKeeper,一般就是zookeeper.connect写成了容器名;ZooKeeper选不出Leader,通常是server列表里的通信端口没映射好,或者防火墙把2888/3888挡住了。

我见过很多排错一整晚都找不到原因的情况,最后发现是三张独立的问题叠加在一起:客户端侧看9092,Kafka侧看2181,ZK侧看2888/3888。所以先明确这个链路模型,后面写配置和排查事故时就不会乱。

2. 拓扑规划与端口清单:先定死IP和端口,再写配置

2.1 三节点拓扑与端口映射

多主机部署的第一件事不是写配置文件,而是把IP、端口、数据目录全部规划清楚并写下来。我用三台机器演示,以下是一份完整的规划表,后续所有配置都围绕这张表展开。

节点宿主机IPZooKeeper运行Kafka运行主动端口映射
节点A192.168.1.10zk1kafka12181/2888/3888,9092
节点B192.168.1.11zk2kafka22181/2888/3888,9092
节点C192.168.1.12zk3kafka32181/2888/3888,9092

这张表意味着:每台机器上跑一个ZK节点和一个Kafka broker。生产环境不推荐把ZK和Kafka放在同一台宿主机上,因为两者都是资源消耗型进程,且同机部署时宿主机故障会让两者同时不可用。但作为多主机方案的首个实践版本,这样最节省机器,也最容易验证整个网络链路是否打通。

2.2 端口、防火墙与数据持久化约定

端口方面,ZK需要映射三个端口:2181是客户端连接端口,2888是follower与leader之间的数据同步端口,3888是leader选举投票端口。Kafka只需要映射9092,用于客户端生产消费。如果后续要开启JMX监控或SSL,再额外加端口。

一个需要注意的细节是:多主机部署时,ZK的server列表里写的是宿主机IP:2888:3888。ZK进程监听的是容器内部的2868/3888端口,消息到达宿主机IP后由Docker端口映射转发到容器内,这在工作原理上是可行的,也是多主机docker部署ZK的标准姿势。

数据目录要挂载到宿主机,而不是容器内部。ZK的数据目录对应容器内的/bitnami/zookeeper(如果用bitnami镜像),Kafka的数据目录对应/bitnami/kafka。注意每个节点必须使用独立的宿主机目录或独立的volume名,三台机器都叫同一个名字没问题,因为是不同宿主机;但同一台机器上的ZK和Kafka目录一旦重名或互相覆盖,数据就乱了。

防火墙方面,需要放行三台机器之间的以下端口:2181、2888、3888、9092。如果用了host网络模式,数据同步和选举端口还需要额外考虑监听地址。我在搭建时先在每台机器上执行了简单的端口监听测试:

# 在节点A上测试到B、C的ZK通信端口是否通 nc -vz 192.168.1.11 2888 nc -vz 192.168.1.11 3888 nc -vz 192.168.1.12 2181

如果nc命令不存在,可以改用bash自带的/dev/tcp测试:

timeout 2 bash -c "</dev/tcp/192.168.1.11/2888"

这一步花不了两分钟,但能提前暴露一大半网络问题。

3. ZooKeeper集群的compose文件与配置逻辑:myid与server列表缺一不可

3.1 镜像选型:bitnami为什么适合新手上路

ZooKeeper镜像的选择,市面上主要三个:官方zookeeper镜像、bitnami/zookeeper镜像、confluentinc/cp-zookeeper镜像。官方镜像的环境变量支持比较少,很多配置要靠挂载自定义zoo.cfg进去,对多主机场景不友好;confluent镜像和Kafka生态绑定比较紧,如果后面要切换到confluent的Kafka发行版才建议一起用。

我最终选择了bitnami/zookeeper,原因有两个:它用环境变量直接生成zoo.cfg,无需挂载配置文件;它的ZOO_SERVERS变量支持用分号分隔多节点列表,写起来非常直观。镜像体积比官方稍大,但对于部署的便捷性来说完全值得。

使用bitnami镜像需要在compose服务里额外设置ALLOW_ANONYMOUS_LOGIN=yes。这个环境变量的含义是允许匿名用户连接ZK,因为没有它的话,bitnami镜像默认会启用认证配置,启动时直接报错退出。开发环境可以这样设置,生产环境建议配置真正的用户认证。

3.2 三份compose配置的核心差异只有一个数字

下面是节点A的zookeeper compose配置,节点B和C的配置几乎完全一样,只需要改动三处:container_name、ZOO_SERVER_ID、volume名。这就是我推荐用环境变量代替挂载zoo.cfg的原因——不需要为每个节点维护一份文件。

services: zk1: image: bitnami/zookeeper:3.8 container_name: zk1 environment: - ZOO_SERVER_ID=1 - ZOO_SERVERS=server.1=192.168.1.10:2888:3888;server.2=192.168.1.11:2888:3888;server.3=192.168.1.12:2888:3888 - ZOO_TICK_TIME=2000 - ZOO_INIT_LIMIT=10 - ZOO_SYNC_LIMIT=5 - ALLOW_ANONYMOUS_LOGIN=yes ports: - "2181:2181" - "2888:2888" - "3888:3888" volumes: - zk_data1:/bitnami/zookeeper restart: unless-stopped volumes: zk_data1:

节点B的配置中,ZOO_SERVER_ID改成2,container_name改成zk2,volume名改成zk_data2,IP换成192.168.1.11;节点C同理。

关于ZOO_TICK_TIME、ZOO_INIT_LIMIT、ZOO_SYNC_LIMIT这三个参数,逐个解释一下实际含义。tickTime是ZK的最小时间单位,默认2000毫秒,代表一次心跳需要的时间。ZK的心跳机制、会话超时时间都以这个为基准。initLimit是follower在启动时能容忍多少个tickTime内未连上leader,10个tickTime也就是20秒。如果服务器之间的网络延迟比较大,这个值建议调大。syncLimit是follower与leader之间正常通信后,多少个tickTime内没有收到leader心跳就判定断连,5个tickTime即10秒。

我之前在一个跨机房场景里,网络延迟约50毫秒,用默认的initLimit完全没问题。但如果你的三台机器在物理距离较远的局域网、或者中间有比较多的网络设备,可以把initLimit调到15~20,避免启动时follower还没连接上就被判定超时。

3.3 myid为什么要从1开始编号且全局唯一

ZooKeeper之所以用myid来区分节点,是因为它直接对应zoo.cfg里server.N这个N。节点A的myid是1,它在zoo.cfg里的身份就是server.1=192.168.1.10:2888:3888;节点B的myid是2,对应server.2。如果两个节点myid相同,就会在集群里产生身份冲突,导致部分节点认为自己是另一个节点,出现数据不一致、反复选主等诡异现象。

bitnami镜像用ZOO_SERVER_ID这个环境变量来生成myid文件,路径在数据目录。这个方案的额外好处是:升级compose文件时,只要不改变ZOO_SERVER_ID和挂载的数据卷,数据就不会丢,ZK的数据状态也会延续。

3.4 ZK集群的启动验证:看日志比看状态更重要

三台机器的compose文件都写好后,先启动ZK部分,不要急着动Kafka。执行:

docker-compose up -d

然后观察容器日志。ZK集群启动成功的标志是日志里出现“LEADING”或“FOLLOWING”关键字,分别代表该节点是leader或follower。如果日志反复出现“LOOKING”,说明节点还在寻找leader,此时不要急着下结论,给它十几秒时间完成选举。

一个更直观的验证方式是进入容器查看状态:

docker exec -it zk1 zkServer.sh status

输出内容会明确显示当前是leader还是follower,以及集群的runid。如果输出报错说无法连接,或者显示standalone(单机模式),说明集群配置没有真正生效,优先检查ZOO_SERVERS里的IP和端口是否写正确。

4. Kafka集群的compose编排与advertised.listeners的正确姿势

4.1 advertised.listeners到底是什么

Kafka集群的参数里,最容易让初学者栽跟头的就是advertised.listeners。这个参数的字面意思是“广播给外部客户端使用的监听地址”,白话讲就是:Kafka进程启动后,会主动告诉所有连接它的客户端“你之后要连我,请连这个地址”。

那么问题来了:Kafka进程在容器里监听某个端口,它能看到的网络环境是容器内部的。如果advertised.listeners被配置成容器名或容器IP,那么外部客户端(包括另一台机器上的producer、consumer)拿到这个地址后,试图连接时当然会失败——因为这个地址在外部网络里根本不存在。

单机docker-compose为什么经常能跑通?因为客户端就在宿主机上,而Kafka监听端口映射到宿主机后,客户端连localhost就能通。很多教程就把advertised.listeners设置成了localhost或127.0.0.1,这样单机演示没问题,一旦转移到多主机,立刻炸锅。

正确的做法是:advertised.listeners写成该broker所在宿主机上客户端可以访问到的IP和端口。在我这个三节点方案里,节点A的Kafka的advertised.listeners就是192.168.1.10:9092,节点B是192.168.1.11:9092,依此类推。

4.2 三节点的Kafka compose配置与内外网监听分离

下面给出节点A的Kafka compose配置。同样,节点B和C只需改KAFKA_BROKER_ID、KAFKA_ADVERTISED_LISTENERS、volume名和IP。

services: kafka1: image: bitnami/kafka:3.7 container_name: kafka1 environment: - KAFKA_CFG_BROKER_ID=1 - KAFKA_CFG_ZOOKEEPER_CONNECT=192.168.1.10:2181,192.168.1.11:2181,192.168.1.12:2181 - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://192.168.1.10:9092 - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true - ALLOW_PLAINTEXT_LISTENER=yes ports: - "9092:9092" volumes: - kafka_data1:/bitnami/kafka restart: unless-stopped volumes: kafka_data1:

这里有几个要点展开说。

KAFKA_CFG_LISTENERS配置的是Kafka进程实际监听的地址。我写的是PLAINTEXT://:9092,表示在容器内所有网卡上监听9092端口。KAFKA_CFG_ADVERTISED_LISTENERS写的是宿主机IP:9092,这是给客户端和其他broker使用的。KAFKA_CFG_INTER_BROKER_LISTENER_NAME的作用是告诉Kafka:broker之间相互通信时使用PLAINTEXT这个listener。这几个字段必须成对出现,只写一个会导致启动报错。

KAFKA_CFG_ZOOKEEPER_CONNECT写的是三台宿主机IP:2181的列表,逗号分隔。这里不要写容器名,因为Kafka进程不在ZK的compose网络里。broker会逐个尝试连接ZK集群,只要其中一台ZK可用就能完成初始化。

ALLOW_PLAINTEXT_LISTENER=yes也是bitnami镜像特有的要求,不设置的话,默认禁止明文监听。

关于不同版本bitnami镜像的变量前缀问题,需要特别提醒:较新版本的bitnami/kafka镜像已经不再支持KAFKA_CFG_前缀,改用KAFKA_前缀,比如KAFKA_BROKER_ID、KAFKA_ZOOKEEPER_CONNECT。我在用3.7版本时,KAFKA_CFG_前缀仍然有效,但这个变更很隐蔽,很多人在升级镜像后莫名其妙发现环境变量不生效、配置全丢。所以拉取镜像前,务必去镜像页确认当前版本的支持前缀。如果启动后Kafka日志里出现了默认端口9092以外的监听,或者advertised.listeners变成了容器内地址,大概率就是前缀问题。

还有一个进阶场景值得提一下:如果客户端不在这些宿主机局域网内,比如要通过NAT或公网访问,就需要配置第二组监听器,即内外网监听分离。内网listener用PLAINTEXT://:9092,外网listener用EXTERNAL://:19092并映射到宿主机的19092端口,然后通过KAFKA_LISTENER_SECURITY_PROTOCOL_MAP声明两套协议映射。这个方案在需要对外暴露Kafka时很有用,但对于本篇文章的三节点局域网方案,单组监听器就够了。

4.3 副本因子与offset存储话题的配置

Kafka集群建立后,有一个容易被忽略的细节:内置的_consumer_offsets和_transaction_state这两个topic的副本因子,默认值是1。如果broker只有一台可用,这些内置topic会丢失副本,导致客户端消费offset管理异常。建议在整个集群启动后,至少把internal topic的复制因子调大,或者在compose配置里显式声明:

KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=2 KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=2 KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1

注意,这两个参数需要在所有broker启动前设置,因为集群首次创建内置topic时就会应用这些设置。如果集群已经启动,再改环境变量也不会生效,只能手动修改topic的分区配置或者重建集群。

对于三节点集群,我会把offsets和事务topic的副本因子设为2,这样既能容忍一台broker故障,又不会给集群带来太大的存储压力。生产环境的数据topic一般会设置replication-factor为2或3,由创建topic时的参数决定。

5. 启动顺序、健康检查与三节点功能验证

5.1 先ZK后Kafka的顺序约束

整个集群的启动顺序是硬性的:先启动全部ZooKeeper节点,等quorum形成(多数节点存活,一般是2/3活着)后,再启动Kafka。原因是Kafka broker在启动时要连接到ZK来注册元数据、执行controller选举。如果ZK集群还没建立,Kafka会反复重试连接,虽然最终能恢复,但启动时间会拖得非常久,日志里全是连接失败的信息,很难判断到底是哪里出了问题。

我个人的操作习惯是分两批执行:先在三台机器上分别执行ZK的docker compose up,然后用zkServer.sh status逐台确认状态,全部输出LEADING或FOLLOWING后,再开始启动Kafka。

还有一个细节:三台机器的ZK不要同时启动。虽然理论上同时启动也能完成选举,但实际中同时启动会增大选举风暴的概率,日志里的报错也更多。稳妥的做法是启动第一台,等它进入LOOKING状态但暂时无法选出leader(因为只有一台),然后快速启动第二台和第三台,让它们在一个相对短的时间窗口内完成选举。实际体验下来,间隔几秒启动比同时启动更平滑。

5.2 用Kafka自带的命令行验证跨主机消息链路

ZK和Kafka全部启动后,验证就开始了。第一步是确认Kafka broker与ZK的连接状态:

docker exec -it kafka1 bash cd /opt/bitnami/kafka/bin ./kafka-broker-api-versions.sh --bootstrap-server 192.168.1.10:9092

如果这条命令能够列出API版本信息,说明客户端能连通Kafka;如果连接失败,则进入第六章的排错流程。

随后创建一个多副本topic来验证跨节点数据复制:

./kafka-topics.sh --create --topic demo-topic \ --partitions 3 \ --replication-factor 2 \ --bootstrap-server 192.168.1.10:9092

这里分区数设为3,副本因子设为2,意味着topic在三个broker上共分布6个副本。创建成功后可以用describe命令确认分区和副本分布情况:

./kafka-topics.sh --describe --topic demo-topic --bootstrap-server 192.168.1.10:9092

理想状态下,你会看到每个分区的Replicas包含两个不同的broker id,并且Leader分散在三台broker上。如果所有分区的Leader都在同一个broker上,说明其他broker的advertised.listeners可能配置有问题——客户端连上了节点A,但A把其他broker的信息广播给了客户端,客户端后续访问时连不上其他broker,就会导致性能瓶颈或数据读写异常。

消息收发验证,我习惯在节点A上启动producer,然后到节点B上启动consumer,因为这样能确保消息真正跨越了物理机:

# 节点A上执行 ./kafka-console-producer.sh --topic demo-topic --bootstrap-server 192.168.1.10:9092 # 节点B上执行 ./kafka-console-consumer.sh --topic demo-topic \ --from-beginning \ --bootstrap-server 192.168.1.12:9092

在producer里输入几条消息,如果consumer能在节点B上实时显示,那么整条链路就已经验证通过了:客户端到Kafka、Kafka到ZK、broker之间的数据复制与协调都正常工作。

5.3 健康检查与自动重启策略

compose文件的restart: unless-stopped策略,保证进程崩溃后被Docker自动拉起。但自动重启不等于集群自动恢复,特别是ZK节点,如果数据目录损坏或myid错乱,restart也救不了。

更可靠的做法是在compose里加上healthcheck。以ZK为例,可以使用四字命令ruok来检测节点状态:

healthcheck: test: ["CMD", "zkServer.sh", "status"] interval: 10s timeout: 5s retries: 5

Kafka的healthcheck相对麻烦,因为Java进程不像ZK那样有命令可以直接查看状态。我常用的方式是用bash脚本检测9092端口是否在接受连接:

healthcheck: test: ["CMD-SHELL", "nc -z localhost 9092"] interval: 15s timeout: 5s retries: 10

healthcheck本身不解决故障,但能让docker compose ps输出清晰的健康状态,配合监控脚本或看板能大幅缩短故障发现的时间。

6. 实际部署中遇到的六个故障与完整排查链路

以下是我在多次部署中真实遇到过的故障,每个都附带排查路径。排错最重要的不是背答案,而是掌握“按链路逐层缩小范围”的方法。

6.1 故障一:Kafka客户端连接报“Connection refused”

这是最常见的故障,而且迷惑性很强,因为broker进程明明启动了,端口也在docker ps里映射了,但客户端就是连不上。

排查链路如下。先在客户端所在机器上测试端口通不通:

nc -vz 192.168.1.10 9092

如果端口不通,检查宿主机防火墙和云安全组是否放行9092。如果端口通,问题大概率在advertised.listeners。进入Kafka容器查看当前生效的配置:

docker exec -it kafka1 bash cat /opt/bitnami/kafka/config/server.properties | grep -E "listeners|advertised"

如果advertised.listeners显示的是localhost或容器IP,那就找到了元凶。修复方法很简单:把KAFKA_CFG_ADVERTISED_LISTENERS改成宿主机IP,重启broker。这里需要强调,advertised.listeners是“广播地址”,不是“监听地址”,我见过很多人把listeners和advertised.listeners搞混——listeners是Kafka进程实际监听的地址,advertised.listeners是告诉别人去哪里连接它。

6.2 故障二:ZK连不上,日志报“SessionTimeout”

Kafka启动时如果连不上ZK,会在日志里持续输出类似“SessionTimeout”或“Connection refused”的信息,而且broker会一直处于STARTING状态,无法对外服务。

排查链路是:先确认ZK的2181端口在三台机器上是否都能连通,然后检查ZK进程本身是否健康。我在实际部署中就遇到过ZK容器全部running、但三台机器之间的2888端口被安全组挡掉的情况。ZK进程自己是活的,2181能通,但Kafka连接后ZK无法转发数据,导致会话不断超时。

此时优先看ZK日志:

docker logs zk1 --tail 50

如果日志里出现“Cannot open channel to X at election address”,说明节点A无法连接节点B或C的3888端口,这就是选举通信链路被阻断的特征。修复方向是检查防火墙规则或调整server列表里的IP/端口。

6.3 故障三:ZK节点反复处于LOOKING状态

ZK集群启动后,日志在“LEADING”和“LOOKING”之间反复横跳,或者一直停在LOOKING,说明选举始终无法完成。这个故障排查起来比较费时,因为单纯看日志不容易定位到哪个节点出了问题。

我的排查步骤是先确认每个节点的myid是否唯一,一一比对我自己的规划表。然后确认zoo.cfg中的server.1、server.2、server.3与每台节点的ZOO_SERVER_ID一致。最后检查2888和3888端口的连通性。

这里说一个我踩过的坑:有一次我在节点的compose配置里把ZK的3888端口映射写成了“3888:3889”,容器内部监听3888,宿主机对外暴露3889,结果ZOO_SERVERS里写的还是3888。其他节点去连接3888端口,什么也连不上,选举就一直无法完成。端口映射的内外对应关系必须和配置文件里的端口一致,这是很多初学者忽视的细节。

6.4 故障四:broker启动成功但topic创建失败报“Broker not available”

broker进程正常运行,但创建topic时报错,提示某些副本无法分配。这种情况通常是broker之间的“内网不可达”。

具体表现是:bootstrap-server用节点A的IP连接成功,但topic创建时Kafka需要把副本同步到其他broker,而节点A把其他broker的地址广播给客户端后,客户端连接其他broker失败。排查方法是用kafka-broker-api-versions.sh分别用三个IP的9092端口测试,看哪个不通。如果单独连A通、单独连B不通,那问题大概率出在节点B的advertised.listeners。

6.5 故障五:bitnami镜像升级后环境变量失效

bitnami的Kafka镜像在某个版本后调整了环境变量的前缀体系。如果你照着老教程写了KAFKA_CFG_BROKER_ID,升级到新版本后启动的broker会以默认配置运行,broker.id变成-1,listeners变成随机的,集群直接混乱。

排查这个故障的关键在于:启动日志里如果出现“No configuration found”或随机端口,就要检查镜像版本和文档。我的建议是锁定镜像版本号,不要在compose文件里写bitnami/kafka:latest,直接用具体版本号,比如bitnami/kafka:3.7.0。这样至少能保证线上使用的配置和验证时保持一致,即使以后要升级,也是一次受控变更而不是排查谜题。

6.6 故障六:消费者能连上却收不到消息

这个故障的隐蔽性很高。客户端能消费,但消息就是不出现在消费者终端上。排查链路与网络无关,要先确认topic是否产生了消息,再确认消费者是否订阅了正确的topic。

有一次我发现producer的send回调显示成功,但consumer没消息。进入broker查看topic状态时发现,topic的Leader分区全部为-1,也就是leader副本全部不可用。原因是一个broker挂掉后,由于部分topic的副本因子设为1,剩下的broker无法接管leadership。解决方案是升级到副本因子为2的topic配置,并且确保每个topic的副本尽量分散在不同broker上。

正因为有这种场景,我强烈建议即使是最初的验证环境,也要在创建topic时指定replication-factor为2,别用默认的1。

6.7 故障排查思路小结:先链路后配置,先连通性后参数

最后把这六个故障的排查思路总结成一张表,方便你在遇到问题时快速对照。核心原则是先定位是网络链路问题还是配置参数问题,不要一上来就怀疑YAML写得不对。

现象优先怀疑链路检查命令常见根因
客户端连接Kafka被拒客户端到broker链路nc -vz IP 9092防火墙未放行/advertised.listeners错误
Kafka连接ZK超时Kafka到ZK链路nc -vz IP 2181ZK选举端口不通/zk地址写错
ZK一直LOOKINGZK节点内部链路docker logs zk13888未映射/myid重复
创建topic失败broker间链路分别用三个IP测9092advertised.listeners配置错误
broker启动配置诡异镜像配置体系检查镜像版本KAFKA_CFG_前缀失效
能连接但无消息副本与数据链路describe topic副本因子为1且leader失效

7. 多主机部署的适用边界与下一步可做的优化

在收尾之前,聊聊这套方案的边界。三节点ZK配合三节点Kafka,适合什么场景?适合开发环境、测试环境、预生产环境的性能验证,也适合中小规模的生产消息流转,比如日均消息量在百万级以内的业务。但如果你的业务对消息可靠性要求极高,或者单topic的吞吐要求非常高,还需要做更多优化,包括但不限于:

  • 将ZK与Kafka分离部署到不同的宿主机,避免同机故障互相影响。
  • 为Kafka配置单独的机器磁盘,使用NVMe或SSD,并合理安排日志分段策略。
  • 针对broker的JVM堆大小、page cache大小做专门的调优。
  • 接入监控系统,比如利用JMX导出器采集Kafka指标,再配合看板展示集群健康状态。
  • 考虑配置SASL/SSL认证,尤其是业务涉及敏感数据时,明文监听(PLAINTEXT)肯定不满足合规要求。

另外,这套compose方案本身也有一个隐含假设:三台机器的时间基本同步。Kafka和ZK的会话超时机制都对时钟漂移比较敏感,生产环境务必部署NTP时间同步服务,不然会出现莫名其妙的session expired。这个问题我在初学阶段吃了不少亏,后来在规划清单里加了一条“每台机器先同步时间再做部署”,再也没出现过相关故障。

还有一个很多人问过的点:这套配置能不能直接搬到Kafka Kraft模式下?不能。如果要用Kraft模式(就是去掉ZooKeeper的Kafka集群模式),broker之间需要一组单独的controller节点,配置方式完全不同,不能再沿用ZOO_SERVERS这一套。Kraft模式的部署以后单独写一篇。

最后分享一个我在维护这套环境时比较受用的习惯。我始终保留一份带日期的部署记录表,内容包括每台机器的IP、镜像版本、compose文件路径、环境变量前缀版本、已知异常备注。这个习惯帮助我在半年后重新审视环境时,不需要靠记忆恢复当时的配置上下文。维护集群这件事,看起来是技术活,实际上大部分时间花在把“当时为什么这么配”交代清楚上。一份好的记录,能让你半年后少走一晚上的弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进