K3s 集群 CNI 迁移实战:从 Flannel 到 Cilium 的踩坑记录
目录
背景#
我的 homelab 运行着一个单节点 K3s 集群(Proxmox VM 上的 Ubuntu),通过 Cloudflare Tunnel 对外提供十几个服务。之前一直用 K3s 默认的 Flannel (VXLAN) 作为 CNI,一切安好。
出于对 eBPF 可观测性(Hubble)和更强 Network Policy 能力的兴趣,我决定将 CNI 替换为 Cilium。迁移本身不复杂——需要重装 K3s(禁用 Flannel 和内置 Network Policy)、安装 Cilium Helm chart、然后按依赖顺序重部署所有工作负载。
迁移过程在 安装计划文档 中有详细记录。本文聚焦于迁移完成后遇到的三个意外问题,以及每个问题的排查过程。
Cilium 配置概览#
先记录一下我最终使用的 Cilium 配置,作为后续排查的上下文:
# cilium-values.yaml 关键配置
kubeProxyReplacement: false # 保留 K3s kube-proxy
routingMode: tunnel # VXLAN 隧道模式
tunnelProtocol: vxlan
ipam:
operator:
clusterPoolIPv4PodCIDRList: ["10.42.0.0/16"]
# 保留 Traefik 作为 Gateway,不用 Cilium Gateway API
gatewayAPI:
enabled: false
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
关键点是 routingMode: tunnel——这意味着 Pod 间的流量通过 VXLAN 封装,Cilium 的 TC BPF 程序挂在每个 veth 上处理入/出站包。后面会看到,这个模式对 Pod→Host 流量有微妙的影响。
对应的 cilium-dbg status --verbose 输出:
Routing: Network: Tunnel [vxlan] Host: Legacy
Masquerading: IPTables [IPv4: Enabled]
KubeProxyReplacement: False
问题 1:Cloudflared QUIC 握手超时#
现象#
迁移完成后,ArgoCD 自动同步的 cloudflared Deployment 创建了 2 个新 Pod,但都进入了 CrashLoopBackOff:
failed to dial to edge with quic: timeout: handshake did not complete in time
只有一个迁移前的旧 Pod 还活着(它的 QUIC 连接是在 Flannel 时代建立的),整个 homelab 的外部流量全靠这一个 Pod 撑着。
排查#
- 确认日志:新 Pod 的日志明确显示 QUIC handshake 超时,说明 UDP/443 到 Cloudflare Edge 的连接有问题。
- 理解 QUIC 在 Cilium 下的路径:Cloudflared 默认使用 QUIC (UDP/443) 连接 Cloudflare Edge。在 Cilium VXLAN 模式下,出站 UDP 流量需要经过 TC BPF → VXLAN 封装 → IPTables MASQUERADE → 物理网卡。
- MTU 和封装开销:VXLAN 封装会增加 50 字节开销,Cilium 设置
cilium_host的 MTU 为 1280。QUIC 本身也有封装层。双重封装可能导致 MTU 问题或者 masquerade 路径上的兼容性问题。
解决方案#
强制 cloudflared 使用 HTTP/2 替代 QUIC:
# cloudflare-tunnel.yaml
containers:
- name: cloudflared
args:
- tunnel
- --protocol
- http2 # 关键:避免 QUIC 在 Cilium VXLAN 下的兼容性问题
- run
HTTP/2 走 TCP,在 Cilium 的网络路径中没有任何问题。修改提交后 ArgoCD 同步,2/2 新 Pod 立即建立了到多个 Cloudflare Edge 的连接。
教训#
Cilium VXLAN 隧道模式下,QUIC 等依赖 UDP 的协议可能出现兼容性问题。在切换 CNI 前,检查集群中是否有服务依赖 UDP 传输(QUIC、gRPC over QUIC 等),提前做好协议降级方案。
问题 2:Pod 无法访问节点物理 IP#
现象#
metrics-server 持续报错,无法抓取 kubelet 指标:
Failed to scrape node: Get "https://10.10.10.10:10250/metrics/resource":
dial tcp 10.10.10.10:10250: connect: connection refused
kubectl top nodes 和 kubectl top pods 完全不可用。
排查(深度诊断)#
这个问题的排查过程比较曲折,因为表面上看是 “connection refused”,但实际原因出乎意料。
Step 1:确认 kubelet 在监听
SSH 到节点确认 kubelet 绑定在所有接口:
$ ss -tlnp sport = :10250
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 4096 *:10250 *:* users:(("kubelet",pid=...))
没问题,kubelet 在 *:10250 上监听。
Step 2:hostNetwork Pod 能连,普通 Pod 不能
# hostNetwork: true 的测试 Pod → 成功 (HTTP 401)
$ kubectl run test --image=curlimages/curl --overrides='{"spec":{"hostNetwork":true}}' \
--rm -it -- curl -ksSo /dev/null -w '%{http_code}' https://10.10.10.10:10250/healthz
401
# 普通 Pod → 失败
$ kubectl run test --image=curlimages/curl --rm -it \
-- curl -ksSo /dev/null -w '%{http_code}' https://10.10.10.10:10250/healthz
curl: (7) Failed to connect to 10.10.10.10 port 10250: Could not connect
结论:不是 kubelet 的问题,是 Pod 网络到 Host 网络的路径有问题。
Step 3:不只是 10250,ANY 端口都不通
普通 Pod 连接 10.10.10.10 的 6443 (API Server)、9100 (node-exporter)、10250 (kubelet) 全部失败。但 Pod 连接 Cilium 内部 IP 10.42.0.219:10250 却成功了(HTTP 401)。
这说明问题跟端口无关,是 Cilium 对目标为节点物理 IP 的流量处理有问题。
Step 4:tcpdump 定位 BPF 拦截
在节点上同时监听 any, cilium_host, lo, eth0 接口:
# 只在 veth (lxc*) 上看到 SYN 包进入
03:34:51 lxcd105... In IP 10.42.0.209.48274 > 10.10.10.10.10250: Flags [S]
# cilium_host, lo, eth0 上完全没有流量
# 0 packets captured on cilium_host
# 0 packets captured on eth0
# lo 上只有 127.0.0.1 流量
关键发现:SYN 包到达了 veth 接口(Pod 侧),但从未到达 cilium_host、lo 或 eth0。Cilium 的 TC BPF 程序在 veth 上把包"吃掉了"。
Step 5:排除防火墙干扰
UFW 规则完全正确——Pod CIDR 10.42.0.0/16 允许所有流量,10250 端口对所有来源开放。问题发生在 UFW (iptables) 之前的 BPF 层。
Step 6:理解根因
为什么 Pod→API Server (10.10.10.10:6443) 能正常工作?因为 Pod 访问的是 kubernetes.default.svc ClusterIP (10.43.0.1),走的是 Service NAT 路径,由 kube-proxy 处理。而 metrics-server 使用 --kubelet-preferred-address-types=InternalIP 直接连物理 IP 10.10.10.10。
在 Cilium routingMode: tunnel + Host: Legacy 配置下,TC BPF 程序无法正确地将 Pod 到节点物理 IP 的流量投递到宿主网络栈。流量指向了 local 路由表(ip route get 10.10.10.10 from 10.42.0.209 返回 local 10.10.10.10 dev lo),但 BPF 程序没有沿着这条路径把包送出去。
解决方案#
修改 K3s 内置的 metrics-server 部署,添加 hostNetwork: true 并更换端口避免与 kubelet 冲突:
# /var/lib/rancher/k3s/server/manifests/metrics-server/metrics-server-deployment.yaml
spec:
template:
spec:
hostNetwork: true # 使用宿主网络,绕过 Cilium BPF
containers:
- args:
- --secure-port=10251 # 改 10251,避免和 kubelet 10250 冲突
ports:
- containerPort: 10251
# 别忘了开放新端口
sudo ufw allow 10251/tcp comment 'metrics-server'
K3s 自动检测 manifest 变更并重新部署。新 Pod IP 变成 10.10.10.10(hostNetwork),kubectl top nodes 恢复正常:
NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
k8s-node 397m 9% 6416Mi 42%
教训#
Cilium tunnel 模式下,Pod→Host 的物理 IP 流量可能被 BPF 静默拦截。如果集群中有组件需要通过节点的 InternalIP 直连节点上的服务(metrics-server、kubelet healthcheck、node-exporter scrape 等),考虑使用
hostNetwork: true或者调整 Cilium 的hostServices配置。
问题 3:ZITADEL Master Key 长度错误#
现象#
ZITADEL(SSO 身份提供商)的 setup Job 持续失败:
masterkey must be 32 bytes, but is 44
SSO 不可用导致所有受保护服务(Calibre-Web、Grafana、Vault UI、Gotify)的登录链条断裂。
原因#
这个问题和 Cilium 无关,是在上一次会话修复 Vault 密钥时引入的。Vault 中写入了 44 字符的 master key,而 ZITADEL 要求恰好 32 字节。
修复#
# 截断为 32 字符
vault kv put secret/homelab/zitadel \
master-key=6ac56c3da96d43a0b3bb631193e16fe4 \
db-password=57226c513b294a1591f51e9d82a7ffd9
# 强制 ESO 重新同步
kubectl annotate externalsecret zitadel-masterkey -n zitadel \
force-sync=$(date +%s)
# 删除失败的 helm-install Job,K3s 自动重建
kubectl delete job helm-install-zitadel -n kube-system
教训#
修改 Vault 密钥时,总是在写入后立刻验证。ZITADEL 的 master key 必须恰好 32 字节——不多不少。可以在 ESO ExternalSecret 的
template中加长度校验,但最简单的方式是写入后检查kubectl get secret -o jsonpath确认实际值。
总结:Cilium 迁移备忘录#
经过这次迁移,我总结了以下几点注意事项:
迁移前的检查清单#
- UDP 协议依赖:检查是否有服务使用 QUIC 或其他 UDP 协议,准备 TCP fallback
- Pod→Host 直连:排查哪些组件直接使用
InternalIP连接节点服务(metrics-server 是最常见的) - 密钥完整性:如果迁移涉及重建集群,确保所有 Vault 密钥的格式和长度正确
Cilium 特定的坑#
| 问题 | 根因 | 解决方案 |
|---|---|---|
| QUIC/UDP 握手失败 | VXLAN 封装 + masquerade 路径兼容性 | 强制使用 TCP 协议 |
| Pod→Host 物理 IP 不通 | TC BPF 静默拦截 tunnel 模式下的 local 路由流量 | hostNetwork: true |
| Cilium monitor 看不到 drop | 不是 policy drop,BPF 程序直接"消化"了包 | tcpdump + 多接口对比排查 |
最终状态#
迁移完成后,所有服务恢复正常运行:
- ✅ 42 个 Pod Running(homelab)+ 27 个 Pod Running(oracle-k3s)
- ✅ Cilium 204/204 controllers healthy,Hubble 可观测性就绪
- ✅ Cloudflared HTTP/2 tunnel 稳定连接
- ✅ 全部公开服务可达,SSO 流程正常
- ✅
kubectl top正常工作