课程目录(第 19 章 / 共 33 章)
课程/进阶:生产实践

集群网络:CNI 选型与 NetworkPolicy

19 章 / 共 33·22 分钟·进阶网络CNINetworkPolicyCilium

搞清 Pod、Service、节点三层网络的分工,比较主流 CNI 的取舍,并亲手写出默认拒绝与最小放行的 NetworkPolicy。

学完这一章,你将能够

  • 说得出 Kubernetes 网络模型的三条基本要求
  • 从性能、策略、可观测、运维复杂度几个维度对比主流 CNI
  • 写出默认拒绝所有入站、再按来源放通端口的 NetworkPolicy
  • 知道裸金属集群为什么需要 MetalLB

Kubernetes 网络模型的三条要求

集群网络看起来复杂,但 Kubernetes 对它的要求只有三条,记住这三条,后面所有插件和策略都能挂上去。

  1. 任意两个 Pod 之间可以直接通信,中间不需要 NAT。
  2. 节点与 Pod 之间可以直接通信,中间不需要 NAT。
  3. Pod 看到的自己的 IP,和别人看到的它的 IP 是同一个。

这三条意味着:Pod 拿到的是「真实可用」的 IP,不需要在应用里做端口映射或地址转换。代价是必须有人替集群实现这套转发与地址分配,这就是 CNI 插件存在的理由。

三层网络:Pod、Service 与节点

同一个集群里其实叠着三张逻辑网络,它们由不同的人分配、由不同的组件转发。

网络地址由谁分配由谁负责转发典型网段
Pod 网络CNI 插件的 IPAMCNI 的数据面10.244.0.0/16 或自定义
Service 网络kube-apiserverkube-proxy 或 eBPF172.23.0.0/16
节点网络机房或云平台物理交换机与路由10.0.0.0/16

两个硬约束:Service 网段和 Pod 网段不能重叠,也不能和节点网络、公司内网、VPN 网段重叠;而且这些网段在集群建好之后基本不可改。规划时先数清楚现网用掉了哪些地址。

用一张图记住三层的叠加关系,以及策略到底在哪一层生效:

text
同一台节点上叠着三张网络(示意)

节点网络   eth0 10.0.1.11   ── 机房分配,负责节点互通与访问集群外部
Pod 网络   10.244.1.0/24(每节点一段,由 CNI 的 IPAM 分配)
           ├── Pod web 10.244.1.5
           └── Pod api 10.244.1.9
                ▲ NetworkPolicy 生效点:Pod veth 的入口(Ingress)与出口
                │ (Egress),由 CNI 数据面执行;没实现策略的 CNI 只存
                │ 对象、不真拦截
Service 网络 172.23.0.0/16(虚拟地址,只存在于 iptables / eBPF 规则里)
           └── ClusterIP 172.23.14.7 ──DNAT──▶ 某个 Pod 的 IP

记住这条因果链:Pod 网络决定「能不能通」,Service 网络决定「怎么找到」,节点网络是两者的承载体,而策略只作用在 Pod 的出入方向上。

CNI 在做什么

CNI(Container Network Interface)是一个标准接口,不是某个具体软件。kubelet 在创建或删除 Pod 的网络命名空间时调用它,插件需要完成三件事:给 Pod 分配 IP(IPAM)、把 Pod 接进网络(通常是 veth pair 加路由)、执行网络策略。

它有几个容易被忽视的性质:CNI 不是 Kubernetes 自带的,集群装完 kubeadm 后如果不装 CNI,节点会一直是 NotReady;一个节点同一时刻只用一个主 CNI;NetworkPolicy 是否生效,完全取决于 CNI 有没有实现它

CNI数据面NetworkPolicyBGPeBPF可观测运维复杂度适合场景
Calicoiptables / eBPF,IPIP、VXLAN 或 BGP完整,含全局策略原生支持可选模式中等通用集群,需要和物理网络做 BGP
CiliumeBPF完整,含 L7(HTTP/gRPC)支持原生Hubble,强中高新集群,追求性能与可观测
FlannelVXLAN 或 host-gw不支持学习、小规模环境
Kube-OVNOVS / OVN完整,含子网与 VPC支持中等需要虚拟化网络、多租户子网

