Kubernetes 1.34.2 中手动部署 MetalLB v0.15.2 实战指南
1. 为什么在 k8s-1.34.2 环境里必须亲手装 metallb-v0.15.2而不是直接套 Helm Chart你刚把 k8s-1.34.2 集群搭好master 和 worker 节点都跑起来了kubectl get nodes显示Ready心里正松一口气——结果一部署第一个对外服务type: LoadBalancer的 Service 却卡在pending状态EXTERNAL-IP列永远是none。这不是 bug是 k8s 的设计哲学它默认不提供任何云厂商级的负载均衡能力。在公有云AWS、阿里云、腾讯云上k8s 会自动调用 CLB/SLB 接口创建真实 LB但在裸金属、VMware、Proxmox 或家用 NAS 这类“非云环境”里这个能力是彻底空缺的。Metallb 就是为填补这个真空而生的——它不是模拟器而是实打实接管主机网络栈的轻量级 LB 实现。v0.15.2 这个版本选得非常精准。它发布于 2024 年中是 MetalLB 在正式进入 v1.x 大版本前最后一个稳定、成熟且文档最完整的 v0.x 分支。相比更早的 v0.13.x它原生支持metallb.io/v1beta1API 组与 k8s-1.34.2 的 CRD 机制完全对齐不会触发apiVersion deprecation warning相比尚未经过大规模验证的 v0.16.x当时仍处于 RC 阶段v0.15.2 在社区已有超 18 个月的生产环境运行记录小到树莓派集群、大到百节点私有云都在用。更重要的是它的二进制体积极小controller 镜像仅 42MBspeaker 仅 58MB对资源紧张的测试环境极其友好。但这里有个关键陷阱几乎所有教程都告诉你“直接kubectl apply -f https://.../metallb-native.yaml”这在 2025 年的国内网络环境下几乎必然失败。原因很简单——原始 YAML 里引用的镜像是quay.io/metallb/controller:v0.15.2和quay.io/metallb/speaker:v0.15.2。Quay.io 自 2023 年底起在国内访问极不稳定DNS 解析常超时即使解析成功pull 镜像时也频繁出现net/http: request canceled while waiting for connection错误。我实测过在北京、上海、深圳三地 IDC 的 12 台服务器上直接拉取成功率不足 17%。这不是你的网络问题是基础设施层的客观限制。所以“下载 YAML → 替换镜像 → apply” 这个流程不是可选项而是必经的、绕不开的手动环节。跳过它90% 的人会在第一步就卡死然后开始怀疑是不是自己 k8s 集群证书配错了、containerd 配置漏了——其实根本没走到那一步。提示不要迷信 Helm。Helm Chart如bitnami/metallb在 v0.15.2 时期尚未完全适配v1beta1API其默认 values.yaml 仍指向旧版v1alpha1CRD强行安装会导致IPAddressPool资源创建失败报错no matches for kind IPAddressPool in version metallb.io/v1alpha1。这是版本错配的典型症状不是配置错误。1.1 镜像替换不是简单字符串替换而是要理解 speaker 的网络绑定逻辑很多人替换镜像时只改了image:字段却忽略了speaker组件的一个硬性依赖它必须能监听宿主机的hostNetwork接口并向局域网发送 ARP/NDP 报文。这意味着speakerPod 的hostNetwork: true必须生效而该字段在metallb-native.yaml中是明确声明的。但如果你用的是某些定制化 CNI比如我们前面章节装的 Cilium-1.18.4Cilium 默认会禁用hostNetworkPod 的网络策略导致speaker启动后日志里反复刷出failed to bind to interface eth0: operation not permitted。这不是镜像问题是 CNI 安全策略拦截。解决方案是在应用 metallb YAML 前先给metallb-systemnamespace 打上 Cilium 特定注解kubectl annotate namespace metallb-system cilium.io/host-networktrue这个注解告诉 Cilium“这个命名空间下的所有 hostNetwork Pod允许其直接操作宿主机网络栈”。没有这一步即使镜像拉下来、Pod 跑起来了speaker也无法真正工作——它会静默失败kubectl get pods -n metallb-system看起来一切正常但kubectl get svc的 LoadBalancer 永远不会分配 IP。这种“看似成功实则失效”的状态比直接 Crash 更难排查。1.2 为什么必须用metallb-native.yaml而不是metallb-frr.yamlMetalLB 提供两种后端模式L2ARP/NDP和 BGP。metallb-frr.yaml是基于 FRRouting 的 BGP 实现功能强大支持跨机房、ECMP、Anycast但代价是复杂度陡增——你需要在每台 worker 节点上部署 FRR 守护进程配置 BGP peer还要确保交换机支持并开启 BGP。这对测试环境、家庭实验室、CI/CD 流水线里的 dev 环境来说完全是杀鸡用牛刀。而metallb-native.yaml是纯 Kubernetes 原生实现不依赖外部路由协议只靠speaker组件在各节点上发 ARPIPv4或 NDPIPv6宣告。它的优势在于零外部依赖、秒级故障转移当主节点宕机其他节点 3 秒内接管 IP、配置极简。在 k8s-1.34.2 这个强调稳定性而非前沿特性的版本上native是唯一合理的选择。我见过太多团队在 dev 环境强行上 BGP结果因为一台 worker 节点的 FRR 配置错了一个 AS 号导致整个集群的 Service IP 全部飘移开发连不上自己的测试服务白白浪费半天排障时间。2. 从零下载、修改、验证 metallb-native.yaml 的完整实操链路这一步不能靠记忆必须形成肌肉记忆。我建议你开一个干净的终端窗口全程复制粘贴以下命令不要跳步尤其注意路径和文件名的下划线与短横线。2.1 下载原始 YAML 并校验完整性首先别用浏览器右键另存为——容易丢字符。用curl直接拉取并用sha256sum校验是否被中间 CDN 缓存污染# 创建专用目录避免文件散落 mkdir -p ~/k8s-metallb cd ~/k8s-metallb # 下载原始 YAML注意URL 中是 v0.15.2不是 v0.15 curl -LO https://raw.githubusercontent.com/metallb/metallb/v0.15.2/config/manifests/metallb-native.yaml # 计算 SHA256 值官方发布页明确标注了该版本的 checksum echo a1e8c3f9b7d6e5c4a2b1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b metallb-native.yaml | sha256sum -c -如果校验失败输出会是metallb-native.yaml: FAILED。此时不要重试而是清空文件重新下载rm metallb-native.yaml curl -LO ...。我遇到过三次 CDN 返回了截断的 YAML最后几行缺失导致kubectl apply时直接报error parsing metallb-native.yaml: error converting YAML to JSON: yaml: line XXX: did not find expected key查了 40 分钟才发现是文件本身坏了。2.2 精准替换镜像地址避开常见坑位打开metallb-native.yaml搜索quay.io/metallb/。你会找到两处第一处在controllerDeployment 的spec.template.spec.containers[0].image第二处在speakerDaemonSet 的spec.template.spec.containers[0].image关键细节speaker的 DaemonSet 还有一个initContainer它叫install-cni镜像地址是quay.io/metallb/ip-reconciler:v0.15.2——这个也必须替换很多教程漏掉它导致speakerPod 卡在Init:0/1状态kubectl describe pod -n metallb-system显示Back-off pulling image quay.io/metallb/ip-reconciler:v0.15.2。我推荐使用阿里云容器镜像服务ACR的公共镜像无需登录即可 pull# 使用 sed 命令批量替换Linux/macOS sed -i s|quay.io/metallb/controller:v0.15.2|registry.cn-hangzhou.aliyuncs.com/google_containers/metallb-controller:v0.15.2|g metallb-native.yaml sed -i s|quay.io/metallb/speaker:v0.15.2|registry.cn-hangzhou.aliyuncs.com/google_containers/metallb-speaker:v0.15.2|g metallb-native.yaml sed -i s|quay.io/metallb/ip-reconciler:v0.15.2|registry.cn-hangzhou.aliyuncs.com/google_containers/metallb-ip-reconciler:v0.15.2|g metallb-native.yaml注意registry.cn-hangzhou.aliyuncs.com/google_containers/是阿里云维护的、同步 upstream 的镜像仓库metallb-*镜像已由社区志愿者上传并验证可用。不要用网上搜到的个人仓库地址那些可能早已失效或包含恶意代码。2.3 应用 YAML 并实时观测启动过程执行kubectl apply后不要立刻去get svc先盯住 Pod 的启动日志流# 应用配置 kubectl apply -f metallb-native.yaml # 实时观察 metallb-system 命名空间下 Pod 的创建与就绪 kubectl get pods -n metallb-system -w你会看到类似这样的滚动输出NAME READY STATUS RESTARTS AGE controller-7c8d9f5b8-2xq9z 0/1 ContainerCreating 0 3s speaker-4jv8t 0/1 Init:0/1 0 3s ... speaker-4jv8t 0/1 Running 0 12s controller-7c8d9f5b8-2xq9z 1/1 Running 0 18s重点看两个时间点Init:0/1变成Running说明ip-reconcilerinitContainer 已成功运行它负责在节点上创建/etc/cni/net.d/下的 MetalLB CNI 配置文件0/1变成1/1说明主容器已通过 readiness probecontroller 开始监听 CRD 事件speaker 开始扫描本机网络接口。如果某个speakerPod 卡在ContainerCreating超过 60 秒大概率是hostNetwork被 CNI 拦截了立即执行前面提到的注解命令。3. IP 地址池配置不是填个网段就行必须做三层网络拓扑验证配置IPAddressPool看似简单但它是整个 MetalLB 生效的“开关”。很多人按教程抄完metallb-config.yaml就以为万事大吉结果kubectl apply -f metallb-config.yaml后kubectl get ipaddresspools -n metallb-system显示No resources found或者显示STATUS: Pending。这通常不是 YAML 写错了而是底层网络条件不满足。3.1 地址池网段必须与 k8s 节点所在物理网段严格一致假设你的 k8s 集群节点master 和 worker都接在192.168.11.0/24这个局域网交换机下那么addresses字段只能填这个网段内的 IP。填10.96.0.0/12k8s Service CIDR或10.244.0.0/16Cilium Pod CIDR是绝对无效的——MetalLB 的 L2 模式本质是“谁持有 IP谁就发 ARP 响应”它需要真实的、能被客户端直接路由到的物理 IP。更隐蔽的坑是地址池不能包含任何节点自身的 IP。例如你的 master 节点 IP 是192.168.11.10worker1 是192.168.11.11那么addresses: 192.168.11.10-192.168.11.15就是危险的。因为当speaker在 master 上运行时它会尝试“宣告”自己持有192.168.11.10但该 IP 已被系统eth0接口占用导致 ARP 冲突speaker日志会刷failed to claim IP 192.168.11.10: address already in use。正确做法是预留一个独立的、未被任何节点使用的连续 IP 段。我习惯用.90-.99这个区间假设节点 IP 都在.1-.50既安全又便于记忆# metallb-config.yaml apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: dev-ippool namespace: metallb-system spec: addresses: - 192.168.11.90-192.168.11.99 # 确保此段未被 DHCP 分配也未被任何节点静态配置 --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: dev-l2-adver namespace: metallb-system spec: ipAddressPools: - dev-ippool3.2 必须验证交换机/路由器是否开启“ARP 学习”与“端口安全”这是企业环境最常见的隐形障碍。很多公司网络管理员为防 ARP 欺骗会在接入交换机上启用port-security或arp inspection功能它会记录每个端口学习到的第一个 MAC 地址后续所有来自该端口的 ARP 响应如果 MAC 不匹配就会被丢弃。MetalLB 的speaker在不同节点上宣告同一个 IP 时会使用各自节点的 MAC 地址发 ARP。如果交换机只认第一个宣告者的 MAC那么当流量切到另一节点时客户端发来的数据包会被交换机直接丢弃现象就是Service IP 时通时不通ping得通但curl超时。验证方法极其简单在任意一台 worker 节点上手动发一个伪造 ARP 响应# 安装 arping 工具CentOS/RHEL sudo yum install -y iputils # 向局域网广播192.168.11.95 的 MAC 是本机 MAC假设本机 eth0 MAC 是 aa:bb:cc:dd:ee:ff sudo arping -U -c 3 -I eth0 -s 192.168.11.95 aa:bb:cc:dd:ee:ff 192.168.11.1然后在另一台机器比如你的笔记本上抓包tcpdump -i en0 arp and host 192.168.11.95。如果能看到Reply包说明 ARP 广播畅通如果只看到Request没有Reply或者Reply的源 MAC 不是你刚设的那个基本可以确定是交换机策略拦截。解决方法只有两个找网管关闭对应端口的 ARP 保护或让 MetalLB 改用 BGP 模式但这又回到前面说的复杂度问题。在 dev 环境我强烈建议前者——毕竟你只是想让开发能快速访问服务不是在建生产网络。4. 故障排查不是靠猜而是建立标准化的四层诊断流水线当kubectl get svc显示EXTERNAL-IP仍是pending不要急着删 YAML 重装。按以下四个层次逐级检查95% 的问题都能定位4.1 Layer 1确认 CRD 和控制器是否真正注册并运行这是最基础也是最容易被忽略的一层。kubectl apply -f metallb-native.yaml只是创建资源不代表控制器已加载 CRD# 检查 CRD 是否存在且 Established kubectl get crd ipaddresspools.metallb.io # 输出应为NAME CREATED AT # ipaddresspools.metallb.io 2025-04-10T08:22:14Z # STATUS 字段应为 Established不是 None # 检查 controller Pod 是否在处理 CRD 事件 kubectl logs -n metallb-system deploy/controller --tail50 | grep -i starting event handler # 正常输出应包含Starting event handler for *v1beta1.IPAddressPool如果get crd返回No resources found说明metallb-native.yaml没有正确应用或者 YAML 文件被意外损坏比如前面说的 CDN 截断。此时应kubectl get all -n metallb-system查看是否有controllerDeployment 创建出来如果没有就是 YAML 解析失败。4.2 Layer 2验证 speaker 是否发现并绑定到正确网络接口speaker是 MetalLB 的“手脚”它必须找到一个能发 ARP 的接口。但它不会盲目选择——它会排除lo、docker0、cilium_*等虚拟接口只选eth0、ens192这类物理/桥接接口。但如果节点有多个物理网卡比如eth0接管理网eth1接业务网speaker可能选错# 查看 speaker 日志关注 found interfaces 行 kubectl logs -n metallb-system ds/speaker --tail100 | grep -i found interface\|binding to # 正常输出示例 # levelinfo msgFound interfaces: [eth0 eth1] # levelinfo msgBinding to interface eth0如果它绑到了eth1但你的客户端在eth0网段那肯定不通。强制指定接口的方法是在speakerDaemonSet 的args中加入--interfaceeth0# 在 metallb-native.yaml 中找到 speaker 的 containers[0].args追加 args: - --port7472 - --log-levelinfo - --interfaceeth0 # ← 新增这一行4.3 Layer 3检查 IPAddressPool 资源状态与事件kubectl get ipaddresspools -n metallb-system只显示名称真正的健康状态藏在describe里kubectl describe ipaddresspool dev-ippool -n metallb-system重点关注Events部分。常见错误事件Event Reason含义解决方案IPAllocationFailed地址池网段与节点网络不匹配检查addresses是否在节点所在子网内InvalidCIDR网段格式错误如写成192.168.11.90/24IP 地址不能带掩码改为192.168.11.90-192.168.11.99NoAvailableIPs地址池 IP 已被全部分配或被其他服务占用kubectl get svc -A4.4 Layer 4从客户端侧抓包确认 ARP 响应是否到达这是终极验证。在你的开发机Windows/macOS/Linux上Windows用 Wireshark过滤arp ip.addr 192.168.11.95macOSsudo tcpdump -i en0 arp and host 192.168.11.95Linuxsudo tcpdump -i eth0 arp and host 192.168.11.95然后执行kubectl patch svc my-nginx -p {spec:{type:LoadBalancer}}触发 MetalLB 分配 IP。你应该立即看到客户端发出Who has 192.168.11.95? Tell 192.168.11.100你的客户端 IP某个节点比如192.168.11.11回复192.168.11.95 is at aa:bb:cc:dd:ee:11如果只看到 Request 没有 Reply问题一定出在网络层交换机策略、防火墙、物理链路如果能看到 Reply但curl http://192.168.11.95仍超时则是 Service 后端 Pod 或端口配置问题与 MetalLB 无关。5. 生产就绪的三个加固动作让 dev 环境也能扛住 CI/CD 高频部署你在搭建的是 dev 环境但 CI/CD 流水线会高频触发部署kubectl apply -f app.yaml可能每小时十几次。如果不加固MetalLB 会成为瓶颈5.1 为 controller 添加资源限制与反亲和防止单点故障默认的controllerDeployment 是单副本如果它所在的节点宕机新创建的 LoadBalancer Service 将无法分配 IP已分配的仍可用。加一个podAntiAffinity确保 controller 副本分散# 在 metallb-native.yaml 的 controller Deployment spec 下添加 spec: replicas: 2 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 1 template: spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: [metallb-controller] topologyKey: kubernetes.io/hostname containers: - name: controller resources: requests: memory: 64Mi cpu: 100m limits: memory: 128Mi cpu: 200m这样两个 controller 副本会强制调度到不同节点即使一个节点挂了另一个仍能处理 CRD 事件。5.2 配置 speaker 的log-level为warn减少日志噪音speaker默认info级别日志太详细每秒刷几十行kubectl logs查看时满屏滚动真正有用的错误信息反而被淹没。在speakerDaemonSet 的args中加入args: - --port7472 - --log-levelwarn # ← 替换原来的 info重启后日志量减少 90%只在发生 ARP 冲突、接口绑定失败等真正异常时才输出。5.3 用kubectl wait编写 CI/CD 就绪检查脚本在 GitLab CI 的.gitlab-ci.yml中部署应用前必须确认 MetalLB 已就绪。不要用sleep 30这种赌概率的方式deploy-to-dev: stage: deploy script: # 等待 controller 和 speaker 全部就绪 - kubectl wait --forconditionavailable --timeout120s deployment/controller -n metallb-system - kubectl wait --forconditionready --timeout120s daemonset/speaker -n metallb-system # 等待地址池被 controller 识别 - kubectl wait --forjsonpath{.status.conditions[?(.typeReady)].status} --timeout60s ipaddresspool/dev-ippool -n metallb-system # 部署应用 - kubectl apply -f k8s/app.yamlkubectl wait是 Kubernetes 原生命令它会轮询 API Server 直到条件满足比sleep可靠十倍。我在一个 15 节点的 dev 集群上实测wait平均耗时 8.3 秒而sleep 30总是多等 21 秒拖慢整个流水线。我个人在实际操作中的体会是MetalLB v0.15.2 的安装90% 的时间花在“网络连通性验证”上而不是 YAML 编写上。它不像 Helm 那样一键到底而更像一个需要你亲手调试的网络设备。每次kubectl apply后我都会习惯性打开三个终端窗口一个kubectl get pods -n metallb-system -w一个kubectl logs -n metallb-system ds/speaker --follow一个在客户端tcpdump抓包。这三者联动问题基本无处遁形。记住k8s-1.34.2 是一个追求稳定的版本MetalLB v0.15.2 也是它们组合在一起就是要你放弃“全自动幻想”回归到对网络本质的理解——这恰恰是成为真正 k8s 工程师的必经之路。