Kafka 只有它连不上:advertised.listeners 广播地址配错排查复盘

引言

客户现场有一套自建的 Kafka,用 Docker Swarm 部署,单节点 KRaft 模式,全公司好几个业务系统都挂在上面收发消息。某天接到反馈:有一个应用一直连不上 Kafka,其他应用全都好好的。

这种”别人都没事,就它不行”的故障是最容易把人带沟里的。第一反应几乎必然是——是不是这个应用自己的配置写错了?是不是它跑的那台机器网络有问题?

最后的结论确实和 Kafka 配置有关,但根因不在那个应用身上,也不在其他任何一个应用身上,而在一个大多数人都配过、却很少有人真正理解其语义的参数:KAFKA_ADVERTISED_LISTENERS。

更准确地说:这个参数配错的故障表现,天生就是”选择性的“——它不会打死所有人,只会打死那些网络路径和它不匹配的客户端。这也正是它最迷惑人的地方。

一、故障现象

先交代一下环境拓扑,这是后面所有推理的基础:

  • Kafka 跑在客户的 Swarm 集群里,通过一个统一的对外入口访问,下文把这个入口的域名脱敏为 pfscene.example.com(真实域名保留在客户环境里)。
  • 大部分业务应用部署在内网,和 Kafka 处在可以互通的网段。
  • 出问题的那个应用部署在另外一台独立机器上,它连不到 Kafka 的内网地址,只能走 pfscene.example.com 这个对外入口。

故障表现的形态很典型。应用启动之后日志里反复刷:

1
2
3
[Producer clientId=producer-1] Connection to node 1 (192.168.0.196/192.168.0.196:9092) could not be established. Broker may not be available.
...
org.apache.kafka.common.errors.TimeoutException: Topic order-events not present in metadata after 60000 ms.

Python 客户端那边是另一个长相,但本质一样:

1
2
3
kafka.errors.NoBrokersAvailable: NoBrokersAvailable
# 或者卡很久之后
kafka.errors.KafkaTimeoutError: KafkaTimeoutError: Failed to update metadata after 60.0 secs

这里有个细节值得先圈出来:报错里的地址是 192.168.0.196:9092,一个内网地址。而那个应用配置里写的连接地址明明是 pfscene.example.com。它从来没主动连过 192.168.0.196,这个地址是Kafka 主动告诉它的。

当时没人注意到这个细节,包括我在内。

二、第一轮排查:先证明它到底能不能连上

面对”连不上”的报障,第一步永远是把”连不上”这三个字拆开——是网络层不通,还是连上了但用不了?这两者的排查方向完全相反。

先从那台故障机器上做最朴素的端口探测:

1
2
# 从故障机器探测对外入口,看四层是否可达
nc -zv pfscene.example.com 19092

结果:通。

再让用户确认防火墙、安全组、出网策略,也都没有拦。也就是说:

网络是通的,应用能建立到 Kafka 的 TCP 连接,但它依然起不来。

这一步排除掉了一大半可能性(路由、ACL、防火墙、DNS 解析失败),同时把问题压缩到了一个很窄的范围里:连接建立得起来,但会话建立不起来。在 Kafka 的语境下,这几乎等价于一句话——客户端拿到的集群信息是错的。

同时,另一个事实也在反复敲打我们:其他应用全都正常。如果 Kafka 本身配置有问题,为什么它们没事?

说实话,到这里我的直觉也是偏向”那个应用自己的问题”——毕竟其他应用都正常这个反证太强了。这恰恰是这次排查里最大的思维陷阱:”其他人都正常”只能证明故障不是全局的,不能证明故障不在被怀疑的服务端。

三、关键一击:让 Kafka 自己交代它广播了什么

既然是”连上了但用不了”,那就要看客户端在握手之后拿到了什么。Kafka 的元数据是可以用命令行直接问出来的,这也是这次排查里最快的一击:

1
2
# 站在故障机器的视角,向对外入口要一份集群元数据
kcat -b pfscene.example.com:19092 -L

输出大意如下(已脱敏):

1
2
3
4
Metadata for all topics (from broker -1: pfscene.example.com:19092/bootstrap):
1 brokers:
broker 1 at 192.168.0.196:9092 (controller)
0 topics:

真相就在这里,一行字:

  • from broker -1: pfscene.example.com:19092/bootstrap——这一行是我主动连的地址,也就是 bootstrap 地址,Kafka 标记为 broker -1,意思是”还没分配身份的入口”。
  • broker 1 at 192.168.0.196:9092——这一行是 Kafka 告诉我”你应该来这儿找我” 的地址。