选型没有标准答案,但有判断顺序:先看要不要 NetworkPolicy(要就别选 Flannel),再看是否需要和物理网络打通(需要就看 Calico 或 Cilium 的 BGP),最后看团队能不能维护 eBPF 这套东西。

Cilium:eBPF 与 Hubble

Cilium 的做法是把转发与策略下沉到内核的 eBPF 程序里,而不是在节点上堆几千条 iptables 规则。带来的直接好处有三个:规则多了之后转发性能更稳定;可以做到七层策略(按 HTTP 方法、路径放行);内置 Hubble 能看到「哪个 Pod 在跟谁说话」。

Cilium 还能替代 kube-proxy:开启 kubeProxyReplacement 后,Service 的负载均衡由 eBPF 完成,节点上不再需要 iptables/IPVS 规则链。

最小安装命令如下(先确认可用版本再装):

bash
helm repo add cilium https://helm.cilium.io/
helm repo update
helm install cilium cilium/cilium --namespace kube-system \
  --set kubeProxyReplacement=true \
  --set k8sServiceHost=10.0.0.100 --set k8sServicePort=6443 \
  --set hubble.enabled=true --set hubble.relay.enabled=true --set hubble.ui.enabled=true

k8sServiceHost 必须填 API Server 的直连地址(VIP 或节点 IP),因为 Cilium 启动时还没有 Service 网络可用。另外,kubeProxyReplacement=true 要求集群里不能同时跑 kube-proxy,如果还留着,就先设为 false,或者移除 kube-proxy 后再开。

bash
kubectl -n kube-system rollout status ds/cilium
kubectl -n kube-system get pods -l k8s-app=cilium
kubectl get nodes -o wide

ds/cilium 的副本数等于节点数且全部就绪,kubectl get nodes 里所有节点 Ready,就说明 Pod 网络已经通了。浏览器看服务依赖图用 kubectl -n kube-system port-forward svc/hubble-ui 12000:80

Cilium 进阶:可观测、加密与 BGP

装完只是开始,生产上还要用三件事:看清流量、加密流量、把网段宣告出去

bash
cilium status --wait                          # 数据面健康检查
hubble observe --verdict DROPPED --last 50    # 只看被策略丢掉的连接,排错起点
hubble observe --from-pod demo/web --last 20  # 看某个 Pod 到底在跟谁说话

# 节点之间的流量加密,应用无需改造
helm upgrade cilium cilium/cilium -n kube-system --reuse-values \
  --set encryption.enabled=true --set encryption.type=wireguard

encryption.type 可选 wireguardipsec,WireGuard 要求节点内核 5.6 以上。它保护的是节点之间链路上的明文,不等于应用层 mTLS,两者可以叠加,见 服务网格

BGP 用来把 Pod 网段或 Service 的 VIP 直接宣告给机房交换机,让外部设备不经 SNAT 就能访问 Pod,代价是需要网络管理员分配 AS 号与邻居地址。配置对象随版本演进(旧版 CiliumBGPPeeringPolicy,新版 CiliumBGPClusterConfigCiliumBGPPeerConfig),写法以你安装版本的文档为准。

两个必须提前知道的坑:Cilium 会占用内核路由表 200/202/2004/2005,宿主机网络若也用这些表号必须调整;Pod 网段用尽时不能改原有网段,只能在 ipam.operator.clusterPoolIPv4PodCIDRList 里追加新段,再 kubectl -n kube-system rollout restart deploy/cilium-operator

参考:reference/k8s-in-action/network/cilium/README.md

进阶网络:多网卡、Underlay 与固定 IP

需要给 Pod 接第二块网卡时(存储、RDMA、必须被机房防火墙直接访问的中间件),用 Multus。它不是 CNI,而是 CNI 的多路复用器:在主 CNI 之外,再把若干附加网络挂到同一个 Pod 上。

