8 min read

裸金属 K3S 对接华为交换机:MetalLB BGP 打通 LoadBalancer 全记录

裸金属 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 bfdProfilekeepaliveTimeconnectTimevrfinterfacelocalASNdynamicASNenableGracefulRestartdisableMPdualStackAddressFamily
全局 存在任何 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 发出去了,对方没回。三个原因,按概率排:

  1. peerAddress 填成了交换机的 router-id。router-id 不等于对等接口地址,两端说的不是同一对地址,会话自然建不起来。
  2. MD5 两端不一致:接收方会静默丢弃签名不对的 SYN,发送方只表现为 timeout。
  3. 节点 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 才算数

六、生产建议

  1. holdTime / BFD 取舍:native 只支持 holdTime(最小 3s);要亚秒级切换必须上 FRR-K8s + BFD
  2. 交换机开了 graceful-restart,native 模式接不住:MetalLB native 不支持 GR 能力协商,speaker 重启时路由会被立刻撤回(两节点场景流量切到另一台,可接受;要求「重启不折腾路由」就必须换 FRR-K8s 并打开 enableGracefulRestart)。
  3. IP 池规划:别和节点 IP、DHCP 池、网关重叠;老设备多的环境开 avoidBuggyIPs: true;需要对外聚合宣告时用 aggregationLength(前提是池内地址连续)。
  4. 路由过滤:交换机侧配 route-policy 只收 LB 网段,别让 K8s 侧任何意外宣告扩散。
  5. 回程路由:客户端在别的网段(如 172.23.10.0/24)时,ToR 学到还不够——要么重分发进 OSPF/ISIS,要么让核心直接与节点建会话,否则客户端侧无回程。
  6. 安全:MD5 口令两端一致、用 Secret 而非明文;配置导出/博客截图里记得把 %^%#...#%^%# 密文抹掉。

七、小结

整套东西拆开看就是三件事:MetalLB 负责宣告、kube-proxy 负责转发、交换机负责把路由带出去。九成的「会话起不来」是地址或口令没对齐;九成的「宣告了但不通」是回程路由没传到位;而「ping 不通」压根不用修。

排查这类问题,顺序永远比技巧重要:先看会话状态(是否有 Established),再看路由表(是否进 FIB),最后才怀疑应用。


写在最后:网络里的每一层都只负责自己那一段路,谁都不越界,链路才通。这也算是运维版的「各安其位」——设备如此,人也如此。