课程目录(第 19 章 / 共 33 章)
集群网络:CNI 选型与 NetworkPolicy
搞清 Pod、Service、节点三层网络的分工,比较主流 CNI 的取舍,并亲手写出默认拒绝与最小放行的 NetworkPolicy。
学完这一章,你将能够
- ✓说得出 Kubernetes 网络模型的三条基本要求
- ✓从性能、策略、可观测、运维复杂度几个维度对比主流 CNI
- ✓写出默认拒绝所有入站、再按来源放通端口的 NetworkPolicy
- ✓知道裸金属集群为什么需要 MetalLB
Kubernetes 网络模型的三条要求
集群网络看起来复杂,但 Kubernetes 对它的要求只有三条,记住这三条,后面所有插件和策略都能挂上去。
- 任意两个 Pod 之间可以直接通信,中间不需要 NAT。
- 节点与 Pod 之间可以直接通信,中间不需要 NAT。
- Pod 看到的自己的 IP,和别人看到的它的 IP 是同一个。
这三条意味着:Pod 拿到的是「真实可用」的 IP,不需要在应用里做端口映射或地址转换。代价是必须有人替集群实现这套转发与地址分配,这就是 CNI 插件存在的理由。
三层网络:Pod、Service 与节点
同一个集群里其实叠着三张逻辑网络,它们由不同的人分配、由不同的组件转发。
| 网络 | 地址由谁分配 | 由谁负责转发 | 典型网段 |
|---|---|---|---|
| Pod 网络 | CNI 插件的 IPAM | CNI 的数据面 | 10.244.0.0/16 或自定义 |
| Service 网络 | kube-apiserver | kube-proxy 或 eBPF | 172.23.0.0/16 |
| 节点网络 | 机房或云平台 | 物理交换机与路由 | 10.0.0.0/16 |
两个硬约束:Service 网段和 Pod 网段不能重叠,也不能和节点网络、公司内网、VPN 网段重叠;而且这些网段在集群建好之后基本不可改。规划时先数清楚现网用掉了哪些地址。
用一张图记住三层的叠加关系,以及策略到底在哪一层生效:
同一台节点上叠着三张网络(示意)
节点网络 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 | 数据面 | NetworkPolicy | BGP | eBPF | 可观测 | 运维复杂度 | 适合场景 |
|---|---|---|---|---|---|---|---|
| Calico | iptables / eBPF,IPIP、VXLAN 或 BGP | 完整,含全局策略 | 原生支持 | 可选模式 | 中等 | 中 | 通用集群,需要和物理网络做 BGP |
| Cilium | eBPF | 完整,含 L7(HTTP/gRPC) | 支持 | 原生 | Hubble,强 | 中高 | 新集群,追求性能与可观测 |
| Flannel | VXLAN 或 host-gw | 不支持 | 无 | 无 | 弱 | 低 | 学习、小规模环境 |
| Kube-OVN | OVS / 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 规则链。
最小安装命令如下(先确认可用版本再装):
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=truek8sServiceHost 必须填 API Server 的直连地址(VIP 或节点 IP),因为 Cilium 启动时还没有 Service 网络可用。另外,kubeProxyReplacement=true 要求集群里不能同时跑 kube-proxy,如果还留着,就先设为 false,或者移除 kube-proxy 后再开。
kubectl -n kube-system rollout status ds/cilium
kubectl -n kube-system get pods -l k8s-app=cilium
kubectl get nodes -o wideds/cilium 的副本数等于节点数且全部就绪,kubectl get nodes 里所有节点 Ready,就说明 Pod 网络已经通了。浏览器看服务依赖图用 kubectl -n kube-system port-forward svc/hubble-ui 12000:80。
Cilium 进阶:可观测、加密与 BGP
装完只是开始,生产上还要用三件事:看清流量、加密流量、把网段宣告出去。
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=wireguardencryption.type 可选 wireguard 或 ipsec,WireGuard 要求节点内核 5.6 以上。它保护的是节点之间链路上的明文,不等于应用层 mTLS,两者可以叠加,见 服务网格。
BGP 用来把 Pod 网段或 Service 的 VIP 直接宣告给机房交换机,让外部设备不经 SNAT 就能访问 Pod,代价是需要网络管理员分配 AS 号与邻居地址。配置对象随版本演进(旧版 CiliumBGPPeeringPolicy,新版 CiliumBGPClusterConfig 加 CiliumBGPPeerConfig),写法以你安装版本的文档为准。
两个必须提前知道的坑: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 封装开销:
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:
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.md、reference/k8s-in-action/network/network-operator/macvlan/README.md
IP 池与固定 IP:Spiderpool
underlay 网络默认「随便挑一个空闲地址」,但生产上有三类需求它满足不了:地址要提前报备给网络管理员、有状态应用重启后 IP 不能变、IP 必须落在指定网段(比如必须与同组交换机在同一个子网)。Spiderpool 就是补这块的:给 macvlan、ipvlan、SR-IOV 这类 CNI 提供统一的 IP 池与固定 IP 能力。
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.20Pod 上再加一行注解指向这个池: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 命名空间里放 web、api、redis 三个应用,api 通过 Service 的 8080 端口暴露,再准备两个客户端 Pod,分别带 app=web 和 app=other 标签——后面靠它们的标签决定谁被放通。
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:
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] 表示只管入站。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: demo
spec:
podSelector: {}
policyTypes:
- Ingresskubectl 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 端口。
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 的话策略永远匹配不上,流量照样被拒。
验证放通与阻断:
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 networkpolicyclient-web 会返回 nginx 的欢迎页 HTML;client-other 则报连接超时(wget: download timed out 或 nc: ... 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 下更稳定 |
| eBPF | Cilium 等在内核 hook 直接处理 | 绕过 iptables/IPVS,性能与可观测更好 |
查看当前集群用的模式:
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 在我这台机器上」宣告给交换机。
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 段要找网络管理员要空闲的:
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:
- defaultkubectl 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 nginxEXTERNAL-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 根本不会被宣告。
# 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 段与机房路由表 | 重建集群并重新规划网段,这是最难事后修的坑 |
LoadBalancer 的 EXTERNAL-IP 一直 <pending> | 裸金属没有云负载均衡器 | kubectl -n demo get svc 看 TYPE 与 EXTERNAL-IP | 装 MetalLB 或改用 NodePort / Ingress |
两个最容易造成事故的细节
- 策略写反:
from里要写来源的选择器,不是目标的。给api放通web,podSelector在顶层选api,from里选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 只能用 curl、nc -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。
练习
- 在
demo里再加一条策略,让redis只接受来自api的 6379 端口访问,并用client-other验证被阻断。 - 用
kubectl -n demo describe networkpolicy api-allow-web观察策略解析结果,说说podSelector与from分别作用在谁身上。 - 把默认拒绝策略里的
policyTypes改成Ingress加Egress,再执行kubectl -n demo exec client-web -- wget -qO- http://api:8080,解释为什么会失败,并补上放行 DNS 的规则。
想复习 Service 与集群内 DNS 的基础,回到第 7 章 Service;集群本身怎么装起来、网段怎么规划,在第 18 章 生产集群部署。网络不通但看不出原因时,排障手册里那套「先看状态再看事件」的顺序同样适用。
网络通了,数据落在哪?下一章我们看生产存储:CSI、Rook/Ceph 与本地盘方案。