多网卡与 Underlay:Multus 与 macvlan

附加网络用 NetworkAttachmentDefinition(简称 NAD)声明。macvlan 最常用——Pod 拿到一张与物理网卡同网段的虚拟子接口,直接出现在物理二层网络里,没有 VXLAN 封装开销:

yaml
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: storage-net
  namespace: demo
spec:
  config: |
    { "cniVersion": "1.0.0", "type": "macvlan", "master": "bond0", "mode": "bridge",
      "ipam": { "type": "whereabouts", "range": "10.20.30.0/24", "gateway": "10.20.30.1" } }

Pod 用一行注解挂上去(k8s.v1.cni.cncf.io/networks: storage-net),接口名默认 net1

bash
kubectl -n demo exec multi-nic -- ip -br addr   # 出现 net1 且是 10.20.30.x 才算成功

eth0 仍是主 CNI 分配的 Pod IP,net1 是 underlay 地址。生产上这么做的理由:underlay 地址能被交换机、存储阵列、防火墙直接看见,省掉 NAT 与隧道开销,适合存储和 RDMA 这类对带宽、延迟敏感的流量;代价是地址必须提前规划(和机房网段撞车就是灾难),而且 macvlan 子接口默认不能与宿主机本身通信(需要额外 shim 接口),别把「Pod 访问宿主机」的需求压在这张网上。GPU 训练场景里,节点装 NVIDIA network-operator 与 DOCA OFED 驱动,Pod 用 resources.limits 申请 rdma/hca: 1,RoCE 网络常再配一张 macvlan,或直接 hostNetwork: true

参考:reference/k8s-in-action/network/network-operator/README.mdreference/k8s-in-action/network/network-operator/macvlan/README.md

IP 池与固定 IP:Spiderpool

underlay 网络默认「随便挑一个空闲地址」,但生产上有三类需求它满足不了:地址要提前报备给网络管理员有状态应用重启后 IP 不能变IP 必须落在指定网段(比如必须与同组交换机在同一个子网)。Spiderpool 就是补这块的:给 macvlan、ipvlan、SR-IOV 这类 CNI 提供统一的 IP 池与固定 IP 能力。

yaml
apiVersion: spiderpool.spidernet.io/v2beta1
kind: SpiderIPPool
metadata:
  name: storage-pool
spec:
  subnet: 10.20.30.0/24
  gateway: 10.20.30.1
  ips:
    - 10.20.30.10-10.20.30.20

Pod 上再加一行注解指向这个池:ipam.spidernet.io/ippool: '{ "ipv4": ["storage-pool"] }',与挂网卡的注解写在同一个 Pod 上(字段名随版本演进,安装前对照官方文档确认)。

为什么要固定 IP:数据库主从、Redis 集群、存储网关这类有状态应用靠 IP 互相识别;机房防火墙白名单按 IP 放通;RDMA 的地址必须与物理网卡同网段,不能由集群随机分配。注意 Spiderpool 目前与 Istio 的 ambient 模式冲突,两者别叠在同一批节点上。

