MetalLB · 裸金属负载均衡

MetalLB 给这个没有云厂商 LoadBalancer 的裸金属 K3s 集群补上了 type: LoadBalancer 的能力 —— 它从一个静态 IP 池里给每个 LoadBalancer Service 分配一个外部 IP,并用 ARP 把这个 IP 广告到二层网络上,让局域网内的流量能找到承载它的节点。部署在 metallb-system 命名空间,当前镜像 quay.io/metallb/{controller,speaker}:v0.16.1

工作原理

集群用的是 L2 模式(无 BGP)。MetalLB 由两类组件配合:controller 监听集群里的 LoadBalancer Service,从 IP 池里给它分配一个地址;speaker 则在每个节点上运行,由其中一个节点出面用 ARP 响应该 IP 的请求,把流量吸引到自己身上,再由 kube-proxy 转发到后端 Pod。L2 模式下同一时刻只有一个节点应答某个 IP,故障切换靠 speaker 之间的 memberlist 选举完成。

IPAddressPool 声明可分配的地址范围,L2Advertisement 声明这个池要以 L2/ARP 方式对外广告 —— 两者都是 metallb.io/v1beta1 自定义资源,缺一个 IP 就分不出去或者广告不出来。

组件清单

  • Name
    controller (Deployment)
    Description
    单副本,跑在带 workload: infra 标签的节点上。以非 root(uid 65534)运行,readOnlyRootFilesystem,丢弃全部 capability。除分配 IP 外,还以 --webhook-mode=enabled 同时充当校验 webhook 的服务端。端口:metrics 7472、monitoring 7473、webhook 9443。健康检查走 /healthz/readyz,端口 17472(独立于 metrics 端口)。
  • Name
    speaker (DaemonSet)
    Description
    每个节点一份(含 master,带对应 NoSchedule 污点的 toleration)。hostNetwork: true,需要 NET_RAW capability 才能发 ARP;同样 readOnlyRootFilesystem、丢弃其余全部 capability。通过 downward API 注入 METALLB_POD_NAMEMETALLB_NODE_NAMEMETALLB_HOSTMETALLB_ML_BIND_ADDR 等环境变量,并挂载 memberlist secret 做节点间加密通信。健康检查同样走 127.0.0.1:17472/healthz/readyz
  • Name
    命名空间策略
    Description
    metallb-system 打了 pod-security.kubernetes.io/enforce: privileged(speaker 需要 hostNetwork + NET_RAW)。配有 LimitRangeResourceQuotapersistentvolumeclaims: "0",禁止申请存储)以及一组 NetworkPolicy:默认拒绝 ingress,仅放行同命名空间互通、controller 的 9443 webhook ingress,以及到 kube-apiserver、kube-dns 的 egress。

IP 池与 LB IP 分配

唯一的 IPAddressPool 名为 default-pool,地址范围 192.168.88.220-192.168.88.230autoAssign: trueL2Advertisement default 把它以 L2 方式广告出去。

目前在用的 LB IP:

IP用途
192.168.88.220gitea-ssh
192.168.88.221Traefik ingress
192.168.88.222游戏服务器

Webhook 与证书

controller 同时运行一个 ValidatingWebhookConfigurationmetallb-webhook-configuration),对 ipaddresspoolsl2advertisementsbgpadvertisementscommunitiesbfdprofilesbgppeers 等 CREATE/UPDATE 做准入校验,failurePolicy: Fail。webhook 通过命名空间内的 webhook-service(443 → 9443)暴露。

webhook 的 TLS 证书由 cert-manager 自签 签发:命名空间内一个 selfsigned-issuerIssuerselfSigned: {})签出 webhook-server-cert(有效期 1 年,提前 30 天续期),并通过 cert-manager.io/inject-ca-from 注解把 CA 注入到 webhook 配置里。这也是本仓库里 MetalLB 的 controller manifest 带 --webhook-mode=enabled 等定制、与上游原生 manifest 不同的原因。

CRD 只部分在 git

MetalLB 的 CRD 只有一部分在本仓库里。 大部分 CRD 是 2025-11 带外(out-of-band)apply 进集群的,git 里只有 0.16 升级时新增的两个:configurationstates.metallb.ioservicel2statuses.metallb.io(见 crds-v0.16.yaml)。这意味着一次需要新 CRD 的版本升级,不会自动从这个仓库拿到对应的 CRD —— 升级前要单独核对。

升级是破坏性的

MetalLB 的 minor 版本就可能带破坏性变更,不能只 bump 镜像 tag。 0.15 → 0.16 这一跳就同时:① 新增了 configurationstates CRD;② 把健康检查从 metrics 端口挪到了 :17472 上的 /healthz + /readyz;③ 让 controller 必须有 METALLB_POD_NAME(downward API 注入)。

renovate 的 PR #968 当时只 bump 了镜像,结果 controller 崩溃循环(resource name may not be empty,缺 METALLB_POD_NAME),speaker 崩溃循环(no matches for kind "ConfigurationState",缺 CRD),L2 ARP 不稳定。修复 PR #1030 才把上游 config/manifests/metallb-native.yaml 里对应的 CRD + RBAC + 探针 + env 全部移植过来,同时保留本仓库的 webhook/证书定制与 7472 metrics 端口。renovate.json 现已把 metallb 的 minor/major 升级挡在 dashboard 审批之后。

评论