因客观条件限制,我打算用手头上3~4个 vps 来搭建一个 k8s 集群用于学习和实验,所以就有了这篇文章。本文主要是探讨一个三/四节点(单 master)的跨云跨国云主机在搭建 k8s 集群时应该采用什么样的网络架构,在阅读这篇文章之前默认你已经有了 k8s 相关的基础理论知识,关于具体的搭建流程后面会写一篇新的文章。

1. 私有网络

为什么首先提到的是私有网络?
结论放在前面:安全!即便是我个人测试用的这里也要使用私有网络

那么为什么不能直接使用各个云主机的公网 ip 地址来搭建 k8s 集群呢?因为直接使用公网 ip 地址搭建 k8s 集群存在极大的安全隐患:

  1. k8s 的控制平面是整个集群的大脑,kube-apiserver 作为其入口,若直接暴露于公网那么你懂的,这将会面对永无休止的恶意扫描和攻击。尽管 apiserver 拥有完善的认证授权机制(TLS、RBAC),但将核心管理端口暴露在互联网上,本身就极大地增加了攻击面,违反了最小权限和纵深防御的安全原则。etcd 的端口(2379/2380)若有任何配置不当,则可能导致整个集群数据被泄露或篡改,后果不堪设想。
  2. 集群内部的流量如果直接通过公网进行通信,那么 k8s 原生的 NetworkPolicy 就很难有用武之地了。网络策略依赖于 CNI 插件对 pod ip 的识别和控制,公网流量使得这种精细化的内部访问控制会变得异常复杂和不可靠。
  3. 如果使用的 CNI 插件用的是 BGP 等明文协议,这相当于对外公布了你集群所有的路由信息,仅是从信息泄露这个层面来看就是不可接受的。

综上所述,为所有节点构建一个统一的、私有的“第二层”网络平面、让所有集群内部的通信(包括控制平面和数据平面)都发生在这个可信的私有网络之上是十分必要的!

k8s 集群内通信机制

在深入组网方案之前,我们有必要先回忆下 k8s 集群内部的通信模型。
首先我们都知道 k8s 集群的通信主要围绕 kube-apiserver 展开,它以 HTTPS API 的形式提供服务:

  • 控制平面内部通信:etcd、kube-controller-manager、kube-scheduler 与 kube-apiserver 之间的通信,均通过 TLS 加密。etcd 尤其敏感,通常只监听在回环地址或一个受信任的私有网络接口上。
  • 控制平面与工作节点通信:
  • kube-apiserver 到 kubelet:apiserver 主动连接 kubelet 的 10250 端口,用于执行 kubectl exec/logs/port-forward 等命令,此连接需要经过 kubelet 的认证和授权。
  • kubelet 到 kube-apiserver:kubelet 作为客户端,主动连接 kube-apiserver 的 6443 端口,上报节点状态、Pod 状态,并接收指令,这是最主要的通信路径。

2. 组网工具

市面上有多种成熟的组网工具,如 ZeroTier、Tailscale、WireGuard 等等,我这里使用的是原生的 WireGuard,关于 WireGuard我之前也写了一篇相对详细的文章: WireGuard原理解析与生产实践
几种组网工具说明以及为何会选择使用 WireGuard,原因如下:

  • ZeroTier:功能强大,通过自有的全球根服务器网络实现复杂的 NAT 穿越和多路径路由,配置简单。但其协议私有,且核心网络依赖于中心化的控制器
  • Tailscale:基于 WireGuard 构建,极大地简化了密钥交换和节点管理。它引入了一个中心化的协调服务器来管理公钥、ACL 和 IP 分配等。最大的特点是易用,but Tailscale 不支持自定义组网网段,并且使用了 100.64.0.0/10 这个为运营商级 NAT 保留的地址段。这本身并不是个问题,问题在于奇妙的阿里云服务器内部也使用了这个地址段。如果你也有阿里云的服务器那就有可能会产生冲突导致路由问题,这里我不具体展开了,搜索关键词会有很多文章
  • WireGuard:一个现代化、高性能的 VPN 协议,从 5.6 版本内核开始已被并入 Linux 内核主线,市面上也有很多基于它构建的组网工具,主要缺点是没有自带的管理工具,在多节点全连接网状拓扑下扩展配置就很麻烦