于是整个故障链条一下子闭合了:

  1. 应用连 pfscene.example.com:19092 → 成功(所以 nc 是通的)。
  2. Kafka 回了一句”我这个 broker 在 192.168.0.196:9092“。
  3. 应用老老实实去连 192.168.0.196:9092 → 那台独立机器根本不在这个内网里,路由不可达 → 超时、重试、再超时。

补一刀验证这个推断:

1
2
3
# 从故障机器分别探测两个地址,对比结果
nc -zv pfscene.example.com 19092 # 通
nc -zv 192.168.0.196 9092 # 超时

再回到 broker 侧确认配置本身:

1
2
3
4
5
# Swarm 下容器名形如 <stack>_kafka.1.<hash>,没有固定名字,按服务名过滤取容器
docker exec "$(docker ps -q -f name=kafka | head -n1)" env | grep -i ADVERTISED

# 输出:
# KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.0.196:9092

现场的 compose 片段(缩进已整理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
services:
kafka:
image: apache/kafka:4.1.1
container_name: broker
environment:
# broker 实际监听的地址:0.0.0.0:9092,容器内外都能进来
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
# ↓↓↓ 问题就在这一行:广播出去的是宿主机内网 IP
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://192.168.0.196:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
ports:
- target: 9092
published: 9092
protocol: tcp
mode: host
networks:
- net

KAFKA_LISTENERS 里写 PLAINTEXT://:9092(即绑 0.0.0.0:9092)不是问题的根源——它决定了 broker 在哪张网卡上接客,是服务端视角的配置。真正捅娄子的是 KAFKA_ADVERTISED_LISTENERS,它是客户端视角的配置,含义是:

“客户端你好,请你用这个地址回来找我。”

而这个地址,被硬编码成了一个外部客户端永远够不着的内网 IP。

四、原理:bootstrap 只是”敲门砖”

如果要问 Kafka 网络配置里最容易踩、后果最隐蔽的一个坑是什么,我会毫不犹豫投给 advertised.listeners。它隐蔽的根本原因在于:Kafka 客户端不是只连一个地址,而是连两次。

一次完整的客户端接入过程是这样的:

1
2
3
4
5
6
7
8
外部应用                    pfscene 入口              Kafka Broker
| | |
|--- ① 用 bootstrap 连接 --->|--- 转发 -------------> | 成功(所以 nc 通)
| | |
|<-- ② Metadata 响应: --------------------------| 返回「我在
| "broker 1 在 192.168.0.196:9092" | 192.168.0.196:9092」
| | |
|--- ③ 改用 ② 给出的地址直连 -----------------------✗ | 路由不可达 → 超时
  • 第一阶段(bootstrap):客户端拿 bootstrap.servers 里的地址,随便挑一个连上去。这个地址的作用仅仅是敲门——它只要能带着客户端进入集群即可,可以是任何一个 broker。
  • 第二阶段(metadata):连上之后客户端会请求一份集群元数据。broker 在响应里给出”分区 leader 在哪台 broker 上”,而这些 broker 的地址,就是它们各自 advertised.listeners 里配的值,由 broker 原样广播出去。
  • 第三阶段(真正干活):客户端丢弃 bootstrap 地址,改用 metadata 里返回的地址去连真正的 leader 收发消息。

关键点在于:Kafka 完全不关心它广播出去的地址对客户端是否可达。 它只是如实转述配置里写的东西。地址能不能通,是配置者的责任。

所以现场的现象不是巧合,而是必然:

客户端位置 bootstrap 用的地址 metadata 拿到的 broker 地址 结果
内网 / Swarm 内的应用 kafka:9092 或内网 IP 192.168.0.196:9092 可达 → 正常
那台独立机器上的应用 pfscene.example.com:19092 192.168.0.196:9092 不可达 → 超时

这张表就是”其他应用都正常、只有它不行”的全部答案:同一个 broker、同一份配置,对不同网络路径的客户端产生了不同的结果。advertised 配错的故障面不是”全体下线”,而是”走到不可达路径的那部分客户端”——这就是它被称为选择性故障的原因。

顺带说一句,如果 advertised.listeners 干脆不配会怎样?Kafka 会退而使用 listeners 的值;而如果 listeners 里的 host 部分留空(就像上面那样写成 PLAINTEXT://:9092),Kafka 会拿 java.net.InetAddress.getCanonicalHostName() 去猜——在容器里通常得到的是容器主机名或容器 IP,那是一个比内网 IP 更不可达的地址。所以这个参数不但要配,还必须配对。

五、为什么”把那一行改掉”不是正确答案

知道了根因,最直觉的修复是:把广播地址改成对外入口不就行了?

1
2
# 看似能治病的改法
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://pfscene.example.com:19092

这个改法确实能让那台独立机器连上。但它会顺手打死另一批客户端。

别忘了,同一套服务里还躺着一个内部组件——Kafdrop(Kafka 的 Web 管理界面),它就在同一个 Swarm 网络里:

1
2
3
4
kafdrop:
image: obsidiandynamics/kafdrop:latest
environment:
KAFKA_BROKERCONNECT: "kafka:9092" # 它走的是内部服务名

改完之后会发生什么?

  1. Kafdrop 用 kafka:9092 做 bootstrap → 依然连得上。
  2. broker 回它一句”broker 1 在 pfscene.example.com:19092“。
  3. Kafdrop 转身去连 pfscene.example.com → 如果容器内的 DNS 解析不到这个名字,或者解析出来但回程路由不通(对外入口多半做了 NAT 甚至反向代理),它照样挂。

于是就成了按下葫芦浮起瓢:修好了外部的,打死了内部的。

问题的本质在于:advertised.listeners 是按 listener 广播的,而当时只有一个 listener,所以全天下所有客户端只能听到同一个地址。可现实里客户端来自两条不同的网络路径,一条走内部服务名,一条走对外入口。一个地址伺候不了两条路径。

六、正确解法:拆成内、外两个 listener

Kafka 早就为这种场景准备好了机制——多 listener:broker 在不同网卡/端口上监听,并针对每类客户端广播各自可达的地址。

改造后的完整 compose(Swarm stack 文件,可直接 docker stack deploy):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
version: '3.8'

services:
kafka:
image: apache/kafka:4.1.1
container_name: broker # 注:Swarm 模式下该字段不生效,容器名形如 <stack>_kafka.1.<hash>
environment:
# ---------- 监听器:内部 9092 / 外部 19092 / controller 9093 ----------
KAFKA_LISTENERS: INTERNAL://:9092,EXTERNAL://:19092,CONTROLLER://:9093

# ---------- 广播地址:每类客户端只听到自己那条路径上可达的地址 ----------
# INTERNAL 用 Swarm 服务名,不写死宿主机 IP,换节点/换机器都不用改配置
# EXTERNAL 用对外入口的域名和端口
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,EXTERNAL://pfscene.example.com:19092

# 所有 listener 都必须在这里登记,漏一个 broker 就起不来
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT

# 多 listener 场景必须显式指定集群内部走哪个 listener,否则启动即报配置异常
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL

# ---------- KRaft 单节点 ----------
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093

# ---------- 单副本兜底 ----------
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_NUM_PARTITIONS: 1

ports:
# 内部端口:主要是给宿主机侧的 kcat / nc 排查用,不需要对外暴露
- target: 9092
published: 9092
protocol: tcp
mode: host
# 外部端口:pfscene 入口最终要指向这里
- target: 19092
published: 19092
protocol: tcp
mode: host

deploy:
replicas: 1
restart_policy:
condition: on-failure

networks:
- net

# 内部客户端的代表:它连 kafka:9092,拿到的也必须是内部可达的地址
kafdrop:
image: obsidiandynamics/kafdrop:latest
container_name: kafdrop
environment:
KAFKA_BROKERCONNECT: "kafka:9092"
JVM_OPTS: "-Xms256M -Xmx1024M"
SERVER_PORT: 9000
ports:
- target: 9000
published: 19000
protocol: tcp
mode: host
depends_on:
- kafka # 注:Swarm 模式下 depends_on 同样不生效,这里只表达启动顺序意图
networks:
- net

networks:
net:
external:
name: sf_net

和现场原配置逐项对比,实质改动其实只有四处:

配置项 改造前 改造后
KAFKA_LISTENERS PLAINTEXT://:9092,CONTROLLER://:9093 拆成 INTERNAL / EXTERNAL / CONTROLLER 三个
KAFKA_ADVERTISED_LISTENERS PLAINTEXT://192.168.0.196:9092 按 listener 分别广播内外两个地址
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP 只登记 CONTROLLER / PLAINTEXT 登记全部三个,并新增 KAFKA_INTER_BROKER_LISTENER_NAME
ports 只发布 9092 追加发布 19092(网关侧同步调整指向)

其余(KRaft 单节点、副本因子、mode: host、外部网络 sf_net)都保持原样。顺带说明两处”看着像配置、实际不生效”的字段:container_name 和 depends_on 在 Swarm 模式下都会被忽略,前者由 <stack>_<service>.<序号>.<hash> 规则生成容器名,后者只剩语义表达——它们不影响功能,但排查时别拿容器名去 docker exec(下一节验证时会用到这一点)。

几个容易忽略的细节:

  • KAFKA_INTER_BROKER_LISTENER_NAME 不能省。 单 listener 时它无所谓,一旦配了多个 listener,broker 就不知道集群内部通信该走哪一个,启动时会直接抛配置异常。这里指定 INTERNAL,因为 broker 之间(当前是单节点自连)走内部网络最稳。
  • advertised 的端口允许和实际监听端口不一致。 上面 EXTERNAL://pfscene.example.com:19092 里的 19092 是”告诉客户端来连这个端口”,broker 自身则在 EXTERNAL://:19092 上监听。这是网关/NAT 场景下的合法用法(早年很多”端口映射后客户端连不上”的问题,根源就在于此)——代价是所有外部客户端都必须真的能从那个地址和端口进来。
  • 内部地址用服务名而不是宿主内网 IP。 原来写 192.168.0.196 的问题是:集群迁到别的节点、宿主机换 IP,配置全废。改成 Swarm 服务名 kafka:9092 之后,内部客户端解析的是集群 DNS,跟宿主机 IP 解耦。
  • mode: host 发布端口时,端口落在运行容器的那个节点上,不经过 Swarm 的 routing mesh。所以 pfscene 入口必须指向实际跑着 broker 的那个节点,别指望随便指一个节点都能转发。

改完 advertised.listeners 属于静态配置,必须重启 broker 才生效,不能靠动态配置下发:

1
2
# 滚动重启该服务(单副本,会有秒级中断)
docker service update --force <stack>_kafka

七、验证:用两个视角各问一次

修复之后不要急着宣告成功,用同一个命令从两条路径分别验证一次,比什么都直观:

1
2
3
4
5
6
7
# 视角一:内部(在 Swarm 网络内的任意容器里执行)
kcat -b kafka:9092 -L
# 期望看到:broker 1 at kafka:9092

# 视角二:外部(在故障机器上执行)
kcat -b pfscene.example.com:19092 -L
# 期望看到:broker 1 at pfscene.example.com:19092

这里有一条可以刻进肌肉记忆的判据:

kcat -L 输出里 broker N at ... 那一行的地址,必须和你的 bootstrap 地址属于同一条可达路径。
如果两者一个是内网 IP、一个是对外域名,那这个客户端迟早要出事——出事的时刻,就是它真正开始收发消息的时候。

顺带提醒两个行为:

  • 修改生效后,客户端可能还抱着旧的 metadata 缓存不放。Java 客户端的 metadata.max.age.ms 默认 5 分钟,急的话直接重启客户端验证。
  • 如果客户端报的是 Topic xxx not present in metadata,别急着去查 topic 存不存在。这个报错的真实含义往往是”我连不上任何 leader”,元数据获取失败和 topic 不存在,在这里是同一个症状。

八、经验固化

这次故障的技术含量其实不高,但很值得记下来,因为它踩中了好几个经典的认知陷阱。

一、”其他人都正常”证明不了”服务端没问题”。
这个反证只能说明故障不是全局的。凡是”部分客户端异常”的故障,都要先问一句:这部分客户端有什么共同点? 这次答案是”它们在另一条网络路径上”——而这句话直接指向了 advertised 配置。

二、报错里出现的陌生 IP,是服务端主动告诉客户端的。
应用配置里明明写的是 pfscene.example.com,日志里却在连 192.168.0.196。这种”身份不明的地址”不要放过,它八成来自服务端的元数据广播,而不是客户端配置。Kafka 的这个地址就是 advertised.listeners。

三、”端口通”不等于”能连上”。
nc -zv 通只验证了四层可达,Kafka 的会话能不能建立,还要看第二阶段拿到的地址对不对。凡是”端口通、应用不通”的中间件故障,都该往元数据/协商层去想。

四、配置来源要区分”服务端视角”和”客户端视角”。
listeners 是服务端绑在哪里,advertised.listeners 是告诉客户端去哪里。混为一谈就会写出一个”服务端自己觉得没问题、客户端全都连不上”的配置。类似的还有 Redis 的 cluster announce、Elasticsearch 的 publish_address,套路如出一辙。

五、动手前先问一句”这个改动会打死谁”。
把 broadcast 地址改成对外域名,能救外部客户端,但会顺手打死内部客户端(比如 Kafdrop)。在只有一个 listener 的前提下,任何单点修改都是在两类客户端之间二选一——正确的动作不是选边,而是把 listener 拆开。

最后一句话收尾:这个参数的正确写法从来没有绝对答案,它取决于你的客户端从哪里来。想清楚有哪些网络路径,就配几个 listener,让每条路径上的客户端都听到自己够得着的地址——这才是 advertised.listeners 的正确打开方式。


Kafka 只有它连不上:advertised.listeners 广播地址配错排查复盘
https://blog.952405.xyz/2026/08/kafka-advertised-listeners-troubleshooting/
作者
iDing
发布于
2026年8月15日
许可协议
转发请注明出处