Docker 部署 Kafka 排障实录:连接超时、健康检查误报、数据判定三连坑
一次用 Docker Compose 部署 Kafka + Zookeeper,再用 Offset Explorer 查看数据的完整排障经历。遇到的三个坑------连接超时 、Zookeeper 永远 unhealthy 、连上了却没数据------每一个都有明确根因和可复现的排查路径,整理成文供参考。
背景
在测试环境用 Docker Compose 拉起一套单节点 Kafka 集群:
- Kafka :
confluentinc/cp-kafka:7.6.1 - Zookeeper :
confluentinc/cp-zookeeper:7.6.1(7.6.1 内置 ZK 3.8,还是 ZK 模式,不是 KRaft) - 宿主机端口映射:Kafka
19092 → 9092,ZK15998 → 2181 - 客户端工具:Offset Explorer 2.3(Windows 桌面端)
业务侧有若干个业务 topic,期望由对端系统推送数据进来,我们用 Offset Explorer 观察数据是否到达。
需求很简单:连上 Kafka,看数据到没到。结果一路踩了三个坑。
坑一:Offset Explorer 连接超时
现象
配置好 bootstrap servers 后点连接,转圈很久后报错:
css
1Connection Error2Error connecting to the cluster. Timeout connecting to broker(s),3bootstrap.servers=192.168.1.100:19092
注意关键词 Timeout------TCP 层面就没建立起来(如果是端口通了但配置不对,通常会报 refused / invalid url,而不是 timeout)。
排查路径
- IP 对不对?
hostname -I确认客户端填的 IP 确实是这台服务器。 - 端口在不在监听?
ss -lntp | grep 19092确认 docker-proxy 正常监听,端口映射没掉。 - 防火墙?
firewall-cmd --list-ports、firewall-cmd --state确认 19092 是否放行。 - 最终定位在 advertised.listeners------这才是真正的根因。
根因:advertised.listeners 广播了 localhost
看 Kafka 容器的监听配置:
dart
1docker exec kafka bash -c 'echo $KAFKA_ADVERTISED_LISTENERS; echo $KAFKA_LISTENERS'
输出:
ruby
1ADVERTISED=PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:190922LISTENERS=PLAINTEXT://0.0.0.0:29092,PLAINTEXT_HOST://0.0.0.0:9092
问题就在 PLAINTEXT_HOST://localhost:19092。
Kafka 的连接机制是两步走:
- 客户端拿着
bootstrap.servers里的地址,先建立第一个 TCP 连接(bootstrap 阶段); - Broker 在元数据响应里返回
advertised.listeners中配置的地址,客户端丢弃 bootstrap 地址,改用广播地址去建立真正的工作连接。
所以 localhost:19092 一广播出去,客户端(Offset Explorer)就拿着它去连自己电脑上的 19092 端口------本机哪有服务,自然是超时/拒绝。
Docker 部署特别容易踩这个坑:容器内部用的是容器名(
kafka)+ 内部端口(29092),对外又是宿主机 IP + 映射端口(19092),一套配置里要同时描述两个网络视角,localhost这种"从哪看都对、从客户端看全错"的写法很容易混进去。
修复
把对外广播地址改成客户端真正可达的地址:
ruby
1kafka:2 environment:3 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://192.168.1.100:19092
PLAINTEXT://kafka:29092保留------容器间通信(生产/消费组件在同一个 docker 网络里)用这个;PLAINTEXT_HOST://<宿主机真实IP>:19092------外部客户端用这个。端口必须写宿主机映射端口 19092,不是容器内 9092。
改完 docker compose up -d 重建,Offset Explorer 一次连上 ✅。
一个口诀
listeners 是"我监听在哪",advertised.listeners 是"我告诉客户端去哪连"。 后者必须是客户端视角的地址,不是服务器视角的地址。
坑二:Zookeeper 永远 unhealthy
现象
连上 Kafka 后回头看 docker ps:
scss
1clodopt-kafka Up 21 minutes (healthy)2clodopt-zookeeper Up 26 minutes (unhealthy) ← 一直红
Zookeeper 从启动起就是 unhealthy,重启也没用。但诡异的是:Kafka 一直是 healthy。
先下个判断:这是误报
Kafka 是 ZK 模式,它的所有元数据(broker 注册、topic、offset)都存在 Zookeeper 里。Kafka 能 healthy,说明它连 ZK 完全正常、读写元数据都没问题------ZK 功能是好的 。这个 unhealthy 几乎肯定是健康检查本身的问题。
定位:看 ZK 日志
1docker logs clodopt-zookeeper
翻到启动日志末尾两行:
lua
1INFO The list of known four letter word commands is : [..., srvr, ruok, ...]2INFO The list of enabled four letter word commands is : [[srvr]]
看到 enabled = [[srvr]] 就明白了。
根因:ZK 3.6+ 默认只放行 srvr 四字命令
Zookeeper 从 3.6.0 起,出于安全考虑,四字命令(4 letter word)默认只启用 srvr 一个 ,其他(ruok、stat、mntr、conf 等)全部被白名单拦掉。白名单由 4lw.commands.whitelist 控制。
而我们 compose 里写的健康检查偏偏用了 ruok:
bash
1zookeeper:2 healthcheck:3 test: ["CMD-SHELL", "echo ruok | nc -w 2 localhost 2181 | grep -q imok"]
ruok 被白名单拒绝,手动执行都能看到明确回复:
python
1$ docker exec clodopt-zookeeper bash -c 'echo ruok | nc -w 2 localhost 2181'2ruok is not executed because it is not in the whitelist.
grep -q imok 永远失败 → 永远 unhealthy。
修复(推荐):healthcheck 改用默认放行的 srvr
与其放行所有四字命令(降低安全水位),不如让健康检查用 srvr------它是默认启用的:
bash
1zookeeper:2 healthcheck:3 test: ["CMD-SHELL", "echo srvr | nc -w 2 localhost 2181 | grep -q 'Zookeeper version'"]
改前手动验证这条命令能返回内容:
bash
1docker exec clodopt-zookeeper bash -c "echo srvr | nc -w 2 localhost 2181"2# 输出 Zookeeper version: 3.8.x-... 的 server stats 即代表可行
如果确实需要 ruok(比如自己写的脚本依赖它),也可以放行白名单,但更推荐前者:
bash
1environment:2 ZOOKEEPER_4LW_COMMANDS_WHITELIST: "*" # 或 ruok,srvr,stat 等按需
经验
容器显示
unhealthy时,先别急着认为服务挂了。结合依赖它的服务是否正常 (这里 Kafka healthy 反向证明了 ZK 正常)+ 健康检查命令本身 (四字命令白名单、命令是否存在于镜像内)两步判断,很多unhealthy是健康检查写得不对,不是服务坏了。
坑三:连上了,但所有 topic 都没有数据
现象
Offset Explorer 连上集群了,Brokers、Topics、Consumers 都出来了,但点进任何一个 topic 的 Data 页,全是 No Data 。Messages 切到 Oldest 也没有。
这时候要回答的问题变成:到底有没有数据进来?是 Kafka 的问题还是对端没推?
定位:用命令查所有 topic 的累计 offset
GUI 里翻不如命令来得直接。一条命令看全部 topic 的消息总数:
kotlin
1docker exec kafka kafka-run-class kafka.tools.GetOffsetShell \2 --broker-list localhost:9092 --time -1
小坑:
--all-topic-list是kafka-topics的选项,kafka-get-offsets不认识。不带--topic直接跑GetOffsetShell才是"列全部"的正确姿势。
输出格式 topic:partition:offset:
makefile
1topic_a:0:02topic_b:0:03topic_c:0:0
全部 offset 为 0 ,结论立刻清晰:从这些 topic 创建至今,Kafka 一条消息都没收到过。
判断依据(可靠):Kafka 消息默认保留 7 天,offset 是只增不减的累计值,没有消费者消费它也不会自己归零。只要推送成功过一次,offset 就不可能还是 0。所以:
offset 全 0 = 对端从未推送成功。 这是 Kafka 侧拿到的硬证据,比双方嘴上扯皮可靠得多。
给"对端没推"下结论前,排除两个边界
- 集群数据卷是否被重建/清空过? 如果 Kafka 容器的数据卷没挂载或重建时被清空,topic 和消息会重新初始化成空的,offset 归零就是假象。确认 compose 里挂了命名卷(如
kafka-data:/var/lib/kafka/data),即可排除。 - 对方对接的确实是这个集群 + 这些 topic 名吗? 他可能推到了别的 Kafka、或 topic 名跟你这边不一致。让对方贴出他们的
bootstrap.servers和 topic 名核对。
最有说服力的当场验证
与其来回确认,不如让对端现场推一条测试消息,本端实时监听:
javascript
1# 本端开一个实时监听窗口2docker exec kafka kafka-console-consumer \3 --bootstrap-server localhost:9092 --topic topic_a --from-beginning
对端推一条后:
- 监听窗口打印出内容 → 对端推送链路通,之前是没推/没推对;
- 毫无输出、再查 offset 还是 0 → 数据根本没送到这个集群,结论坐实。
可以直接发给对端的话术:
我们 Kafka 侧确认:
topic_a/topic_b/topic_c等 topic 目前消息数全部为 0。请核对你们的 bootstrap 地址和 topic 名称,并抽一条 topic 现场推送一条测试数据,我们这边实时验证。
总结:Docker 部署 Kafka 的自查清单
把这三次的坑收敛成一份清单,下次部署+联调直接照着查:
| # | 检查项 | 命令 | 要点 |
|---|-------------------|----------------------------------------------------------------|------------------------------------------------------|------------------|
| 1 | 端口监听 | `ss -lntp | grep <映射端口>` | docker-proxy 在监听 |
| 2 | 防火墙 | firewall-cmd --state && firewall-cmd --list-ports | 映射端口放行(云上还要看安全组) |
| 3 | advertised 地址 | docker exec kafka bash -c 'echo $KAFKA_ADVERTISED_LISTENERS' | 外部 listener 必须写客户端可达的 IP+映射端口,绝不能是 localhost/容器名 |
| 4 | 服务健康 | docker ps | 结合依赖服务反向判断,区分"真坏"和"健康检查误报" |
| 5 | 数据是否到达 | kafka-run-class kafka.tools.GetOffsetShell --broker-list ... | offset 全 0 = 从未收到;实时验证用 kafka-console-consumer |
三个核心认知,一次排障全部到位:
- advertised.listeners 是客户端视角------Docker 部署下最容易写错,也最隐蔽;
- unhealthy 不一定是服务坏了------先看健康检查命令本身,再看依赖它的服务;
- offset 是判定数据是否到达的最硬证据------比日志、比口头确认都可靠。