Day 8(attempt 1):跨 WAN GPU 加入主集群(失败复盘)
主集群在某 IDC 内网 10.0.24.0/24,5 节点都拿到公网 IP;GPU 节点是另一家供应商的 A800-SXM4-40GB 单机,只能通过公网访问。本来想顺着 Day 1 的 per-node HAProxy 姿势,让这台 GPU 节点 kubeadm join 进主集群,scheduler 直接看到 nvidia.com/gpu: 1,省一套独立集群的维护成本。
实际走了 1.5 小时,链路打通到 kubeadm join 成功,最后卡在 Cilium agent 跨 WAN CrashLoopBackOff,并且 k3s install 阶段 SSH 突然断了。综合评估调通 Cilium VXLAN over WAN 的代价(每条路径都是 1-3 小时调试且不一定通),决定放弃这条路线,改走 Day 8-ai-infra.md 里的 k3s 单节点 + 独立运行 方案。
这一篇记录走到一半就放弃的尝试,重点不是「怎么修」,而是「为什么不修」—— 跨 WAN K8s 节点集成什么时候应该硬上,什么时候应该早点止损。
起点条件 / 网络拓扑
┌──────────────────────────────┐ 公网 ┌──────────────────────────┐
│ 主集群(IDC A) │ ←──────────────→ │ GPU 节点(IDC B, NAT 后)│
│ k8s-cp-1 10.0.24.31 │ │ 公网: ***.109.239.32 │
│ k8s-cp-2 10.0.24.29 │ │ 内网: 192.168.122.6 │
│ k8s-cp-3 10.0.24.30 │ │ GPU: A800-SXM4-40GB │
│ k8s-w-1 10.0.24.28 │ │ 40GB HBM, Ampere │
│ k8s-w-2 10.0.24.32 │ │ │
│ 5 节点都有公网 IP │ │ │
│ Pod CIDR 10.244.0.0/16 │ │ │
│ Cilium 1.16 VXLAN │ │ │
└──────────────────────────────┘ └──────────────────────────┘
关键不对称:
- 主集群 5 节点在同子网,Cilium VXLAN tunnel 走内网 8472/udp,已稳跑 7 天
- GPU 节点跟主集群没有 L2 邻居关系,只能 公网 IP ↔ 公网 IP 通信
- GPU 节点本机
ip addr看不到自己的公网 IP(在 supplier NAT 后),ens3只有192.168.122.6
主集群单 worker 算力够用,加 GPU 节点纯粹是想让 vLLM/Triton 这类 GPU Pod 跑在 K8s 调度框架内(声明 nvidia.com/gpu: 1 即可),而不是裸 docker。
网络可达性验证
kubeadm join 之前先把所有端口走一遍。结果都通:
GPU → cp-1:6443/tcp OK (apiserver)
GPU → cp-3:6443/tcp OK
GPU → cp-1:8472/udp OK (Cilium VXLAN data plane)
GPU → Harbor 30002/tcp OK (镜像 mirror, 10.0.24.28:30002)
cp-1 → GPU:15128/tcp OK (SSH 反向, 用于 kubectl exec/logs 回连)
UDP 测通用的是 nc -uvz + 对端 tcpdump,不能只看 nc 返回码 —— UDP 没有握手,本地 nc 永远 success,必须对端抓到包才算数。
5 个端口 + 2 个方向都通 = 链路层 OK,但这只是 「单数据流跑通」,跨 WAN 真正的难点在 NAT 表的稳定性,后面会复盘。
系统准备
GPU 节点按主集群同款基线装一遍,跟 Day 0 / Day 1 完全对齐:
# 内核基线(同 Day 1 A.2)
swapoff -a; sed -i '/[[:space:]]swap[[:space:]]/s/^/# /' /etc/fstab
modprobe overlay && modprobe br_netfilter
# sysctl br-nf + ip_forward ...
# 运行时(跟主集群版本对齐,避免 sandbox image 跨版本拉不到)
containerd 2.0.0 # 跟主集群一致
runc 1.2.1
CNI plugins 1.6
# K8s 工具
kubeadm/kubelet/kubectl 1.30.14 # 跟主集群完全一致(hold 住)
# 镜像 mirror(公网拉不到 registry.k8s.io)
/etc/containerd/certs.d/
docker.io/hosts.toml → docker.m.daocloud.io
quay.io/hosts.toml → quay.m.daocloud.io
10.0.24.28:30002/hosts.toml → 主集群内 Harbor(走公网)
per-node HAProxy 同款 Day 1 模式:
listen 127.0.0.1:16443 → cp-1/cp-2/cp-3 公网 6443 (各家公网 IP)
/etc/hosts: 127.0.0.1 k8s-api
验证:
curl -k https://k8s-api:16443/version
# {"major":"1","minor":"30","gitVersion":"v1.30.14",...}
到这里前置工作没坑,跟集群内 worker join 体感几乎一样。
kubeadm join 成功,但 InternalIP 不对
kubeadm join k8s-api:16443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash> \
--cri-socket unix:///run/containerd/containerd.sock
# This node has joined the cluster
kubectl get nodes -o wide 出来 GPU 节点 InternalIP 显示的是 192.168.122.6(GPU 本机内网 IP,主集群其他节点完全不可达)。
主集群其他节点想跟 GPU 节点的 kubelet 通信(拉日志、exec 进 Pod),都会走这个 InternalIP 失败。
kubelet --node-ip 的两个隐藏要求
第一个坑:KUBELET_EXTRA_ARGS 的位置。systemd drop-in 习惯写到 /etc/systemd/system/kubelet.service.d/,但 kubeadm 装的 kubelet 用的是 /etc/default/kubelet(EnvironmentFile 路径)。写错位置不会报错,只是不生效。
第二个坑(真正卡住的):即便 --node-ip=***.109.239.32 写对位置,kubelet 校验该 IP 必须在本机网卡上。GPU 节点 ens3 只有 192.168.122.6,公网 IP 在 supplier 的 NAT 网关上,kubelet 找不到对应网卡,fallback 回 192.168.122.6。
绕开方式:给 ens3 加 secondary IP,让 kubelet 在本机能看到这个公网 IP:
ip addr add ***.109.239.32/32 dev ens3
systemctl restart kubelet
这是个 trick —— supplier 那边的 NAT 仍然在工作,GPU 节点对外其实是 ***.109.239.32,但本机 routing table 也声明了一个同名地址,骗过 kubelet 的本地校验。重启 kubelet 后 InternalIP 切到公网 IP,kubectl get nodes 看上去正常了。
到这一步是这次尝试的 局部最高点:节点 Joined、kubelet 健康、InternalIP 正确。接下来 Cilium 一上就崩。
卡在哪:Cilium agent 跨 WAN CrashLoopBackOff
GPU 节点 join 后,主集群 Cilium DaemonSet 会自动调度一个 cilium-agent Pod 过去。13+ 次 restart 后状态稳定为 CrashLoopBackOff:
back-off 1m20s restarting failed container=cilium-agent
CNI 层的报错链(来自 kubelet journal):
plugin type="cilium-cni" failed (add):
unable to connect to Cilium agent:
failed to create cilium agent client after 30.000000 seconds timeout:
Get "http://localhost/v1/config":
dial unix /var/run/cilium/cilium.sock: connect: no such file or directory
cilium-agent 自己的 log 拉不到具体 fatal —— 启动过程中就退出了,stderr 截断。从配置侧推测有三条可能的根因,都不容易单独证实:
- 跨 WAN VXLAN/WireGuard 握手失败:Cilium 默认假设节点同网段,VXLAN UDP 8472 单向通了不代表握手层一定能完成;WireGuard 4244/51871 在 supplier NAT 后行为不确定
- NodeIP 跟实际 NAT 地址不一致:cilium-agent 启动时会探测自己的 NodeIP,secondary IP hack 骗得过 kubelet,不一定骗得过 cilium-agent 的探测逻辑
- WireGuard mesh 加入失败:主集群 5 节点的 Cilium WireGuard mesh 已经建好,第 6 个 peer 跨 WAN 加入时 handshake 走公网,NAT 设备的 UDP 表项过期/丢包都会让 handshake 超时
VXLAN/WireGuard 单向 UDP 通了 ≠ 双向 handshake 稳。一个数据流测试包过去,回程包能不能在 NAT 表项过期前回来,是另外的问题。
决策点:为什么没继续修
把「调通跨 WAN Cilium」拆成可能的路径:
| 方向 | 估时 | 通过率(主观) |
|---|---|---|
Cilium 显式配置 tunnel-endpoint 用公网 IP + 关 host firewall | 1-2h | 50% |
| 单独建 WireGuard tunnel,绕开 Cilium 自己的 WG mesh | 2-3h | 60%(但脆弱) |
| 改用 Cilium ClusterMesh 联邦(GPU 节点单独成集群) | 3-5h | 90%,但等同于放弃 join 方案 |
每条路径都有 supplier NAT 行为这个不可控变量 —— 不是 K8s 层面的问题,是底层网络给不给力。
触发放弃的两个直接事件:
kubeadm reset之后正要装 k3s 重启思路,curl 灌 install.sh 跑到一半 SSH 断了。k3s install 会大量改 iptables(KUBE-* 链),改的过程中 supplier 的 NAT/防火墙映射被打断,SSH inbound 直接挂。移动 + 电信都Connection closed,等了 30 分钟才恢复- SSH 断的同时意识到一件事:跨 WAN 集群里,任何一次 iptables 改动都可能让节点失联。哪怕 Cilium 调通了,后续日常运维(升级、加 NetworkPolicy、Cilium agent 自身重启)都是潜在的 SSH 断点
这两件事让 trade-off 翻转:调通的代价是 1-3 小时,但维护一个跨 WAN K8s 节点的长期成本 远大于「独立 k3s + 集群外调用」。
切方案路径:
- 主集群
kubectl drain+kubectl delete nodeGPU 节点 - GPU 节点
kubeadm reset -f,清/etc/kubernetes/etc/cni/net.d/var/lib/etcd - 装 k3s 单节点(
INSTALL_K3S_MIRROR=cn),把 GPU Pod 跑在独立集群里 - 后续 ArgoCD / observability 通过 集群外 API 调用 接入主集群
完整的 k3s 单节点 + NVIDIA Device Plugin + vLLM 落地见 Day 8 - AI Infra。
教训:跨 WAN K8s 节点什么时候要、什么时候别要
应该硬上的场景:
- 多 region 高可用是业务硬要求(金融、跨国 SaaS),架构上必须接受跨 WAN K8s 的复杂度
- 节点数 ≥ 3 才考虑做 ClusterMesh / 联邦,单节点不值得
- 底层有专线 / VPN tunnel 兜底,不依赖 supplier NAT
不应该硬上的场景(当前这种):
- 单个 GPU 节点跨 WAN —— 收益就是「scheduler 看得到」,但失去 K8s 假设的「节点同网段 + 低延迟 + 高带宽」前提
- supplier NAT 行为不可控 —— 任何 iptables 改动都可能误伤 SSH
- 没有第二通道(VNC / 串口 / 带外管理)—— SSH 是唯一入口的情况下,不要在跨 WAN 节点上跑
curl install.sh | sh这种「改 iptables 的网络脚本」
K8s 的核心假设是「同子网 + 低延迟 + 高带宽」。一旦把单节点跨 WAN 接进来,Cilium VXLAN / WireGuard / kube-proxy / kubelet 的所有「默认配置」都不再适用,要么走 ClusterMesh 这种为跨地域设计的姿势,要么就别 join,让 GPU 节点保持独立 cluster + 集群外服务调用。
这一轮真正学到的东西
kubelet --node-ip跨 WAN:必须配 secondary IP 在本机网卡上,kubelet 才接受kubeadm join跨 WAN:本地 HAProxy 转发到 cluster 公网 IP,通过/etc/hosts给k8s-api别名,姿势上跟集群内 join 完全一致- Cilium 跨 WAN 不是配置问题,是设计问题:helm values 改不出来,需要专门的 ClusterMesh 或前置 WireGuard tunnel
- 改 iptables 的脚本不要在唯一 SSH 通道上裸跑:
tmux、screen、带外管理任选其一,否则一次断连 30 分钟起步 - 「单数据流测通」≠「链路稳定」:UDP 端口走 nc 测通不代表跨 WAN VXLAN/WireGuard handshake 一定稳,NAT 表项过期和丢包是隐藏变量
简历可写一句:在跨地域多 WAN 环境集成单 GPU 节点至主 K8s 集群,通过 per-node HAProxy + 公网 IP secondary 注册 + 双向 NAT 端口映射,将节点 kubeadm join 成功;评估 Cilium VXLAN over WAN 的工程代价后,trade-off 选择独立 k3s 集群 + 集群外 API 调用方案,避免长期运维风险。