状态:草案(2026-10-17),部分经过实测。 基准版本为 metallb v0.16.1(提交 791dfcc63ced)。由于小型集群正在运行该版本(包含用于 FRR 模式的 controller、speaker 以及 frr-k8s),因此实测于 2026-10-17 在该集群中通过 仅读取 的方式获取(读取 CR、节点标签、服务、事件和 speaker 日志)。未直接捕获 ARP 数据包。 编写规则:以实测数据为基础,未能测量的内容标记为「未实测」,未能确认的内容标记为「未确认」。
目录
- 1. 一行总结:发放号码牌的窗口,以及听到名字便举手的人
- 2. 架构图
- 3. 代码地图
- 4. 跟踪核心路径
- 5. 作者的贡献与后续延伸
- 6. 实测:小型集群中的读取结果
- 7. 从哪里开始阅读
- 8. 适合做出第一次贡献的地方
- 9. 局限性
- 参考资料
1. 一行总结:发放号码牌的窗口,以及听到名字便举手的人
在云环境中,创建 type: LoadBalancer 服务时,云平台会为其附加一个外部 IP。而在裸金属环境中没有这种「云」,因此服务将永远保留为 <pending> 。MetalLB 填补了这一空白。
它主要负责两项工作。 号码牌窗口(controller)为每个服务从地址池中确定一个 IP,并写入服务的 status 中。 举手的人 (speaker)每个节点部署一个,当网络询问「这个 IP 是谁的?」时,仅指定的某一个节点回答「是我」(L2 模式)。或者向路由器宣告「将此 IP 发送给我」(BGP 模式)。
分工为两者的原因是所需的权限和位置不同。挑选 IP 的工作需要纵观整个集群并在 单一位置 进行以避免冲突,且不需要特殊权限。而宣告工作需要读取和写入每个节点网卡的 ARP 数据包,因此必须在 每个节点上 以 hostNetwork 和
NET_RAW 权限运行。权限较大的组件(speaker)不挑选 IP,而挑选 IP 的组件(controller)不接触数据包。
| 组件 | 职责 | 运行位置 |
|---|---|---|
| controller | 为每个服务分配地址池 IP、检查共享 IP 规则、记录 status、Web 钩子 | 1 个 Deployment |
| speaker | 通过 memberlist 互相检查生死、判断每个服务「是否轮到我来宣告」、响应 ARP/NDP | DaemonSet(每个节点,hostNetwork) |
| frr-k8s(FRR 模式) | 将 speaker 编写的 FRRConfiguration 转换为 FRR 配置并运营 BGP 会话 | DaemonSet(每个节点) |
| CRD | IPAddressPool, L2Advertisement, BGPPeer, BGPAdvertisement, ServiceL2Status … | API Server |
2. 架构图
读图指南:
- 左上角的 controller 仅查看 API Server 的 Service 和 IPAddressPool 并确定 IP,写入
status.loadBalancer.ingress中。网络上不会发送任何内容。 - 中间的 speaker 读取 status 中写入的 IP,并通过 memberlist(端口 7946)互相确认是否存活。对于同一个服务,所有 speaker 各自进行相同的计算,且根据计算结果仅由排名第一的一个节点进行宣告。这里没有单独选出代表的共识(领导者选举)过程。
- 颜色代表小型集群的当前状态。worker A 正在宣告,worker B 处于等待状态,控制平面节点因排除标签而被排除在候选之外,GPU 节点位于 L2Advertisement 的 nodeSelectors 范围之外(第 6 节)。
- 右下角的 BGP 是虚线。小型集群虽然安装了 FRR 模式,但由于没有 BGPPeer,因此不进行 BGP 宣告。
3. 代码地图
| 路径 | 内容 | 首次阅读的文件 |
|---|---|---|
controller/ |
controller 二进制文件:服务调谐、调用 IP 分配 | main.go, service.go |
speaker/ |
speaker 二进制文件:各协议处理器(L2・BGP)与公共流程 | main.go, layer2_controller.go |
internal/allocator/ |
从地址池中选择 IP,共享 IP(sharing key)规则 | allocator.go |
internal/layer2/ |
ARP/NDP 响应器,免费 ARP(gratuitous ARP),遍历接口 | announcer.go, arp.go |
internal/speakerlist/ |
封装 memberlist:加入、生死事件、可用的 speaker 列表 | speakerlist.go |
internal/config/ |
将 CR 转换为内部配置(nodeSelectors → 节点集合等) | config.go |
internal/k8s/ |
Controller Runtime 上的监控与重新同步,节点状态及标签助手 | k8s.go, nodes/nodes.go |
internal/bgp/ |
BGP 会话实现集(native、frr、frr-k8s) | frrk8s/frrk8s.go |
api/ |
CRD Go 类型 | v1beta1/l2advertisement_types.go |
website/content/ |
文档(概念、配置、故障排查) | concepts/layer2.md |
4. 跟踪核心路径
4.1 controller:一个服务对应一个 IP
controller 的 SetBalancer, controller/main.go:52 在每次服务变更时
会被调用。通过复制服务
convergeBalancer, controller/service.go:49 将其调整为「应有的状态」后,如果有变化,则仅更新 status。
convergeBalancer 的思考逻辑是「能否保留当前拥有的 IP → 不能则重新获取」。如果服务已经拥有 IP,则尝试将该 IP 重新注册到分配器中(若符合配置则无事发生);如果地址池发生变化并不再被允许,则将其删除并重新获取。获取新 IP 的地方是
allocateIPs, controller/service.go:252 。如果用户
指定了 IP(如metallb.io/loadBalancerIPs 注解等),则使用该 IP;如果指定了地址池,则从该地址池中获取;如果什么都没指定,则遍历所有地址池
(Allocate, internal/allocator/allocator.go:363)查找空闲地址。
多个服务也可以共用一个 IP。条件是相同的 sharing key(metallb.io/allow-shared-ip 注解值)、不冲突的端口、
以及相同的 backend key(checkSharing, allocator.go:728)。
当 controller 删除一个服务时,会触发「重新处理所有服务」
(SyncStateReprocessAll)。这是为了让因没有空位而等待的服务能够获得该空位。
controller 在没有领导选举(LeaderElection: false)的情况下以单个 Deployment 运行。分配状态虽然保存在内存中,但重启时会通过将各服务的 status 中记录的 IP 重新注册到分配器(convergeBalancer 的第一步)来恢复状态。这一设计之所以可行,是因为真实数据的源头是 API Server 的 status。
4.2 speaker:针对所有服务的「轮到我了吗」
speaker 的 SetBalancer, speaker/main.go:385 也会针对每个服务被调用。如果不是 LoadBalancer、尚未分配 IP,或者该 IP 不属于任何地址池,则撤销广播。否则,对于每个启用的协议调用
handleService, :446 ,若其中的协议处理器
ShouldAnnounce 返回空字符串则进行广播,若返回原因字符串则因该原因撤销广播
(deleteBalancerProtocol, :495)。广播时会留下 info 日志
serviceAnnounced 和服务事件 nodeAssigned(「announcing from node …」),撤销时会留下 serviceWithdrawn 及原因
(notOwner, noMatchingAdvertisement …)。第 6 节中的数字就是对这些日志行的统计。
4.3 L2 选举:过滤并按哈希排序
L2 处理器的 ShouldAnnounce, speaker/layer2_controller.go:83
是整个选举的核心。按顺序阅读如下:
- 如果服务没有准备就绪的端点(endpoint),则
notOwner。没有可用 Pod 的 IP 不会由任何人进行广播。 - 如果在对应服务的 L2Advertisement 中 没有选中我的节点,则
noMatchingAdvertisement(adsMatchNodeL2,:213。nodeSelectors 在 读取配置时已被预先转换为节点名称集合 (l2AdvertisementFromCR,internal/config/config.go:743). - 构建候选节点列表(
speakersForAds,:253).
for s := range eligibleNodes {
if k8snodes.IsNetworkUnavailable(nodes[s]) {
level.Debug(l).Log("event", "skipping should announce l2", "service", name, "reason", "speaker's node has NodeNetworkUnavailable condition")
continue
}
if !c.ignoreExcludeLB && k8snodes.IsNodeExcludedFromBalancers(nodes[s]) {
level.Debug(l).Log("event", "skipping should announce l2", "service", name, "reason", "speaker's node has labeled 'node.kubernetes.io/exclude-from-external-load-balancers'")
continue
}
if adsMatchNodeL2(ads, s) {
res[s] = true
}
}
来源:metallb v0.16.1 speaker/layer2_controller.go,Apache-2.0
eligibleNodes 是 memberlist 当前认为存活的 speaker(4.4 节)。在此基础上,处于 NetworkUnavailable 状态的节点、 附加了排除标签的节点以及 nodeSelectors 之外的节点会被剔除。排除标签的检查
是
IsNodeExcludedFromBalancers, internal/k8s/nodes/nodes.go:30
,它 不检查标签值,只检查是否存在键 。由于 kubeadm 会将此标签设为空值,因此即使值为空也会被排除。
关闭此检查的配置是 speaker 的 --ignore-exclude-lb 标志(speaker/main.go:99)。
4. 如果服务是 externalTrafficPolicy: Local ,则只保留拥有该服务端点的节点。由于这是一种不希望将流量转发到其他节点的策略,因此没有端点的节点不能占用 IP。
5. 对剩余的节点进行排序。
// Using the first IP should work for both single and dual stack.
ipString := toAnnounce[0].String()
// Sort the slice by the hash of node + load balancer ips. This
// produces an ordering of ready nodes that is unique to all the services
// with the same ip.
sort.Slice(availableNodes, func(i, j int) bool {
hi := sha256.Sum256([]byte(availableNodes[i] + "#" + ipString))
hj := sha256.Sum256([]byte(availableNodes[j] + "#" + ipString))
return bytes.Compare(hi[:], hj[:]) < 0
})
// Are we first in the list? If so, we win and should announce.
if len(availableNodes) > 0 && availableNodes[0] == c.myNode {
return ""
来源:metallb v0.16.1 speaker/layer2_controller.go,Apache-2.0
这种排序是设计的核心。由于所有 speaker 使用 相同的输入(存活节点列表、相同的配置、相同的 IP)计算 相同的函数,因此无需相互询问就能确定唯一的第 1 名。既不需要投票,也不需要加锁。由于是通过将 IP 附加到节点名称进行哈希计算,因此每个 IP 的第 1 名各不相同;如果存在多个服务,广播就会分散在各个节点上(即第 6 节中分为 4 个和 6 个的情况)。由于哈希的原料是 IP 而不是服务,因此共用 IP 的服务如果候选列表相同,则会由同一个节点进行广播。这是因为同一个 IP 不能拥有两个 MAC 地址。
代价也源于这种排序。如果作为输入的「存活节点列表」发生变化,第 1 名可能会随之改变。如果某个节点的 speaker 不断重复宕机和恢复,那么该节点原本作为第 1 名的那些 IP 就会每次都更换所有者。在 「公网访问中断的那一天」中记载的 「广播节点一天内变动了 1,275 次」正是这段代码的必然结果。因此,使用 nodeSelectors 将候选范围缩小到 稳定可靠的节点便成为了一种解决方案。
并且在第 3 阶段和第 2 阶段交汇的地方有一个陷阱。如果 nodeSelectors 仅选择带有排除标签的节点,这些节点虽然能通过第 2 阶段,但会在
第 3 阶段被排除,而其他节点则会在第 2 阶段 noMatchingAdvertisement 处被排除。候选者变为 0, 所有 speaker 都会停止广播。
剩下的痕迹只有一行 debug 级别的日志。这就是第 5 节的 PR 在文档中写的内容。
4.4 memberlist:存活判断的来源
作为选举输入的“存活的 speaker”由 HashiCorp memberlist 提供。
speakerlist.New, internal/speakerlist/speakerlist.go:57
使用节点名称作为 memberlist 的名称(为了与端点的 nodeName 进行匹配),绑定到 Pod IP 和 7946 端口,如果存在密钥,则用其哈希来
加密流量。如果没有绑定地址或标签设置,memberlist 将被关闭,此时会假设“speaker 存在于所有节点上”,
并直接使用 API Server 的节点列表作为候选者。
UsableSpeakers, :303 会直接返回 memberlist
的当前成员。当成员发生变化(加入或离开)时, memberlistWatchEvents, :339
会留下 “node event - forcing sync” 并 重新计算所有服务。
为什么不只使用 Kubernetes 的节点 Ready 条件?默认情况下,节点控制器将节点更改为 NotReady 需要数十秒钟, 在此期间死掉的节点会一直占有 IP。memberlist 通过节点之间直接发送 PING,在几秒钟内将其标记为可疑(Suspect)并显示为失败。 相反,如果有 speaker 频繁崩溃和恢复的节点,就会频繁发生“重新计算”。我们将在第 6 节中看到这一痕迹。
4.5 响应 ARP:应答器与免费 ARP(Gratuitous ARP)
如果在选举中获胜,L2 处理器的 SetBalancer, layer2_controller.go:131
会将 IP 注册到 Announce.SetBalancer, internal/layer2/announcer.go:258
。数据包处理是单独运行的。
- 在哪个网卡上响应。
updateInterfaces,announcer.go:61会定期扫描节点的网卡,为每个处于启用状态、不在网桥下方且没有 NOARP 标志的网卡创建 ARP 应答器,为每个具有 IPv6 链路本地地址的网卡创建 NDP 应答器。排除列表(正则表达式)从 ConfigMapmetallb-excludel2中读取 (parseAnnouncedInterfacesToExclude,speaker/main.go:572)。 如果在 L2Advertisement 中写入interfaces,则只在该网卡上响应。 - 当有人询问时。
processRequest,internal/layer2/arp.go:73当 ARP 请求是广播或者是发往本机时,如果该 IP 已注册且网卡匹配 (shouldAnnounce,announcer.go:237) 就会使用本机 MAC 地址进行响应。由于是由 speaker 通过原始套接字(Raw Socket)而不是内核进行响应,因此节点的 IP 配置不会改变。这就是需要NET_RAW权限的原因。 - 当所有者发生变化时。 对于刚开始广播的 IP,
spamLoop,announcer.go:157会在 5 秒内以 1.1 秒的间隔广播免费 ARP。这是为了让记住旧 MAC 地址的交换机和路由器快速更新其转发表。注释中说明 1100ms 的间隔 是根据 Issue #172 的经验得出的。
4.6 BGP 与 FRR 模式(简介)
在 BGP 处理器的 ShouldAnnounce, speaker/bgp_controller.go:155
中没有选举。满足条件的所有节点各自向路由器通告路由,负载均衡由路由器的 ECMP 完成。不过,排除标签
检查在这里也存在(:178).
实际打开 BGP 会话的实现通过环境变量 METALLB_BGP_TYPE 在三者中选择
(speaker/main.go:109,
newBGP, bgp_controller.go:492). frr-k8s 如果是 FRRConfiguration ,speaker 不会直接使用 BGP 协议,而是为每个节点创建一个updateConfig, internal/bgp/frrk8s/frrk8s.go:193CR
(
)。 frr-k8s 守护进程集(DaemonSet)会将该 CR 转换成 FRR 配置。这种分离是为了让同一节点上的 FRR 也能被 MetalLB 以外的其他组件(例如节点自身的路由通告)共同使用。
5. 作者的贡献与后续metallb/metallb#3156(截至 2026-10-17 仍然处于开启状态)是一个文档 PR。修改的地方有两个。
- 在 L2 高级设置文档的
nodeSelectors说明中添加了以下指引:“带有排除标签的节点即使符合 nodeSelectors 也不会进行广播,kubeadm 会将此标签附加到控制 平面上,如果只选择这些节点,将没有任何节点进行广播,绕过方法是--ignore-exclude-lb(Helmspeaker.ignoreExcludeLB)”。 - 在故障排查文档的“speaker 未能通告服务的情况”列表中添加了此标签。
与代码的连接与 4.3 节完全相同。标签检查位于
nodes.go:30,L2 中使用它的地方是
layer2_controller.go:268,剩下的日志是同一文件
第 269 行的一行 debug 日志(PR 正文中提到,这条日志是以 Issue #2505 为契机在 #2538 中添加的)。文档未能跟上代码的
地方在于“nodeSelectors 是硬过滤器”这一解释。虽然它是硬过滤器没错,但漏掉了在其之前还有 另一个硬过滤器(排除标签)这一点。事故的来龙去脉
记录在 “公网访问中断的那一天”中。
6. 实测:小型集群观察
2026-10-17,所有命令均为只读(kubectl get·logs)。节点名称已替换为角色,地址已被遮罩,原始数据已单独保存。
架构。
| 项目 | 值 |
|---|---|
| controller | Deployment 1个, controller:v0.16.1,参数为 --webhook-mode=enabled |
| speaker | DaemonSet 8/8, speaker:v0.16.1,hostNetwork,权限仅有 NET_RAW 一个(其余全部 drop),只读根文件系统 |
| speaker 端口 | 9120(指标),7946 TCP·UDP(memberlist) |
| speaker 环境变量 | METALLB_BGP_TYPE=frr-k8s, METALLB_ML_BIND_ADDR=Pod IP, METALLB_ML_BIND_PORT=7946 |
| frr-k8s | DaemonSet 8/8(5 个容器),用于状态清理的 Deployment 1 个。FRRNodeState 8 个,FRRConfiguration 0 个,BGPPeer 0 个 |
| CRD | metallb.io 9 种, frrk8s.metallb.io 4 种 |
| IPAddressPool | 1 个,16 个地址, autoAssign: true。status 中已分配 11,剩余 5 |
| L2Advertisement | 1 个, nodeSelectors = 主机名 In [Worker A, Worker B], interfaces 无 |
| LoadBalancer 服务 | 14 个服务共用 11 个 IP(四个服务通过相同的 sharing key 共享一个 IP) |
metallb-excludel2 |
基本列表 14 个(^docker.*, ^veth.*, ^lxc.*, ^cali.* …) |
即使安装了 FRR 模式,由于没有 BGPPeer,也没有创建 FRRConfiguration。4.6 节的 frr-k8s 路径在当前小型集群的配置中处于 闲置状态。
排除标签在 -L 中是看不出来的。 kubectl get nodes -L node.kubernetes.io/exclude-from-external-load-balancers 八个节点的这部分内容全都是
空白。由于标签值为空字符串,看起来和“不存在”完全一样。通过按键是否存在来筛选的 -l node.kubernetes.io/exclude-from-external-load-balancers
再次查看时, 控制平面集出现了。结合 4.3 节的检查看的是键而不是值,这造成了在人眼看来是“无标签”,而在代码
眼中则是“被排除”的组合。NetworkUnavailable 条件在所有八个节点上均为 False。
选举结果。 每个正在由 speaker 广播的服务所留下的 ServiceL2Status 有 10 个。
| 广播节点 | 服务数 |
|---|---|
| Worker A | 4 |
| Worker B | 6 |
| 其他 6 台 | 0 |
这是 2 台候选者(图 2 的第 4 阶段)按 sha256 顺序分配 IP 的结果。共享 IP 的那组服务没有 L2 状态,这四组服务的就绪端点数
全部为 0。正如 4.3 节第 1 阶段(notOwner)所述,没有任何人进行广播。在服务事件中,
announcing from node "<워커 A>" with protocol "layer2" 每个服务留下了 8 条(在事件保留期限内的值)。
memberlist 与应答器。 这是 Worker A 的 speaker(当前容器从 10-14 开始)的启动日志。
| 日志 | 次数·内容 |
|---|---|
node event - forcing sync(NodeJoin) |
8(包含自身共 8 个节点) |
memberlist join successfully |
1, number of other nodes: 7 |
created ARP responder for interface |
3:有线网卡、无线网卡、 cilium_vxlan |
created NDP responder for interface |
2:有线网卡、 cilium_vxlan |
明明是只使用有线的节点, 无线网卡和 Cilium 的 VXLAN 设备上也 附加了 ARP 应答器。这是因为默认排除列表中没有 cilium_* 且
L2Advertisement 中没有写入 interfaces (4.5 节)。虽然目前没有暴露出问题,但如果想把广播限定在特定的网卡上,
写入
interfaces 才是正确的做法。
动荡之日的痕迹。 统计了 Worker B 的 speaker(当前容器,约 22 天的日志)。
| 日志 | 次数 |
|---|---|
service has IP, announcing |
1,948 |
withdrawing service announcement |
920(原因: notOwner 918, noMatchingAdvertisement 2) |
node event - forcing sync |
434 |
memberlist: Suspect … has failed, no acks received |
166(大部分为 GPU 节点 C) |
memberlist: Marking … as failed, suspect timeout reached |
106 |
按日期划分,memberlist 可疑消息在 10-04 有 138 条,10-05 有 39 条,10-07 有 10 条,10-13 有 1 条,同一天的广播日志在 10-04 有
1343 条,10-05 有 322 条,此后每天大约 10 条左右。同期 GPU 节点 C 的 speaker 重启次数为 360 次(最后一次退出在 10-08,退出代码为 1)。
每当一个 speaker 崩溃和恢复时,memberlist 成员就会发生变化(4.4 节),所有 speaker 都会重新计算,该节点曾排在第一位的 IP 的所有者
也会发生变化(4.3 节)。 noMatchingAdvertisement 2 条(10-14)似乎是把 nodeSelectors 更改为控制平面那一瞬间,Worker B 从候选者中剔除时留下的日志。
speaker 重启次数。 控制平面集 0,Worker A 2,Worker B 10,GPU 节点 A 8,GPU 节点 B 0(Pod 存活 2 天),GPU 节点 C 360。原因未在本文中 进行追查(未确认)。
版本字符串。 speaker 和 controller 的启动日志全都是 MetalLB … starting version 0.16.1 (commit dev, branch dev) 。这是官方镜像,但 commit 却显示为 dev 。第 8 节的第一项就是原因所在。
7. 从哪里开始阅读
website/content/concepts/layer2.md。首先掌握概念 (文档 5 分钟即可读完)。speaker/layer2_controller.go:83。完整阅读 选举的一个函数。4.3 节的五个阶段全都在这里。internal/speakerlist/speakerlist.go:57。 查看选举的输入来源。internal/layer2/announcer.go:61和arp.go:73。这是获胜后实际响应数据包的地方。controller/service.go:49。另一侧,即决定 IP 的部分。speaker/main.go:385。最后查看两个协议是如何组合到一个流程中的。
8. 适合进行第一次贡献的地方
这是在阅读源码时实际注意到的一些问题。尚未提交到任何地方。
- 发行版镜像的 commit 为
dev. 。制作镜像的工作流将build-args以키: 값的形式传递 (.github/workflows/publish.yaml:109).
build-args: |
GIT_BRANCH: ${{ github.ref_name }}
GIT_COMMIT: ${{ github.sha }}
来源:metallb v0.16.1 .github/workflows/publish.yaml,Apache-2.0
docker/build-push-action 的 build-args 每行都期望有 키=값 ,因此 Dockerfile 的默认值 ARG GIT_COMMIT=dev 似乎直接保留了下来。我们在第 6 节中确认了两个官方 v0.16.1 镜像都打印了 commit dev, branch dev 。这是一个可以将其更改为 = 的单行修改候选项。
- memberlist 配置文件注释与实际行为不符。
memberlistFileConfig注释虽然说“如果文件存在,所有字段都是必填项,缺失则报错” (speakerlist.go:357),但实际上会报错的 只有时间字符串字段,整数和布尔字段如果缺失会静默变为 0 或 false(例如:IndirectChecks0)。此外,读取文件时,--ml-wan-config将从局域网默认值开始,而与:397).internal/speakerlist无关。 - 错误文案。 当 backend key 不同时,提示信息为 “does not match existing sharing key”
(
allocator.go:623)。排除标签函数注释中的 “labeld”与不匹配的引号(nodes.go:29)也是同类问题。 - 安静的“无广播”。 4.3 节的陷阱只会留下的一条 debug 日志。如果候选者因为排除标签而变为 0,那么记录一个服务事件可以作为文档 PR(#3156) 之后的下一步来考虑。由于这会改变行为,最好先通过 Issue 询问。
- 默认排除列表与 Cilium。 在第 6 节中,
cilium_vxlan上看到了 ARP 响应器附加。在默认列表 (config/native/exclude-l2-config.yaml) 中 有 Calico 和 Flannel 的设备名称,但 Cilium 的只有lxc一个。需要首先确认这是否有害(未确认)。
贡献流程要求 CONTRIBUTING 进行 DCO 签名
(Signed-off-by),详细说明请参考网站的社区页面。PR 模板提到,如果变动不小,请先开启一个 Issue。在 2026-10-17,以 good first issue 标签开启的 Issue 有 5 个以上(测试执行文档化、容器级测试等)。
9. 局限性
- BGP 模式仅介绍了概述。会话管理、BFD、Community、frr-k8s 内部没有深入查看,并且在小集群中由于没有 BGP 对端(Peer)也未能进行测量(未实测)。
- 未涉及 IPv6 与双栈分配、
avoidBuggyIPs、地址池选择器(serviceAllocation)以及 Webhook 验证。 - 广告节点切换时实际中断了多少秒,以及 gratuitous ARP 何时修正路由器表,这些均未通过抓包进行测量(未实测)。
- 第 6 节中的日志数量仅统计了各个 speaker 的 当前容器 日志,因此不包含之前重启前的记录。
- 事故本身的经过与诊断顺序交给了 “公网连接中断的那一天”。
Titi 的一句话。 如果八位同学分别用相同的公式计算“这球轮到谁?”,即使不进行会议,也恰好只有一个人会举手。但是,如果只挑出那些名字牌背面贴着“我退出”贴纸的同学,那球就只能在地 板上白白滚来滚去啦!
参考
所有资料均查阅于 2026-10-17。
- metallb v0.16.1 源码: https://github.com/metallb/metallb/tree/v0.16.1
- L2 概念文档: https://github.com/metallb/metallb/blob/v0.16.1/website/content/concepts/layer2.md
- L2 高级配置文档: https://github.com/metallb/metallb/blob/v0.16.1/website/content/configuration/_advanced_l2_configuration.md
- 故障排除文档: https://github.com/metallb/metallb/blob/v0.16.1/website/content/troubleshooting/_index.md
- 贡献指南: https://github.com/metallb/metallb/blob/v0.16.1/CONTRIBUTING.md
- HashiCorp memberlist: https://github.com/hashicorp/memberlist
- Kubernetes 官方标签列表(排除标签): https://kubernetes.io/docs/reference/labels-annotations-taints/#node-kubernetes-io-exclude-from-external-load-balancers
- 作者的文档 PR: https://github.com/metallb/metallb/pull/3156
- 同系列: 阅读 CloudNativePG。其他系列: 公网连接中断的那一天
引用的代码和配置属于 metallb,遵循 Apache License 2.0 协议。