状态:已解决 (2026-10-14),记录基准。 当天的数字(广播节点变动次数、speaker 重启次数)来自工作备忘录,当前的配置与节点标签通过 2026-10-16 的集群 读取进行确认。由于未能查看路由器日志,关于路由器内部发生的事情记录为 推测。 编写规则:仅依据记录中的事实,猜测标记为「推测」,未测量的内容标记为「未实测」。
目录
- 一句话总结
- 1. 架构:路由器 → 网关 IP → 「拥有」该 IP 的单个节点
- 2. 症状:仅外部无法访问
- 3. 排查:广播节点变动了上千次
- 4. 最初的对策是错误的
- 5. 修复方案:绑定两个有线工作节点
- 6. 然而内网中依然不可见:发卡(Hairpin)
- 7. 以后听到「无法连接」时
- 8. 遗留问题与局限性
- 参考
一句话总结
| 影响 | 外部无法访问网站(80 和 443 端口在 No route to host)。最初的对策实施后,短暂出现了 连内网也 无法访问的情况 |
| 原因 | 通过 ARP 响应网关 IP 的节点(广播节点)不断变化,其中甚至包含路由器无法触及的节点(推测) |
| 最初对策的失误 | 将广播节点固定为控制平面三节点组 → 三个节点都带有 exclude-from-external-load-balancers 标签,导致 无人进行广播 |
| 修复方案 | 固定到两个有线连接的工作节点。在外部三个地点确认返回 200 |
| 防止复发 | 选择广播节点时,优先检查排除标签。制定排查顺序。向 MetalLB 文档提交了指出此陷阱的 PR |
1. 架构:路由器 → 网关 IP → 「拥有」该 IP 的单个节点
发往网站的请求会经过家用路由器的端口转发。路由器将 80 和 443 端口转发到集群的网关服务 IP,该 IP 是 MetalLB 通过 L2 模式分配的 LoadBalancer 地址。
在 L2 模式下,该 IP 并不绑定到任何一台机器的网卡上。当路由器通过 ARP 询问「这个 IP 的 MAC 地址是什么?」时,运行 MetalLB speaker 的 其中一个节点会回答「是我」。MetalLB 文档对确定该节点的方法进行了如下说明(基于文档):
- 所有 speaker 按照相同的规则生成「节点+IP」哈希列表,排在最前面的节点进行广播。因此广播节点同时只有一个。
- 通过 memberlist 感知节点的死亡,由存活的节点接管,并广播免费 ARP(gratuitous ARP)以通告「MAC 地址已更改」。
- 局限性:所有流量都涌向单个节点,接管需要几秒钟,在此期间未能及时更新 ARP 缓存的客户端仍会将流量发送到旧的 MAC 地址。
2. 症状:仅外部无法访问
- 当从外部通过网站地址访问 80 和 443 端口时
No route to host. - 如果直接在集群内部发送给网关服务则一切正常。域名指向的公网 IP 正确,DNS 也正常。
「内部可以访问」这一信号最让人迷惑。网关以及它后面的 Web Pod 都很健康。中断的地方在前面,即 路由器与广播节点之间。
3. 排查:广播节点变动了上千次
在网关服务的事件中,每当 speaker 开始广播时就会留下 announcing from node "…" (这也是 MetalLB 故障排查文档推荐的确认方法)。当天清点这些事件时,发现 广播节点变动了 1,275 次(工作备忘录)。
- 其中一个工作节点的 speaker 陷入了 CrashLoop。截至当天已重启 360 次(2026-10-16 读取的重启次数也是 360 — 此后停止了)。由于每当 speaker 生死切换时 memberlist 的成员就会发生变化,根据第 1 节的规则,哈希列表的最前面也会随之改变。
- 广播节点的候选者中甚至包含了通过笔记本电脑连接的节点。
No route to host 出现这一现象的路径 推测如下。路由器通过 ARP 询问要转发的目标 IP 的 MAC 地址,如果那一瞬间的广播节点是路由器无法触及的地方(如无线网络或虚拟网络背后的笔记本节点等)或者正在完成接管,就不会收到回复。如果没有回复,路由器会返回「无法到达主机」的 ICMP,客户端将其显示为 No route to host 。未能查看路由器的 ARP 表和日志(未证实)。
4. 最初的对策是错误的
如果广播节点四处飘移是问题所在,那么将其绑定到最稳固的节点即可。最初想到的对象是 控制平面三节点组。不仅重启概率低,工作负载也很少。我们将 L2Advertisement 的 nodeSelectors 缩小到了这三个节点。
结果反而更糟了。 连内网中也 无法访问网站了。这是因为这次没有任何节点在进行广播。
原因是单个节点标签。
- kubeadm 在标记控制平面节点时,会附加
node-role.kubernetes.io/control-plane以及node.kubernetes.io/exclude-from-external-load-balancers标签。kubeadm 源码的markcontrolplane阶段会无值(空字符串)地附加这两个标签,常量注释解释称「为了与以往行为兼容而附加到控制平面」(基于源码)。Kubernetes 官方标签列表将该标签描述为「从外部负载均衡器的后端列表中排除节点的标签」。 - MetalLB 尊重此标签,带有该标签的节点不会进行广播。故障排查文档中包含了此内容以及绕过标志(
--ignore-exclude-lb)。不过,解释 L2nodeSelectors的地方并没有提及这一点。
即使标签值为空,只要 存在键 就会被排除。2026-10-16 读取时发现,所有控制平面节点都无值地挂载了此标签。
# 광고 노드를 고르기 전에 이것부터
kubectl get nodes -L node.kubernetes.io/exclude-from-external-load-balancers
nodeSelectors 如果选择的节点都带有排除标签,服务就会在保留外部 IP 的同时在任何地方都不予响应。不会显眼地留下错误或事件。为了将这一点记录在文档中,我向 MetalLB 提交了 PR —
metallb/metallb#3156(截至 2026-10-16 处于开启状态)。
5. 修复方案:绑定两个有线工作节点
排除控制平面后,剩下的标准是「与路由器处于同一有线局域网、不经常重启的工作节点」。我们将其固定到两台这样的迷你 PC 上。
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
spec:
ipAddressPools: [<게이트웨이 풀>]
nodeSelectors:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [<워커 A>, <워커 B>]
设置两台的原因是为了确保当其中一台宕机时另一台可以接管。如果绑定到一台,每次重启该节点时网站都会中断。重启一个工作节点后,在外部三个地点确认了 HTTP 200。
6. 然而内网中依然不可见:发卡(Hairpin)
修复后,在家里通过公网地址(域名)依然无法打开。这是另一个问题。本路由器不支持从内网流向公网 IP、再折返进入内网的路径(发卡 NAT,NAT 回环)。因此,无法在家里告知外部「无法访问」的状态,反过来,家里的「可以访问」也无法保证外部的情况。
确认分为两个方向:
# 안: 공인 DNS 를 건너뛰고 게이트웨이 IP 로 바로 — 이름(SNI·Host)은 그대로 둔다
curl -sS -o /dev/null -w '%{http_code}\n' --resolve <도메인>:443:<게이트웨이 IP> https://<도메인>/
# 밖: 여러 나라에서 HTTP 요청을 보내 주는 외부 점검 서비스(check-host.net 등)로
7. 以后听到「无法连接」时
- 直接通过网关 IP(
--resolve)访问。如果这里失败,则是集群内部的问题。 - 外部检查服务。如果 1 成功但 2 失败,则问题出在路由器和广播节点之间。
- 查看网关服务事件中的当前广播节点和变动次数(
kubectl get events --field-selector involvedObject.name=<서비스>). - 查看 speaker 日志中的 memberlist 加入失败次数和重启次数。
- 如果需要更改广播节点,请 优先 检查排除标签。
8. 遗留问题与局限性
- 本文未探讨 speaker 陷入 CrashLoop 的工作节点的根本原因。虽然该节点现在不再是广播候选者,对网站没有影响,但这并不意味着问题本身得到了修复。
- 广播节点变动时实际会中断几秒钟,这一点并未进行测量(未实测)。MetalLB 文档指出在大多数操作系统中为几秒钟。
- 如果两个广播节点 同时 宕机,连接将再次中断。目前尚无防止这种情况的机制(例如禁止同时重启的规则或警报)。
Titi 的碎碎念。 「内部可以访问啊?」是最让人害怕的话。内部和外部的路径不同,一边的绿灯并不能说明另一边的情况。而且选择节点时,比起名称更要看标签!一个静静的标签可能正在说「我就先告退了」。
参考
所有资料均于 2026-10-16 查阅。
- MetalLB L2 概念: https://metallb.universe.tf/concepts/layer2/
- MetalLB 故障排查(排除标签、
--ignore-exclude-lb、确认广播节点): https://metallb.universe.tf/troubleshooting/ - Kubernetes 官方标签列表: https://kubernetes.io/docs/reference/labels-annotations-taints/#node-kubernetes-io-exclude-from-external-load-balancers
- kubeadm 源码(控制平面标记阶段): https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/phases/markcontrolplane/markcontrolplane.go
- 因本次事件提交的文档 PR: https://github.com/metallb/metallb/pull/3156