特性WireGuardTailscaleZeroTier
核心协议WireGuardWireGuard自有协议
性能极高 (内核态)很高 (基于 WireGuard)良好
配置复杂度中等 (手动管理密钥)极低 (自动化)
网络自定义完全自定义有限 (固定网段)较高
中心化依赖无 (纯 P2P)有 (协调服务器)有 (根服务器)

我的需求场景: 一个稳定、高性能、且网络配置完全可控的底层。
3~4个节点的 wg 配置不会太麻烦,对比 Tailscle 和 ZeroTier 原生 wg 的网络稳定性实测要更优秀,所以最终选择 WireGuard 进行节点间的组网。

节点间组网规划

  1. 网络拓扑:对于三到四个节点的小规模集群,最理想的拓扑显然是 全连接网状网络。每个节点都与其他所有节点直接建立 WireGuard 隧道。这样任意两个节点间的通信都只需要一跳,延迟最低,且没有单点故障。

  2. 网段选择

    • 必须在 RFC 1918 定义的私有地址空间中选择:10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
    • 为了最大程度地避免与节点所在云环境的内网、本地开发环境的局域网或未来可能接入的其他网络产生冲突,一个最佳实践是选择一个相对冷门的网段。例如,不直接使用 10.0.0.0/24192.168.1.0/24,而是选择像 10.66.66.0/24 这样的网段
    • 对于3-4个节点,建议直接使用 /24 这种标准的 C 类子网便于记忆和管理,当然选择一个小的子网(如 /29)地址也完全够用

3. CNI 插件

上面我们讨论了 k8s 集群中 Node-to-Node 的通信问题。接下来需要解决 k8s 中更加核心的 Pod-to-Pod 通信问题。

首先要澄清一个事实,也是常见的疑惑。 K8s 官方仅提出了个网络规范模型,并不提供具体实现。这个模型的核心原则是:

  1. 集群中每个 Pod 都拥有一个唯一的、可路由的 IP 地址
  2. 任何节点上的 Pod 都可以直接与任何其他节点上的 Pod 通信,无需 NAT
  3. 节点上的代理(如 kubelet)可以与该节点上的所有 Pod 通信。

CNI 插件则是这个模型的具体实现,k8s 会通过用户选择安装的 CNI 插件来实现创建虚拟网络设备、配置 IP、设置路由等操作。

常见的 CNI 插件

市面上有众多 CNI 插件,我们选取几个主流的进行对比:

CNI 插件核心技术网络模式网络策略性能适用场景
FlannelVXLAN / host-gwOverlay不支持 (需配合其他)中等简单、快速部署,功能要求不高的场景
CalicoBGP / IPIP / VXLANOverlay / Underlay(BGP)强,功能丰富高 (BGP 模式)对网络策略和性能有高要求的生产环境
CiliumeBPFOverlay / Underlay极强 (基于身份/API)极高云原生、微服务、高性能和可观察性场景

