跨云缝合怪:记一次多云 K3s 集群组网的血泪史(Flannel NAT 穿透之坑)
在云原生玩家的日常折腾中,“白嫖”各大云厂商的免费/吃灰资源组建一个大一统的 Kubernetes 集群,绝对是一件极具诱惑力的事情。
最近,我打算把手头散落在阿里云、AWS 和 Oracle Cloud (OCI) 的机器整合起来,用 K3s 搭一个轻量级的跨云混合集群,并在上面跑一套完整的 ArgoCD。本以为 K3s 主打一个“开箱即用”,结果却在跨云网络(Flannel)上踩进了一个巨大的“天坑”。
这篇文章记录了我们整个踩坑、排错以及最终利用 Tailscale 完美破局的全过程。
1. 我们的“万国牌”机器阵容
这套集群的物理拓扑横跨了三大云厂商,机器配置也参差不齐:
- Master 节点 (Control Plane):
- 阿里云 (广州):
2C 2G,作为集群的总指挥部,负责承载 API Server 等核心组件。
- 阿里云 (广州):
- Worker 节点 (Data Plane):
- AWS EC2 (新加坡):
1C 1G(aws-moon-proxy),主力 Worker,承载 ArgoCD 的 Repo Server 和 Redis。 - Oracle Cloud (OCI - AMD VM 1):
1C 1G(free-amd-vm),跨云打工人 1 号。 - Oracle Cloud (OCI - AMD VM 2):
1C 1G(free-amd-vm2),跨云打工人 2 号。
- AWS EC2 (新加坡):
我们的目标是:让这 4 台跨越半个地球的机器,组成一个内网互通的 K3s 集群。
2. 梦魇的开始:看似美好的公网直连
最开始的思路很简单粗暴:既然大家都有公网 IP,那我直接通过公网 IP 互联不就行了?
于是在 Master 节点上,我们指定了--tls-san <阿里云公网IP>。
在各大海外 Worker 节点上,我们通过--node-external-ip <公网IP>强制注册,并连接到 Master 的公网 IP。
敲下命令后,一切看起来无比顺利:
$ kubectl get nodes NAME STATUS ROLES AGE VERSION aliyun-master Ready control-plane 10m v1.35.5+k3s1 aws-moon-proxy Ready<none>2m v1.35.5+k3s1 free-amd-vm Ready<none>2m v1.35.5+k3s1...但是,当我们满心欢喜地部署完 ArgoCD 后,灾难降临了:
- Pod 疯狂卡死:ArgoCD 的各种 Controller 持续卡在
CreateContainerConfigError或是Terminating状态。 - 跨节点通信全挂:在阿里云的 Pod 试图访问 AWS 上的 Pod 时,全部报
502 Bad Gateway或者Timeout。 - 运维指令失效:当我们在 Master 节点执行
kubectl exec或是kubectl logs试图进入海外 Worker 节点上的容器时,指令直接卡死超时。 - 资源雪崩:大量的底层 TCP 握手重试和挂起的网络连接,甚至直接把原本只有 1G 内存的海外节点给 OOM(内存溢出)干崩了。
3. 深入底层:为什么 Flannel 处理不了云厂商的 NAT?
经过痛苦的抓包和排查,我们发现了罪魁祸首:K3s 默认使用的 Flannel 网络插件(VXLAN 模式)与云厂商的网络架构存在不可调和的矛盾。
在 AWS 和 OCI 的机器里,如果你敲下ip a,你会发现网卡绑定的根本不是公网 IP,而是172.x.x.x或10.x.x.x这样的内网 IP。云厂商的公网 IP 实际上是挂在外层的网关上,通过1:1 NAT(网络地址转换)映射给你的机器的。
这就是著名的“NAT 穿透陷阱”:
- 封包阶段:当阿里云上的 Pod A 要发数据给 AWS 上的 Pod B 时,阿里云的 Flannel 根据注册信息,将数据包用 UDP 8472 端口进行 VXLAN 封装,并将目标 IP 写为 AWS 的公网 IP。
- NAT 转换:这个封包跨越汪洋大海来到 AWS 的外层网关。AWS 网关一看:“这包是给我的公网 IP 的”,于是将其 NAT 转换为内网 IP
172.31.x.x,丢给对应的 EC2 实例。 - 拆包丢弃(致命一击):AWS 机器上的 Flannel 收到数据包后,解开外层一看——“等一下,这个 VXLAN 包里面记录的 Target IP 怎么是公网 IP?我的网卡明明是 172.31.x.x 啊!”。Linux 内核的网络栈瞬间产生“精神分裂”,认为这个包不是发给自己的,直接将其无情丢弃(Drop)。
这就是为什么kubectl get nodes能通(因为 kubelet 是主动向 API Server 发起连接的单向请求),但只要涉及 Master 主动向 Node 发起连接(如 exec/logs),或者 Pod 跨节点通信(双向路由),就会彻底陷入网络黑洞。
4. 破局之道:用 Tailscale 构建 Overlay 虚拟大内网
既然公网直连会被 NAT 网关和 Flannel 的底层逻辑“卡脖子”,那我们就换个思路:在机器之间拉一根虚拟的“物理网线”。
我们引入了基于 WireGuard 的大杀器:Tailscale。
我们在 4 台机器上全部安装了 Tailscale,组建了一个专属的虚拟局域网(网段为100.x.x.x)。Tailscale 会在每台机器的系统里强行插入一张名为tailscale0的真实虚拟网卡。
接着,我们对 K3s 进行了重构,放弃使用公网 IP 互联,强制绑定 Tailscale 网卡:
在 Aliyun Master 节点上重新启动 K3s:
curl-sfLhttps://get.k3s.io|sh-s- server\--node-ip=100.114.103.101\--flannel-iface=tailscale0在 AWS 和 OCI 的 Worker 节点上重新加入集群:
curl-sfLhttps://get.k3s.io|K3S_URL=https://100.114.103.101:6443K3S_TOKEN=xxxsh-s- agent\--node-ip=<当前机器的Tailscale IP>\--flannel-iface=tailscale0奇迹发生了。
K3s 的 Flannel 现在只认tailscale0这张网卡。当数据包需要跨云时:
- Flannel 看到源 IP 是 100.x,目标 IP 也是 100.x。
- 数据包被交给 Tailscale。
- Tailscale 在底层使用 WireGuard 将其加密,并利用其极其强悍的 NAT 打洞(P2P Hole Punching)能力,直接在广州机房和新加坡机房的公网之间建立了点对点的 UDP 直连!
通过tailscale status,我们可以清晰地看到流量没有绕路中转服务器,而是直接走了公网直连(direct):
100.104.30.75 aws-moon-proxy active; direct 13.212.67.185:41641 100.67.168.10 free-amd-vm active; direct 161.118.250.97:416415. 最终成果
得益于 Tailscale 的加持,我们获得了一个极为纯净且安全的跨云 K8s 运行环境:
- 零丢包:广州与新加坡/日本之间的跨云 PING 延迟稳定在70ms ~ 100ms,0% 丢包。
- 绝对安全:所有的跨云通信都被 WireGuard 进行了高强度加密,并完全隐匿了原本暴露在公网的 Kubernetes 端口(6443/10250 等),无视各种恶意扫描。
- 完美运行:之前疯狂报错卡死的 ArgoCD,在更换为 Tailscale 内网后,几十个 Pod 瞬间拉起并变为绿色的
Running状态,丝滑无比。
结语
在多云/混合云场景下折腾 K3s 时,千万不要试图去和各大云厂商复杂的公网 NAT、安全组以及 Flannel 的底层路由死磕。
引入 Tailscale 这类 Overlay 网络,将复杂的公网拓扑降维打击成一个纯净的“本地局域网”,才是最优雅、最省头发的终极解决方案。