参考:reference/k8s-in-action/network/.arch/spiderpool/README.md(RDMA 的 RoCE/IB 细节见同目录 roce/ib/roce-mif/

NetworkPolicy 实操:先拒绝,再放通

NetworkPolicy 的思路和 Deployment 一样是声明式的:你不写「禁止谁」,而是写「允许谁」。所以正确的顺序是先加一条默认拒绝,再一条条放通,最后任何没写进策略的流量都会被挡掉。

动手练习:先造一个能验证策略的环境

demo 命名空间里放 webapiredis 三个应用,api 通过 Service 的 8080 端口暴露,再准备两个客户端 Pod,分别带 app=webapp=other 标签——后面靠它们的标签决定谁被放通。

bash
kubectl create namespace demo
kubectl -n demo create deployment web --image=nginx:1.27 --replicas=1
kubectl -n demo create deployment api --image=nginx:1.27 --replicas=1
kubectl -n demo create deployment redis --image=redis:7.4 --replicas=1
kubectl -n demo expose deployment api --port=8080 --target-port=80

kubectl -n demo run client-web --image=busybox:1.36 --labels=app=web --restart=Never -- sleep 3600
kubectl -n demo run client-other --image=busybox:1.36 --labels=app=other --restart=Never -- sleep 3600

现在还没有任何策略,两个客户端都能访问 api

bash
kubectl -n demo exec client-web -- wget -qO- --timeout=3 http://api:8080
kubectl -n demo exec client-other -- wget -qO- --timeout=3 http://api:8080

两条命令都应该返回 nginx 的欢迎页 HTML(Welcome to nginx!),这就是「加策略之前」的基线。记下它,后面被拦掉时才有对照。

第一步,写一份默认拒绝所有入站流量的策略。podSelector: {} 表示选中命名空间里所有 Pod,policyTypes: [Ingress] 表示只管入站。

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: demo
spec:
  podSelector: {}
  policyTypes:
    - Ingress
bash
kubectl apply -f default-deny-ingress.yaml
kubectl -n demo exec client-web -- wget -qO- --timeout=3 http://api:8080

此时会看到 wget: download timed out——连接被丢包了。注意:默认拒绝只影响入站,DNS 查询走的是出站流量,所以域名解析仍然正常,报错是连接超时而不是「地址不存在」。

第二步,只允许 app=web 的 Pod 访问 api 的 8080 端口。

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-allow-web
  namespace: demo
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: web
      ports:
        - protocol: TCP
          port: 80

这里最容易写错的细节

api 的 Service 端口是 8080,但它的 targetPort 是 80。NetworkPolicy 的 `ports` 看的是 Pod 的容器端口,不是 Service 端口,所以这里写 port: 80。写成 8080 的话策略永远匹配不上,流量照样被拒。

验证放通与阻断:

bash
kubectl apply -f api-allow-web.yaml
kubectl -n demo exec client-web -- wget -qO- --timeout=3 http://api:8080
kubectl -n demo exec client-other -- wget -qO- --timeout=3 http://api:8080
kubectl -n demo exec client-other -- nc -zv -w 3 api 8080
kubectl -n demo get networkpolicy

client-web 会返回 nginx 的欢迎页 HTML;client-other 则报连接超时(wget: download timed outnc: ... Connection timed out)。kubectl get networkpolicy 应该看到两条策略。清理时删掉命名空间即可:kubectl delete namespace demo

Service 负载均衡与 kube-proxy 模式

Service 的虚拟 IP 由谁兑现?传统答案是 kube-proxy,它把 Service 的转发规则写进节点内核;新一点的答案是 CNI 的 eBPF 数据面。

模式原理特点
iptables内核 iptables 规则 + 随机概率做 DNAT兼容性最好,规则多了之后转发变慢
IPVS内核 IPVS 哈希表,支持多种调度算法大规模 Service 下更稳定
eBPFCilium 等在内核 hook 直接处理绕过 iptables/IPVS,性能与可观测更好

查看当前集群用的模式:

bash
kubectl -n kube-system get ds kube-proxy
kubectl -n kube-system get cm kube-proxy -o yaml | grep -E "mode|scheduler"

如果 kube-proxy 这个 DaemonSet 根本不存在,说明集群用的是 eBPF 替代方案,这是正常现象。

裸金属的 LoadBalancer:MetalLB

云上创建 type: LoadBalancer 的 Service,云控制器会自动给你一个公网或内网 IP。裸金属集群没有这个角色,Service 的 EXTERNAL-IP 会一直显示 <pending>

MetalLB 补的就是这块:它从你指定的地址池里分配 VIP,再通过 ARP(L2 模式)或 BGP 把「这个 IP 在我这台机器上」宣告给交换机。

bash
helm repo add metallb https://metallb.github.io/metallb
helm install metallb metallb/metallb --namespace metallb-system --create-namespace
kubectl -n metallb-system rollout status deploy/metallb-controller

然后申请一个地址池并开启 L2 宣告,IP 段要找网络管理员要空闲的:

yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default
  namespace: metallb-system
spec:
  addresses:
    - 172.18.15.200-172.18.15.210
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb-system
spec:
  ipAddressPools:
    - default
bash
kubectl apply -f metallb-pool.yaml
kubectl -n metallb-system get ipaddresspools
kubectl -n demo create service loadbalancer nginx --tcp=80:80
kubectl -n demo get svc nginx

EXTERNAL-IP<pending> 变成一个池里的地址,就说明 MetalLB 生效了。

地址池只是「有哪些 IP 可以发」,怎么让外部网络知道这个 IP 在我这里才是关键,两种宣告方式差别很大:

维度L2 模式(ARP / NDP)BGP 模式
谁在宣告speaker 直接回答 ARP,VIP 只落在一台节点上speaker 与交换机建 BGP 邻居,宣告 VIP 路由
负载分担没有,入口流量先到同一节点再转发多节点 ECMP,真正分担入口流量
故障切换ARP 重新收敛,通常秒级依赖 BGP 收敛时间与交换机配置
前置条件不需要网络设备配合交换机要配 AS 号与邻居,需要网络管理员参与

L2 最简单,适合内网测试与中小规模生产;入口流量大或要求入口高可用就得上 BGP,否则承载 VIP 的那台节点既是单点也是瓶颈。

想保留客户端真实源 IP,给 Service 加 externalTrafficPolicy: Local:只有后端 Pod 所在的节点才承接并宣告这个 VIP。代价是失去跨节点负载均衡,而且 speaker 必须运行在有后端 Pod 的节点上,否则 VIP 根本不会被宣告。

bash
# VIP 落在哪台节点(较新版本)
kubectl get servicel2statuses.metallb.io -n metallb-system
# 池里已经用掉哪些地址
kubectl get svc -A -o jsonpath='{.items[*].status.loadBalancer.ingress[*].ip}' | tr ' ' '\n' | sort -u
参考:reference/k8s-in-action/network/metallb/README.md

常见坑与排错

网络问题的麻烦之处在于「没有任何报错」——包被悄悄丢掉,你只能看到超时。所以排查要按「策略是否真的被支持 → 选择器是否匹配 → 端口是否正确 → 网段是否冲突」的顺序走。

现象原因怎么确认怎么办
kubectl get networkpolicy 有策略,流量却照样通CNI 没有实现 NetworkPolicy(如 Flannel)kubectl -n kube-system get ds 看 CNI 类型,再查它是否支持策略换成 Calico 或 Cilium,策略才会真的生效
该放通的流量仍然超时ports 写成了 Service 端口,或 from 里选错了对象kubectl -n demo describe networkpolicy api-allow-web 看解析结果,对照容器端口ports 写容器端口,from 写来源 Pod 的标签
加了 Egress 默认拒绝后所有域名解析超时DNS 查询是出站流量,没放通 CoreDNS 的 53 端口kubectl -n demo exec client-web -- nslookup api 失败,而直连 IP 正常加一条 Egress 规则放通 kube-system 里 CoreDNS 的 53
curl ClusterIP 不通ClusterIP 是虚拟地址,ping 本来就不响应;或 Service 没有后端curl -sS -o /dev/null -w '%{http_code}\n' http://api:8080,再看 kubectl get endpointslices用端口探测代替 ping,确认 Endpoints 里有地址
部分节点上的 Pod 互相不通Pod / Service 网段与机房内网重叠kubectl get pods -o wide 对比 IP 段与机房路由表重建集群并重新规划网段,这是最难事后修的坑
LoadBalancerEXTERNAL-IP 一直 <pending>裸金属没有云负载均衡器kubectl -n demo get svcTYPEEXTERNAL-IP装 MetalLB 或改用 NodePort / Ingress

两个最容易造成事故的细节

  • 策略写反from 里要写来源的选择器,不是目标的。给 api 放通 webpodSelector 在顶层选 apifrom 里选 web
  • 换了 CNI 之后旧 Pod 不通:节点的 /etc/cni/net.d 里残留了多个配置,或旧 Pod 还持有已失效的地址,通常需要重建 Pod;Multus 卸载后遗留的 00-multus.conf 也会让新 Pod 起不来,删掉它再重试。

自测题

自测:为什么 NetworkPolicy 写了却没生效?(点击展开答案)

先按三层原因排查。第一层是 CNI 不支持:Flannel 会老老实实把 NetworkPolicy 对象存进 etcd,kubectl get networkpolicy 一切正常,但它从不实现拦截逻辑,所以流量毫无阻碍——这是「写了没用」最常见的原因。第二层是策略没匹配上podSelector 选的是被保护对象,from 选的是来源,两者写反、标签拼错、或者 ports 写成了 Service 端口(8080 而不是容器端口 80),都会让规则永远不命中。第三层是方向不对:只写了 Ingress,却被出站流量(比如 DNS)拦住。判断顺序就是:先确认 CNI 支持策略,再用 describe 看选择器解析结果,最后核对端口与方向。

自测:为什么 ClusterIP ping 不通,curl 却能通?(点击展开答案)

ClusterIP 不是一个真实存在的地址,没有任何网卡持有它。kube-proxy 或 eBPF 只是在内核转发规则里写了「访问这个 IP 的某个端口时,改写成某个 Pod 的 IP 和端口」。ping 用的是 ICMP,而转发规则通常只处理 TCP/UDP,也没有任何主机会响应这个虚拟地址的 ICMP 请求,所以必然超时。验证 Service 只能用 curlnc -zv 这类打到具体端口的工具。这也是为什么「ping 不通」不能作为 Service 有问题的证据。

自测:为什么默认拒绝写了 `Egress` 之后,域名解析会失败?(点击展开答案)

因为解析域名是出站行为:Pod 里的应用要向 kube-system 命名空间的 CoreDNS 发一个 UDP 53(必要时还有 TCP 53)的请求,CoreDNS 再把答案返回。默认拒绝一旦覆盖 Egress,所有出站流量都被拦下,DNS 请求自然发不出去,表现就是 nslookup 超时、应用报「名字解析失败」。修法是显式放通 CoreDNS:在 Egress 规则里用 namespaceSelector 选中 kube-system,并允许 UDP 与 TCP 的 53 端口。注意只写 Ingress 的默认拒绝不会有这个问题,因为 DNS 是出站。

小结

  • Kubernetes 网络模型只有三条要求:Pod 之间、节点与 Pod 之间免 NAT 互通,且 Pod 看到的 IP 一致。
  • Pod 网络由 CNI 负责,Service 网络由 kube-proxy 或 eBPF 负责,节点网络由机房负责,三者网段不能重叠。
  • 选 CNI 先看要不要 NetworkPolicy,再看要不要 BGP,最后看团队能否维护。
  • Cilium 用 eBPF 做转发与策略,配 Hubble 可观测,并能替代 kube-proxy。
  • NetworkPolicy 的正确姿势是「默认拒绝 + 最小放通」,且 ports 写容器端口。
  • 裸金属集群要靠 MetalLB 之类的方案才能让 LoadBalancer 类型的 Service 拿到 IP。

练习

  1. demo 里再加一条策略,让 redis 只接受来自 api 的 6379 端口访问,并用 client-other 验证被阻断。
  2. kubectl -n demo describe networkpolicy api-allow-web 观察策略解析结果,说说 podSelectorfrom 分别作用在谁身上。
  3. 把默认拒绝策略里的 policyTypes 改成 IngressEgress,再执行 kubectl -n demo exec client-web -- wget -qO- http://api:8080,解释为什么会失败,并补上放行 DNS 的规则。

想复习 Service 与集群内 DNS 的基础,回到第 7 章 Service;集群本身怎么装起来、网段怎么规划,在第 18 章 生产集群部署。网络不通但看不出原因时,排障手册里那套「先看状态再看事件」的顺序同样适用。

网络通了,数据落在哪?下一章我们看生产存储:CSI、Rook/Ceph 与本地盘方案。