**我这里选择使用 Calico 这个 CNI 插件并使用 BGP 网络模式,原因如下:

  • 避免双重封装: Flannel 通常使用 VXLAN 封装 Pod 流量。如果将其运行在已经是 WireGuard 隧道的网络上,就会产生 Pod流量 -> VXLAN封装 -> WireGuard封装 -> 公网 的双重封装,带来额外的性能开销(MTU 问题、CPU 消耗)。
  • 利用底层网络: Calico 的 BGP 模式将每个 K8s 节点变成一个 BGP aS(自治系统)。节点之间通过 BGP 协议交换各自负责的 Pod CIDR 的路由信息。这意味着,当 Node1 上的一个 Pod 要访问 Node2 上的 Pod 时,Node1 的内核路由表会明确地知道,目标 Pod IP 的下一跳是 Node2 的节点 IP。
  • 与 WireGuard 的协同: 我们可以让 Calico 的 BGP Peering 直接在 WireGuard 的私有网络接口(如 wg0)上进行。这样,路由信息和 Pod 数据流量都将通过加密的 WireGuard 隧道传输,既实现了路由的高效宣告,又保证了数据的安全性。整个数据路径是:Pod流量 -> WireGuard封装 -> 公网,只有一层封装,性能最优。

下面的 Mermaid 图展示了 Calico BGP over WireGuard 的数据流:

sequenceDiagram
    participant Pod1 on Node1
    participant Node1 Kernel
    participant WG Tunnel on Node1
    participant WG Tunnel on Node2
    participant Node2 Kernel
    participant Pod2 on Node2

    Pod1->>Node1 Kernel: Send packet to Pod2 IP
    Note over Node1 Kernel: Routing Table Lookup<br/>Destination: Pod2's CIDR<br/>Next Hop: Node2's WG IP (10.20.30.2)
    Node1 Kernel->>WG Tunnel on Node1: Forward packet to wg0 interface
    WG Tunnel on Node1->>WG Tunnel on Node2: Encapsulate & send over Internet
    WG Tunnel on Node2->>Node2 Kernel: Decapsulate & receive packet
    Note over Node2 Kernel: Packet arrives on wg0<br/>Destination: Pod2 IP
    Node2 Kernel->>Pod2 on Node2: Deliver packet

Pod 网段规划

