7.8 网络模型 (CNI / NetworkPolicy)

一句话

K8s 的网络模型只有三条要求——每个 Pod 一个 IP、Pod 之间不用 NAT 就能直通、节点也能直接访问 Pod IP;CNI 决定 “怎么实现这三条”,NetworkPolicy 决定 “谁能连谁”,Service 只是在这之上加的一层虚拟 IP。

三条要求,三套实现

要求 谁实现 实现方式
每个 Pod 一个独立 IP CNI 插件 + IPAM 从节点 PodCIDR 里分配,或直接用 VPC 子网(云 CNI)
跨节点直通、不 NAT 路由或隧道 VXLAN/IPIP 封装,或 BGP 宣告 Pod 网段(underlay)
节点与 Pod 互通 CNI + 路由 节点上装路由/iptables 规则,或云路由表

CNI 是一个规范:容器运行时调用 ADD/DEL/CHECK,插件返回网卡与 IP。所以 “换网络方案” 通常只是换一个 CNI 二进制 + 一套 IPAM,Pod 清单不用改。

三条路径,各自的 NAT 与坑

路径 实际转发 常见坑
Pod → Pod(同节点) veth → 网桥(或 eBPF)→ veth 网桥被 net.bridge.bridge-nf-call-iptables 影响,策略失效
Pod → Pod(跨节点) 封装(VXLAN/IPIP)或 BGP 路由 MTU 未减封装开销 → 大包不通(见 6.7 容器存储与网络)
Pod → Service DNAT 到某个 Endpoint + conntrack 记录 conntrack 表满、会话粘滞、Endpoints 摘除竞态
外部 → Service NodePort / LB → SNAT → Pod 源 IP 丢失(除非 externalTrafficPolicy: Local)

conntrack 是最容易被忽略的容量项:每个连接占一条表项,nf_conntrack_max 打满会丢包且日志稀少。高连接数场景(网关、长连接)要单独调大并监控。

NetworkPolicy:默认全通,且需要 CNI 支持

事实 含义
默认全通 不写任何 NetworkPolicy 时,所有 Pod 互相可达
策略是白名单 一旦某个 Pod 被某条策略的 podSelector 选中,它的对应方向就变成 “只允许策略里写的”
需要 CNI 支持 Flannel 默认不支持(会静默忽略),Calico / Cilium 支持
分方向 只写 Ingress 时,出站仍然全通;出站管控要单独写 Egress

“默认拒绝 + 按需放行” 的标准模板(注意先放行 DNS,否则域名解析全挂):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: default-deny}
spec:
  podSelector: {}                    # 本命名空间所有 Pod
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: allow-dns-and-web}
spec:
  podSelector: {matchLabels: {app: api}}
  policyTypes: [Egress]
  egress:
    - to:                            # 先放行 DNS(kube-system 的 CoreDNS)
        - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: kube-system}}
      ports: [{protocol: UDP, port: 53}, {protocol: TCP, port: 53}]
    - to:
        - podSelector: {matchLabels: {app: db}}
      ports: [{protocol: TCP, port: 5432}]

验证方式是实测,不是看 YAML:从源 Pod 里 nc -zv <目标IP> <端口>,通了就是没生效(或策略写错),不通才算隔离成立。

DNS:集群里最常被忽略的延迟源

现象 原因 修法
首次调用外部域名慢 100–300ms /etc/resolv.conf 的 ndots:5:外部域名先试 5 个搜索域 设 dnsConfig.options: [{name: ndots, value: "2"}]
解析随机失败 CoreDNS 副本不足或被打满(限流/连接数) 加副本、开 NodeLocal DNSCache
跨命名空间解析失败 用了短名(db),只在本命名空间生效 用 FQDN:db.ns.svc.cluster.local
Egress 策略后解析全挂 没放行到 CoreDNS 的 53 端口 见上例的第一条 egress 规则

反模式

反模式 后果
以为配了 NetworkPolicy 就隔离了 CNI 不支持时策略被静默忽略,集群实际全通
只写 Ingress 不写 Egress 出站全通,横向移动不受限
“默认拒绝” 后忘了放行 DNS 所有域名解析失败,表现为 “服务全挂”
用 ICMP/ping 验证 Service 连通 Service IP 是虚拟地址,ICMP 通常被丢,误判故障
忽略 conntrack 上限 高并发下随机丢包,日志里几乎没有线索
跨节点 MTU 未调整 小包通、大包不通(传文件/上传必现)
用 Pod IP 做服务发现 Pod 重建后 IP 变化,调用方全挂

三条铁律

🔑 默认全通:不做策略就是全开;做了策略要实测,不能只看 YAML。

🔑 策略先放行 DNS 与探针,再收紧业务流量;顺序反了会把自己锁在门外。

🔑 Service 只是虚拟 IP:连通性排查看 Endpoint 与 conntrack,不要用 ping 判断。

自检

Q1 从 Pod A curl Pod B 的 IP 超时,但 curl B 的 Service 正常,为什么?

▸ 答案:说明Pod IP 直连被策略或路由挡住,而 Service 路径(DNAT → Endpoint)是通的。常见原因:① NetworkPolicy 只放行了 Service 网段/端口,没放行 Pod 网段;② 跨节点路由/BGP 未宣告该 PodCIDR;③ 目标 Pod 的容器端口只监听在 localhost。按 “策略 → 路由 → 监听地址” 三步查。

Q2 配了 “默认拒绝所有入站” 之后,服务全部 502。最先查什么?

▸ 答案:先查探针与 DNS。默认拒绝会连 kubelet 的健康检查(来自节点 IP)一起拒掉,导致 Pod 一直 NotReady、Endpoint 为空 → 502;同时如果没放行 DNS(Egress 方向),所有域名解析失败。这两条是 “默认拒绝” 的第一批受害者,模板里必须显式放行。

Q3 为什么 ping <ClusterIP> 不通,但服务是好的?

▸ 答案:因为 ClusterIP 是 kube-proxy/eBPF 在数据面实现的虚拟地址,只对 TCP/UDP 做 DNAT;ICMP 通常不被处理,所以 ping 无响应。正确验证方式是 nc -zv <ClusterIP> <port> 或 curl,再看 endpointslices 是否有地址。

Q4 一个外部域名解析要 200ms,怎么把它降到个位数?

▸ 答案:根因通常是 ndots:5:api.example.com 只有 2 个点,会先被拼成 api.example.com.<ns>.svc.cluster.local 等 5 个候选,全部失败后才走真实域名,每次多 5 次查询。把 dnsConfig.options 的 ndots 调到 2,并在热点路径用 NodeLocal DNSCache,通常能把首次解析压到 10ms 内。

Q5 两个团队都说 “网络不通”,你怎么在 10 分钟内定位到层?

▸ 答案:按四层问:① DNS:nslookup/getent hosts 能否解析;② Service:endpointslices 有没有地址;③ 策略:kubectl get networkpolicy -A 有没有选中这两端;④ 数据面:同节点直连 Pod IP 是否通(排除封装/MTU/路由)。四步定位把 “网络不通” 拆成具体一层,避免在 “是不是 CNI 坏了” 上空转。

跨篇相关:Pod 网络与 CNI 插件的对比见 6.7 容器存储与网络;Service 的四种类型与转发模式见 7.3 Service 与 Ingress;四层与七层的分工见 8.9 四层与七层负载均衡;TCP 重传、MTU 与握手见 8.1 TCP/IP 与三次握手;VPC 与安全组的边界见 8.7 VPC、子网、安全组;服务网格下的东西向流量见 30.5 Service Mesh 与 Sidecar。