裸金属 K3S 对接华为交换机:MetalLB BGP 打通 LoadBalancer 全记录
环境:K3s 集群· MetalLB v0.16.1(native BGP 模式)· 华为交换机 VRP · 单 AS iBGP(AS 65000)
一、背景
裸金属/自建环境没有云厂商的 LoadBalancer,Service 一旦写成 type: LoadBalancer 就永远卡在 <pending>。业界标准做法是 MetalLB:它只负责把 LB IP「宣告」进网络,真正的转发由 kube-proxy 完成。
本文场景:K3s 集群与华为交换机同处一个 AS(65000,iBGP),需要把 172.23.6.221-172.23.6.230 作为 LoadBalancer IP 池,通过 BGP 宣告给交换机,再由交换机把路由传到客户端所在网段。
二、选型三连
2.1 L2 还是 BGP
| 模式 | 原理 | 代价 |
|---|---|---|
| L2 | speaker 应答 ARP,把流量引到某个节点 | 简单、无需网络配合;但一个 IP 只有一台节点应答,带宽受单节点上行限制 |
| BGP | 与交换机建会话,宣告 /32 路由 |
多节点 ECMP 真分担、故障切换快;需要网络侧配合 |
内网本来就有成熟的 BGP 架构,直接选 BGP,不做额外妥协。
2.2 native 还是 FRR-K8s
| 模式 | 能力边界 |
|---|---|
| native(轻量) | 内置 BGP 实现;不支持 BFD / 优雅重启 / IPv6 / VRF / 多跳 |
| FRR-K8s(官方推荐) | 功能全,支持 BFD 亚秒级故障切换、优雅重启 |
本次先用 native(只要基础 BGP 能力),也因此踩了配置坑(见第四节)。
manifest 三选一:metallb-native.yaml / metallb-frr-k8s.yaml / metallb-frr.yaml(已废弃)。
2.3 eBGP 还是 iBGP
| 维度 | eBGP | iBGP |
|---|---|---|
| AS 号 | 两端不同 | 同一 AS |
| 路由再传递 | 学到就传给其它邻居 | iBGP 学到的路由不再传给其它 iBGP 邻居(防环)→ 需要全互联或 RR |
| next-hop | 默认改成自己 | 默认不改写;转给其它 iBGP 邻居要配 next-hop-local |
| 直连要求 | 默认 TTL=1,跨跳要 ebgp-max-hop |
不要求直连,三层可达即可 |
沿用现有网络的单 AS + RR 设计:MetalLB 侧 myASN = peerASN = 65000。
三、落地配置
3.1 MetalLB 侧
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata: { name: prod-pool, namespace: metallb-system }
spec:
addresses:
- 172.23.6.221-172.23.6.230
---
apiVersion: metallb.io/v1beta2 # 注意 v1beta2,v1beta1 已弃用
kind: BGPPeer
metadata:
name: huawei-csw
namespace: metallb-system
spec:
myASN: 65000 # 与交换机同 AS → iBGP
peerASN: 65000
peerAddress: 172.23.6.1 # 交换机面向 K8s 的对等接口地址(不是 router-id!)
peerPort: 179
holdTime: 9s # native 模式下 keepalive 由它 ÷3 推导
passwordSecret:
name: bgp-md5 # 对象形式,不是字符串
---
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata: { name: prod, namespace: metallb-system }
spec:
ipAddressPools: [prod-pool]
peers: [huawei-csw]
MD5 认证的 Secret(三个硬要求:类型、同 namespace、key 必须是 password):
kubectl -n metallb-system create secret generic bgp-md5 \
--type=kubernetes.io/basic-auth \
--from-literal=password='你的BGP口令'
3.2 华为交换机侧(VRP)
bgp 65000
router-id 172.23.5.1
graceful-restart
group To-K3S internal
peer To-K3S description To-JLD-K3S-Cluster-Group
peer To-K3S password cipher %^%#********%^%# # 与 MetalLB 侧口令一致
peer 172.23.5.51 as-number 65000
peer 172.23.5.51 group To-K3S
peer 172.23.5.52 as-number 65000
peer 172.23.5.52 group To-K3S
#
ipv4-family unicast
undo synchronization
peer To-K3S enable
peer 172.23.5.51 enable
peer 172.23.5.51 group To-K3S
peer 172.23.5.52 enable
peer 172.23.5.52 group To-K3S
要点:
password cipher后跟明文,设备自动加密保存(display current-configuration显示成%^%#...%^%#)- 组内两个邻居共用一条 peer-group 配置,MetalLB 侧一条 BGPPeer 即可(它在所有 speaker 节点生效)
- 建议补
peer To-K3S connect-interface VlanifXXX,把会话源地址钉住,避免接口变动后会话抖动
四、踩坑记录:四个报错,四个原因
坑 1:passwordSecret 类型不对
spec.passwordSecret: Invalid value: "string": spec.passwordSecret in body must be of type object: "string"
passwordSecret 是对象(Secret 引用),不是字符串:
passwordSecret:
name: bgp-md5 # 只填 name;namespace 默认与 MetalLB 同空间
对应的 Secret 还必须是 kubernetes.io/basic-auth 类型、里面存 password 键——CRD 注释里写得很清楚,但容易漏。
坑 2:native 模式不支持 keepaliveTime
admission webhook "bgppeersvalidationwebhook.metallb.io" denied the request:
peer 172.23.6.1 has keepalive-time set on native bgp mode
native 模式的「FRR 专属」字段黑名单(源码 internal/config/validation.go):
| 层级 | 不可用项 |
|---|---|
| BGPPeer | bfdProfile、keepaliveTime、connectTime、vrf、interface、localASN、dynamicASN、enableGracefulRestart、disableMP、dualStackAddressFamily |
| 全局 | 存在任何 BFDProfile CR、被宣告的池含 IPv6、非 legacy 类型的 community |
为什么只拦 keepalive 不拦 hold? 看 native 实现(internal/bgp/native/native.go):
// native mode does not support empty holdtime, we explicitly set it to 90s
if args.HoldTime == nil { ht := 90 * time.Second }
...
t = time.NewTicker(ht / 3) // keepalive = 协商出的 holdTime / 3
也就是说 native 模式下 keepalive 是推导出来的,只给 holdTime: 9s 就等价于「9 秒检测、3 秒保活」。
坑 3:dial "x.x.x.x": timeout
{"caller":"native.go:115","error":"dial \"172.23.8.1\": timeout","level":"error",
"localASN":65000,"msg":"failed to connect to peer","op":"connect","peer":"172.23.8.1","peerASN":65000}
timeout(不是 refused、不是 unreachable)= SYN 发出去了,对方没回。三个原因,按概率排:
peerAddress填成了交换机的 router-id。router-id 不等于对等接口地址,两端说的不是同一对地址,会话自然建不起来。- MD5 两端不一致:接收方会静默丢弃签名不对的 SYN,发送方只表现为 timeout。
- 节点 firewalld 拦了入向 179(交换机是主动连节点的):
firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=<交换机对等地址> port port=179 protocol=tcp accept'
firewall-cmd --reload
排查用(按顺序):
ip route get <交换机地址> # 看走哪个接口、src 是不是对的
nc -zv -w3 <交换机地址> 179 # 通不通
tcpdump -i any -nn 'tcp port 179' -c 20 # 看 SYN 有没有 SYN-ACK 回来
交换机侧对照:
display current-configuration configuration bgp | include password
display bgp peer
display logbuffer | include -i "auth" # 认证失败会留痕
坑 4:业务正常,但 ping 不通
官方 troubleshooting 文档原话:
"please be aware that pinging the service IP won't work. You must access the service to ensure that it works."
原理:
| 环节 | 谁负责 | 覆盖协议 |
|---|---|---|
| IP 宣告(BGP/ARP) | MetalLB speaker | — |
| 转发到 Pod | kube-proxy 的 NAT | 只处理 TCP / UDP / SCTP |
| ICMP | 没人 | LB IP 不落在任何节点网卡上,无人应答 |
所以 ICMP 不通属于实现特性,不是故障。要「能 ping 的 VIP」只能换实现:keepalived/VRRP(VIP 真实落在主机上)或前置硬件 LB/nginx(VIP 落在前置设备上,后端接 NodePort)。监控平台一律改 TCP 端口探针 / HTTP 探针,否则天天误报。
另一个相关坑:在节点上 curl 自己的 LB IP 通了,不代表 MetalLB 正常(那只证明 CNI 在工作)。验证一律从客户端侧打真实业务。
五、验证清单
# MetalLB 侧
kubectl -n metallb-system get ipaddresspool,bgppeer,bgpadvertisement
kubectl -n metallb-system logs -l component=speaker --tail=80 | grep -iE "established|error"
# 业务
kubectl create deploy web --image=nginx --replicas=2
kubectl expose deploy web --type=LoadBalancer --port=80
kubectl get svc web -w # 拿到 EXTERNAL-IP
curl -s -o /dev/null -w '%{http_code}\n' http://<EXTERNAL-IP>
# 交换机侧
display bgp peer # 应为 Established,Uptime 正常
display bgp routing-table <LB-IP> # 应看到 /32 路由
display ip routing-table <LB-IP> # 进了 FIB 才算数
六、生产建议
- holdTime / BFD 取舍:native 只支持 holdTime(最小 3s);要亚秒级切换必须上 FRR-K8s + BFD。
- 交换机开了
graceful-restart,native 模式接不住:MetalLB native 不支持 GR 能力协商,speaker 重启时路由会被立刻撤回(两节点场景流量切到另一台,可接受;要求「重启不折腾路由」就必须换 FRR-K8s 并打开enableGracefulRestart)。 - IP 池规划:别和节点 IP、DHCP 池、网关重叠;老设备多的环境开
avoidBuggyIPs: true;需要对外聚合宣告时用aggregationLength(前提是池内地址连续)。 - 路由过滤:交换机侧配
route-policy只收 LB 网段,别让 K8s 侧任何意外宣告扩散。 - 回程路由:客户端在别的网段(如 172.23.10.0/24)时,ToR 学到还不够——要么重分发进 OSPF/ISIS,要么让核心直接与节点建会话,否则客户端侧无回程。
- 安全:MD5 口令两端一致、用 Secret 而非明文;配置导出/博客截图里记得把
%^%#...#%^%#密文抹掉。
七、小结
整套东西拆开看就是三件事:MetalLB 负责宣告、kube-proxy 负责转发、交换机负责把路由带出去。九成的「会话起不来」是地址或口令没对齐;九成的「宣告了但不通」是回程路由没传到位;而「ping 不通」压根不用修。
排查这类问题,顺序永远比技巧重要:先看会话状态(是否有 Established),再看路由表(是否进 FIB),最后才怀疑应用。
写在最后:网络里的每一层都只负责自己那一段路,谁都不越界,链路才通。这也算是运维版的「各安其位」——设备如此,人也如此。