我这里就用默认的 10.244.0.0/16,默认就是一个极大的值,而且我觉得也没有调小的必要性。
如果你想要自定义个网段一个至关重要的点: Pod 网段(--pod-network-cidr)绝对不能与节点 WireGuard 网络的网段重叠,当然如果按照我前面的规划,节点间 wg 组网使用 10.66.66.0/24 则即可与 Pod 的默认网段分开。

  • K8s 的 kube-controller-manager 会从你指定的 Pod 网段中为每个节点分配一个小子网(例如,--pod-network-cidr=10.244.0.0/16,那么 Node1 可能分到 10.244.0.0/24,Node2 分到 10.244.1.0/24
  • Calico 会将这些子网的路由信息通过 BGP 在节点间广播
  • 如果 Pod 网段与节点网络重叠,将导致严重的路由冲突

推荐的规划是:

  • 节点网络: 10.66.66.0/24
  • Pod 网络: 10.244.0.0/16 (默认,--pod-network-cidr 指定 )
  • Service: 10.96.0.0/12 (kubeadm 默认值,通常无需修改)

4. 服务暴露

上面已经解决了节点和 Pod 的通信,现在我们还需要考虑集群内部服务间的通信以及如何将服务安全地对外暴露。

kube-proxy 模式

毕竟是讨论 k8s 集群的网络设计,这里顺带也提下 kube-proxy 吧。

kube-proxy 是实现 k8s 集群中 service 的关键组件,它负责将发送到 Service ClusterIP 的流量负载均衡到后端的 Pods。它主要有两种工作模式:

  • iptables: 默认采用的模式。kube-proxy 会为每个 Service 和 Endpoint 创建大量的 iptables 规则。当 Service 数量巨大时,iptables 规则链会变得非常长,数据包在内核中进行规则匹配的路径也会变长,导致性能下降。其查找算法的时间复杂度是 O(n),其中 n 是规则数量
  • IPVS (IP Virtual Server): IPVS 是 Linux 内核中专门用于负载均衡的模块,内建于 LVS (Linux Virtual Server) 项目。它使用哈希表来存储 Service 和 Real Server (Pod) 的映射关系,查找效率极高,时间复杂度为 O(1)。此外,IPVS 支持更丰富的负载均衡算法(轮询、最少连接、加权等)

对于有一定规模或对性能有要求的 k8s 集群,强烈推荐使用 IPVS 模式。虽然在我的 3 节点小集群中实测性能差异并不明显,但我就是想要换成 ipvs 哈哈哈。
修改的方法也很简单,确保 ipvs 模块已经加载并且在 kube-proxy 的 ConfigMap (kube-proxy-cm) 中将 mode 字段从 "" (或 "iptables") 修改为 "ipvs" 即可。

服务暴露方案

K8s 默认提供的服务暴露方式(service 类型):

  • ClusterIP: 默认类型。为 Service 分配一个只能在集群内部访问的虚拟 IP。节点间可通过这个 IP 访问服务,但无法从外部访问
  • NodePort: 在 ClusterIP 的基础上,在每个节点上都开放一个固定的静态端口(默认范围 30000-32767)。任何发送到 <NodeIP>:<NodePort> 的流量都会被转发到该 Service。在生产环境中,它通常不直接对最终用户暴露,而是作为上游负载均衡器(如 F5, Nginx)的目标
  • LoadBalancer: 在 NodePort 的基础上,请求云服务商创建一个外部负载均衡器,并将流量导向所有节点的 NodePort。这是云上环境最常用的生产级暴露方式,但它依赖于云厂商的服务,在我们的跨云、自建环境中不可用
  • ExternalName: 这个比较特别它不转发流量,而是将 Service 名称映射到指定的 DNS 名称。适用于集群内的应用需要通过固定名称访问外部服务的场景

对于我的跨云环境,LoadBalancer 首先就被排除了,其次 ExternalName 不适用测试、日常使用场景,ClusterIP 无法在外网直接使用,看起来就只能使用 NodePort 了,但是这也有个问题,使用 NodePort 意味着我访问的时候还必须要用 ip:端口 的形式,这看起来令人不爽。我渴望一个统一的入口或者说反向代理吧!
习惯了使用因为我个人测试用也是通过域名去访问,而我的域名托管在 cloudflare,cloudflare 的免费版不支持给同一个域名解析到不同的 A 记录。

针对这个问题,自然就会想到生产环境常用的 ingress 了。

简单介绍下 ingress,我们一般提到的 ingress 指的是 ingress + ingress controller
首先 Ingress 它不是一种 service 类型,它是一个独立的 API 对象,它定义了从集群外部访问内部 Service 的规则,主要针对 HTTP/HTTPS 流量。
Ingress Controller 则是实现这些规则的“大脑”,它是一个运行在集群内的反向代理程序,持续监听 Ingress 对象的变化,并动态更新自身的路由配置。
对于我当前的环境选择使用 ingress 可以让多个 service 可以共享同一个 Ingress Controller 和同一个外部入口,这很好。

但是依然存在一个问题。Ingress 没有解决一个根本问题: 外部流量最初应该发送到哪里? Ingress Controller 本身也需要被暴露。在我的跨云环境中,有以下几种传统思路:

  • NodePort + DNS 轮询: 将 Ingress Controller 的 service 设置为 NodePort 类型。这样,三个节点的公网 IP 都会监听在同一个端口上。随后,我们可以为域名(如 hello.example.com)配置三条 A 记录,分别指向这三个公网 IP。可惜的是我的域名托管在 cloudflare,cloudflare 的免费版不支持给同一个域名解析到不同的 A 记录
  • NodePort + 专业 DNS 服务: 使用支持健康检查的 DNS 服务(常见的有 AWS Route, Google Cloud DNS 或 Cloudflare 的付费套餐)。这些服务可以定期探测 NodePort 的可用性,并动态地从 DNS 解析结果中移除故障节点的 IP。可惜的是都不免费!
  • 自建外部负载均衡器: 在三台 K8s 节点之外,再部署一组高可用的负载均衡器(HAProxy + Keepalived),由它们来接收所有流量并分发到 K8s 节点的 NodePort。这应该是最可用的方案了,但是仍然会有一定的额外开销。

我这里最终方案是妥协使用 Cloudflare Tunnel 来免费解决问题,当然代价是大陆访问存在网络负优化。

Cloudflare Tunnel (原名 Argo Tunnel) 是一个非常巧妙的工具。它的工作原理如下:

  1. 在 Kubernetes 集群内部署一个 cloudflared 连接器(通常是 Deployment)
  2. 这个连接器会主动向 Cloudflare 的边缘网络发起一个出站连接隧道
  3. 然后你可以在 Cloudflare 的 DNS 配置中,将你的域名(如 hello.example.com)指向这个 Tunnel
  4. 当外部用户访问 hello.example.com 时,请求会到达 Cloudflare 的边缘节点。Cloudflare 通过已经建立的隧道,将流量安全地转发到集群内的 cloudflared 连接器上
  5. cloudflared 连接器再将流量转发给集群内部的 Ingress Controller Service

这个方案的优势在于:

  • 零公网暴露: 因为 cf 使用隧道连接到集群内部,所以我们的服务就可以使用上面提到的 ClusterIP 方式仅对集群内暴露,这样任何一台 K8s 节点都不需要暴露任何公网端口,所有入站流量都通过 Cloudflare 的加密隧道进入,安全性极高。
  • 原生高可用: cloudflared 的 Deployment 可以有多个副本,它们会自动连接到 Cloudflare 边缘,实现负载均衡和高可用。Cloudflare 会自动处理到健康隧道的流量路由。
  • 简化网络配置: 无需处理公网 IP 变化、DDNS、防火墙端口开放等繁琐事宜。

这个方案的核心缺点在于: 网络对大陆不友好,部分省份延迟可能达到4-5秒。

最后还有一个问题,既然已经决定使用 cloudflare tunnel 了,那么前面说的 ingress 还需要继续使用吗?

答案是仍然推荐使用 ingress,看下面两张图就明白了。

  • 方案一: 为每个服务创建一个 Tunnel。在这个方案中需要在 Cloudflare 那边配置三条独立的 Tunnel,并且为每个 Tunnel 配置一个域名,让它们分别指向 Kubernetes 内部对应的服务
  • 方案二:使用单一 Tunnel + Ingress。在这个方案中,只需要在 Cloudflare 那边配置一个 Tunnel。这个 Tunnel 的目标是集群内的唯一入口——Ingress Controller,所有的域名都先解析到这个 Tunnel 再由 ingress 分发到不同的服务中。

方案一示例:

graph TD
    subgraph "用户端"
        User[<fa:fa-user> 用户]
    end

    subgraph "Cloudflare 全球网络"
        CF_Edge[<fa:fa-globe> Cloudflare Edge]
        User -- "访问 web01.example.com" --> CF_Edge
        User -- "访问 web02.example.com" --> CF_Edge
        User -- "访问 web03.example.com" --> CF_Edge
    end

    subgraph "Kubernetes 集群内部"
        subgraph "网络入口"
            Cloudflared[<fa:fa-shield-halved> cloudflared Pods]
        end
        
        subgraph "应用服务 (L4)"
            Svc1[<fa:fa-server> web01-svc]
            Svc2[<fa:fa-server> web02-svc]
            Svc3[<fa:fa-server> web03-svc]
        end

        subgraph "应用Pods (分布在3个节点上)"
            Pod1A[<fa:fa-cube> web01-pod-a]
            Pod1B[<fa:fa-cube> web01-pod-b]
            Pod1C[<fa:fa-cube> web01-pod-c]
            
            Pod2A[<fa:fa-cube> web02-pod-a]
            Pod2B[<fa:fa-cube> web02-pod-b]
            Pod2C[<fa:fa-cube> web02-pod-c]

            Pod3A[<fa:fa-cube> web03-pod-a]
            Pod3B[<fa:fa-cube> web03-pod-b]
            Pod3C[<fa:fa-cube> web03-pod-c]
        end

        CF_Edge -- "Tunnel for web01" --> Cloudflared
        CF_Edge -- "Tunnel for web02" --> Cloudflared
        CF_Edge -- "Tunnel for web03" --> Cloudflared

        Cloudflared -- "转发给 web01-svc" --> Svc1
        Cloudflared -- "转发给 web02-svc" --> Svc2
        Cloudflared -- "转发给 web03-svc" --> Svc3
        
        Svc1 --> Pod1A & Pod1B & Pod1C
        Svc2 --> Pod2A & Pod2B & Pod2C
        Svc3 --> Pod3A & Pod3B & Pod3C
    end
    
    style User fill:#cde4ff
    style CF_Edge fill:#ffb366

方案二示例:

graph TD
    subgraph "用户端"
        User[<fa:fa-user> 用户]
    end

    subgraph "Cloudflare 全球网络"
        CF_Edge[<fa:fa-globe> Cloudflare Edge]
        User -- "访问 web01/02/03.example.com" --> CF_Edge
    end

    subgraph "Kubernetes 集群内部"
        subgraph "统一网络入口"
            Cloudflared[<fa:fa-shield-halved> cloudflared Pods]
        end
        
        subgraph "应用路由层 (L7)"
            IngressSvc[<fa:fa-network-wired> Ingress Controller Service]
            IngressPod[<fa:fa-map-signs> Ingress Controller Pod]
        end
        
        subgraph "应用服务 (L4)"
            Svc1[<fa:fa-server> web01-svc]
            Svc2[<fa:fa-server> web02-svc]
            Svc3[<fa:fa-server> web03-svc]
        end

        subgraph "应用Pods (分布在3个节点上)"
            Pod1A[<fa:fa-cube> web01-pod-a]
            Pod1B[<fa:fa-cube> web01-pod-b]
            Pod1C[<fa:fa-cube> web01-pod-c]
            
            Pod2A[<fa:fa-cube> web02-pod-a]
            Pod2B[<fa:fa-cube> web02-pod-b]
            Pod2C[<fa:fa-cube> web02-pod-c]

            Pod3A[<fa:fa-cube> web03-pod-a]
            Pod3B[<fa:fa-cube> web03-pod-b]
            Pod3C[<fa:fa-cube> web03-pod-c]
        end

        CF_Edge -- "统一入口 Tunnel" --> Cloudflared
        Cloudflared -- "所有流量都转发给 Ingress" --> IngressSvc
        IngressSvc --> IngressPod

        IngressPod -- "读取Ingress规则<br/>if host == web01.example.com" --> Svc1
        IngressPod -- "if host == web02.example.com" --> Svc2
        IngressPod -- "if host == web03.example.com" --> Svc3
        
        Svc1 --> Pod1A & Pod1B & Pod1C
        Svc2 --> Pod2A & Pod2B & Pod2C
        Svc3 --> Pod3A & Pod3B & Pod3C
    end
    
    style User fill:#cde4ff
    style CF_Edge fill:#ffb366
    style IngressPod fill:#99ff99

5. 总结

  • 节点间使用原生 WireGuard 全连接网状组网
  • 节点网段使用 10.66.66.0/24
  • CNI 插件使用 Calico,网络模式采用 BGP
  • Pod 网段使用默认的 10.244.0.0/16
  • Service 网段使用默认的 10.96.0.0/12
  • 采用 ClusterIP 仅对集群内暴露服务
  • 部署 ingress 统一服务入口
  • 使用 cloudflare tunnel 安全引入外部流量到集群内部

结合实际情况最终还是给出了这样一套不算完美的跨云、跨地域 k8s 集群网络设计思路,对于小型的实验环境(至少对于我)应该算是够用了。