第 9 章 网络模型:Pod 互通、Service、DNS、Ingress

墨:老哥,这章是很多人弃坑 K8s 的地方。我先说句实话——K8s 网络不是 K8s 发明的,它只是定了一组”规矩”,然后让插件去填。 你怕的不是网络本身,是”规则多还没人讲清为什么”。今天我把”为什么”讲透,规则就自己长出来了。


9.1 先立规矩:K8s 对网络提了哪三条铁律

K8s 官方给网络模型定下三个不妥协的要求(你记死这三条,后面所有设计都从它们长出):

  1. 每个 Pod 有自己独立的 IP(IP-per-Pod),且集群里所有 Pod 的 IP 在一个扁平、可直接路由的地址空间里;
  2. 任意两个 Pod 可以不经 NAT 直接互通(Pod A 访问 Pod B 的 IP,源 IP 就是 A 的 IP,中间不篡改);
  3. Pod 内所有容器共享这个 Pod IP,且节点上的 Pod 和节点自身也能互通(至少不 NAT)。

墨:你先品,这三条”反直觉”在哪? 反直觉的是第一条——Docker 默认可是”每个容器在私有网桥、靠端口映射才能出网”的。K8s 偏要”每个 Pod 一个真 IP、扁平互通”。为什么?

因为如果 Pod 没有稳定 IP、要靠 NAT/端口映射,那”服务发现”和”网络策略”全没法做干净。你想:调用方要记 Pod 的 IP(稳定),如果 IP 每次都变、还要映射,等于回到物理机时代的混乱。K8s 的赌注是:给每个 Pod 一个真 IP,让网络”看起来像没容器一样”——Pod 之间像两台真机直连。这样上层(Service、DNS、NetworkPolicy)全建立在”Pod 有真实 IP”这个简单假设上,复杂度被压到最低。

设计哲学:在底层做一个”简单到无聊”的假设(Pod = 有 IP 的机器),把复杂度留给上层插件。 如果底层就乱(NAT、端口映射、私有网段),上层每个功能都要补丁。简单底层假设 = 全局省复杂度。


9.2 那”扁平网络”谁来实现?K8s 自己不做——交给 CNI

墨:K8s 定完规矩,那”给 Pod 分配 IP、让 Pod 跨节点互通”这活谁干? K8s 自己不干——它只定义接口 CNI(Container Network Interface):一组给”容器运行时”调用的标准操作(ADD 容器上网、DEL 容器下线)。任何网络方案只要实现 CNI,就能给 K8s 用。

为什么开放?因为网络是强依赖基础设施环境的:你在 AWS、在裸金属、在 OpenStack,底层网络完全不同。K8s 不可能为每种环境写一套网络。它定义”我要的效果”(IP-per-Pod、扁平互通),具体”怎么在两台物理机间打通 Pod 网络”交给插件(Calico、Flannel、Cilium、Weave…)。又是”核心定接口、实现可插”的祖传哲学(第 3、4、6 章都见过)。

9.2.1 主流方案的分类(你扫一眼思路,不用背)

  • Overlay(覆盖网络),如 Flannel(VxLAN)、Calico(in IPIP 模式):在真实网络之上再封一层隧道(把 Pod 包封进节点 IP 的包里传)。优点:不依赖底层网络支持,啥环境都能跑;缺点:封包解包有开销,性能略损。
  • 路由方案(BGP),如 Calico 原生:让每个节点向网络宣告”这些 Pod IP 归我这台机器”,靠路由协议让别的机器知道怎么到这些 Pod。优点:无封包、性能好;缺点:要求底层网络支持 BGP 或你能控制路由。
  • Underlay(直接挂底层),如基于 SR-IOV / 云厂商 ENI 分配:Pod 直接用底层网络的 IP。性能最好,但灵活性低。

墨:你公司用云(AWS/阿里云),Pod 网络大概率走的哪种? 云上多用 VPC 原生路由或云厂商的 CNI 插件(如 AWS VPC CNI 直接给 Pod 分配 VPC 内 IP,属于 underlay 思路),因为云已经提供了三层互通,无需 overlay。裸金属/混合云更可能 overlay 或 BGP。选型本质是”性能 vs 部署简单”的权衡,环境决定上限。


9.3 Service:那个”虚拟 IP”到底是什么

Pod IP 会随 Pod 重建而变(Pod 死了 IP 回收,新 Pod 新 IP)。调用方不能绑死 Pod IP。于是 Service 出场:它给一组 Pod 一个稳定的虚拟 IP(ClusterIP),调用方只认这个 VIP,背后 Pod 怎么变都不影响。

墨:这 VIP 背后没人接(不是进程、不是网卡),数据包打给它,怎么到真正的 Pod? 这是全章最核心的机关。答案是:kube-proxy 在每台节点上写了一套 iptables(或 ipvs)规则,把”访问 VIP:端口”偷偷转发(DNAT)到某个真实 Pod IP。

换句话说——Service 的 VIP 不是个实体,是一堆”网络规则”的代称。 没有进程在监听 VIP,是节点内核的 netfilter 规则在”截胡”去往 VIP 的包,改写到后端 Pod。

9.3.1 iptables 模式(经典实现)

kube-proxy 监听 API Server 的 Service 和 Endpoint 变化(第 4 章 List-Watch),在节点上维护类似这样的规则:

# 访问 10.96.0.10:80(Service VIP)的包
iptables -t nat -A KUBE-SERVICES -d 10.96.0.10 --dport 80 -j KUBE-SVC-XXX
# KUBE-SVC-XXX 里用概率规则随机 DNAT 到某个端点
iptables -t nat -A KUBE-SVC-XXX -j DNAT --to-destination <PodIP1>:80
iptables -t nat -A KUBE-SVC-XXX -j DNAT --to-destination <PodIP2>:80

调用方发往 VIP 的包,在内核态被改写目的 IP 为某 Pod IP,然后正常路由出去。 全程应用无感知,没有代理进程中转(所以叫 “iptables 模式”,不是”代理转发”)。

墨:你说”随机 DNAT”,那负载均衡(LB)不就自动有了? 对。iptables 的 statistic 模块按概率选后端,天然实现”轮询式”负载均衡。Service 一出生就自带 LB——你不用额外搞 nginx 做负载均衡(对集群内服务而言)。

9.3.2 iptables 模式的瓶颈 → ipvs 的演化

iptables 在大集群下露怯:每个 Service 一串规则,链越来越长。一个节点上有几千个 Service、上万个端点,iptables 规则可能几万条。包每过一个链要线性遍历匹配,规则多了,每个包的处理延迟和 kube-proxy 刷规则的耗时都飙升。更要命:iptables 规则是”全量刷新”,Endpoint 一变,kube-proxy 重写一堆规则,CPU 抖动。

演化(K8s 1.8+ 引入 ipvs 模式,1.11 GA): ipvs 用内核的哈希表(IP Virtual Server)做转发,查找是 O(1),不随规则数线性变慢。而且 ipvs 支持更多 LB 算法(rr/wlc/…),刷新增量。所以大集群 kube-proxy 用 ipvs 模式明显更稳更快。

墨:那小集群用 iptables 还是 ipvs? 小集群(规则少)差别不大,iptables 够。但 ipvs 依赖内核模块 ip_vs,某些极简环境没这模块,得退回 iptables。所以默认现在很多发行版直接 ipvs。这又是”规模逼出更优实现”的演化——iptables 在小规模是简单解,规模大了被 ipvs 取代,和 etcd v2→v3、Docker→containerd 同一种剧本:老方案在小规模 OK,规模/需求变了,新方案补位。

9.3.3 userspace 模式(已被弃,提一句)

最早的 kube-proxy 是真的在用户态起个代理进程转发流量(userspace 模式)。缺点明显:每个包都要从内核拷贝到用户态再拷回去,性能极差,早就弃用。提它是让你看到演化全貌:userspace(慢)→ iptables(内核态但规则线性)→ ipvs(内核哈希 O(1))。三条路都是”把 VIP 翻译成 Pod IP”,只是性能和复杂度逐代优化。


9.4 kube-proxy:它到底是不是”代理”

墨:名字叫 kube-proxy,它真在代理流量吗? 看你用哪种模式:

  • userspace 模式:是真代理(进程中转流量);
  • iptables / ipvs 模式:不是代理。kube-proxy 只是个”规则维护者”——它 watch 端点变化,去节点上增删 iptables/ipvs 规则。流量是内核直接转发的,不过 kube-proxy 的手。

所以 kube-proxy 的本质是 “数据面规则的控制器”(又是控制器模式!),不是流量中转站。这纠正一个常见误解:Service 的负载均衡是内核做的,不是 kube-proxy 做的。 kube-proxy 只负责”把规则放对”。

演化看点: 新一代方案(如 Cilium 用 eBPF)连 kube-proxy 都省了——直接用 eBPF 程序挂在内核做 Service 转发和负载均衡,绕过 iptables/ipvs。这是”用更底层的内核技术替换中间层”的又一次演进(eBPF 还能做 NetworkPolicy、可观测性,第 17 章提)。趋势:网络数据面持续往内核下沉、往更高效演进。


9.5 DNS:让”服务名”代替”VIP”

你写代码调服务,总不能硬编码 10.96.0.10 这种 VIP 吧?而且 VIP 虽稳,但不同环境(dev/test/prod)VIP 不同,硬编码不跨环境。所以 K8s 用 DNS:每个 Service 自动注册一条 DNS 记录(my-svc.my-namespace.svc.cluster.local → VIP)。你代码里写 http://orders:8080(同 namespace 可省后缀)就行。

谁提供 DNS?CoreDNS(早期的 kube-dns 被取代)。CoreDNS 是个 Pod,跑在集群里,监听 Service 变化,动态维护 DNS 记录。它自己也是个 Service(VIP 通常 10.96.0.10,写在每个 Pod 的 /etc/resolv.conf 的 nameserver 里)。

墨:Pod 怎么知道该问哪个 DNS? kubelet 起 Pod 时,往它的 /etc/resolv.conf 写 nameserver 10.96.0.10 和搜索域(search default.svc.cluster.local 等),所以你写 orders 会被自动补成 orders.default.svc.cluster.local 去查。这又是一处”kubelet 负责注入基础设置”的例子(类似注入 SA token、pause 网络)。

演化看点: kube-dns → CoreDNS。kube-dns 是多容器拼的(dnsmasq + 查询组件),容易出缓存问题。CoreDNS 是单二进制、插件化配置(Corefile),更稳更灵活,且能被用于集群外解析。又是”多组件拼 → 单二进制更稳”的演进。

9.5.1 按 Pod 名解析(headless)

普通 Service 解析到 VIP(由 kube-proxy 负载均衡到某 Pod)。Headless Service(clusterIP: None)不分配 VIP,DNS 直接返回所有后端 Pod 的 IP 列表。调用方拿到列表,自己选(或用于 StatefulSet 的 mysql-0.mysql 精确寻址,第 7.4.4)。这是”要负载均衡”和”要精确访问某实例”两种语义的分叉——DNS 一层就支持了。


9.6 把服务暴露到集群外:NodePort / LoadBalancer / Ingress

Service 默认是 ClusterIP(只在集群内可访问)。要对外,三种方式:

9.6.1 NodePort

在每个节点上开一个固定端口(如 30080),访问”任意节点 IP:30080”都转发到 Service。原理:kube-proxy 在节点上写规则,把 NodePort 流量导到 ClusterIP。简单,但端口范围有限(30000-32767),且你直接暴露节点 IP 不优雅(节点 IP 也会变)。

9.6.2 LoadBalancer

在云上,创建一个云厂商的负载均衡器(如 AWS ELB),自动把流量引到节点 NodePort。这是”对接云基础设施”的经典:K8s 的 LoadBalancer 类型触发 cloud-controller-manager 去云 API 申请一个 LB,再绑定。你 kubectl get svc 看到 EXTERNAL-IP 就是云 LB 的地址。

墨:没有云(裸金属)怎么办 LoadBalancer? 用 MetalLB 这类项目模拟——它在裸金属上”扮演云厂商”,分配一个真实可达的 IP 当 LB。本质还是”用插件补齐云能力缺失”,呼应 CNI 的思路:K8s 定义接口,环境差异由插件填。

9.6.3 Ingress:L7 的”智能入口”

NodePort/LB 都是 L4(TCP/UDP),不懂 HTTP。你要的是”按域名/路径路由”:a.com 进服务 A,b.com/api 进服务 B——这是 L7(HTTP) 需求。Ingress 就是干这个的:它定义”域名/路径 → 后端 Service”的路由规则,由一个 Ingress Controller(如 nginx-ingress、Traefik、Envoy)实现。

墨:Ingress 和 Service 啥关系?Ingress 是”更高级的 Service”吗? 不是。Service 解决”集群内稳定寻址+LB”,Ingress 解决”集群外 HTTP 路由”。Ingress 的后端指向 Service(它把外部 HTTP 请求按规则转给对应 Service 的 VIP),所以它是”站在 Service 前面的 L7 路由器”。一个 Ingress Controller(Deployment) + 一个 LoadBalancer(把外网流量引到 Ingress Controller)是常见拓扑:外网 → LB → Ingress Controller → 按规则 → Service → Pod。

演化看点: 后来有了 Gateway API(取代/增强 Ingress):Ingress 太简单(只能路径/域名),表达不了”按 Header 路由、按权重灰度、按角色分权”。Gateway API 把”路由”拆成 Gateway(基础设施层)+ HTTPRoute(应用层),且面向角色(平台组管 Gateway、应用组管 Route),权限更清晰。又是”简单原语 → 表达力不足 → 更丰富 API”的演进(呼应 GVK 多版本、CRD)。


9.7 NetworkPolicy:默认全通,你要自己锁

墨:默认情况下,Pod A 能访问 Pod B 吗?集群里任意 Pod 能访问任意 Pod 吗?

默认全通。K8s 网络模型不限制 Pod 间通信(只要路由通),所以任意 Pod 能访问任意 Pod——包括”前端 Pod 能直接打到数据库 Pod”,或”被攻陷的 Pod 能横向移动扫全场”。这在安全上是裸奔。

NetworkPolicy 是”网络层面的防火墙规则”:声明”哪些 Pod 允许被哪些 Pod 访问”。比如”只有带 app=frontend 的 Pod 能访问 app=mysql 的 3306 端口”。

关键坑(必记): NetworkPolicy 是**“白名单 + 依赖 CNI 支持”。第一,它只定义”允许”,不匹配的流量默认行为取决于 CNI 实现——但默认的”全通”不会因为写了某条 policy 就变全禁**;要让”没匹配的就是拒绝”,需要 CNI 支持默认拒绝(如 Calico 配默认拒绝策略)。第二,如果你的 CNI 不支持 NetworkPolicy(部分 overlay 插件早期不支持),你写了也没用——规则是纸。所以”网络隔离”能力实际上由 CNI 决定,不是 K8s 核心保证。

墨:这又体现了什么设计取向? “安全隔离”被做成”可插的能力”,核心只定义 API(NetworkPolicy 对象),真正实现下沉到网络插件。和 RBAC(API 层鉴权)互补:RBAC 管”能不能调 API”,NetworkPolicy 管”Pod 间能不能通”——一个是控制面权限,一个是数据面隔离,两层防护。


9.8 把全章串成一张图(文字版)

外部用户
   │ (HTTP)
   ▼
LoadBalancer (云 LB) / NodePort
   ▼
Ingress Controller (L7 路由,按域名/路径)
   ▼
Service (ClusterIP,VIP,iptables/ipvs 做 DNAT + LB)
   ▼
Pod (真实 IP,CNI 打通的扁平网络)
   │
   └─ Pod 内容器通过 pause 共享的网络互相 localhost 通
   └─ 跨 Pod 通信靠 CNI 提供的路由/隧道
   └─ 服务名解析靠 CoreDNS

每一层都是”解决一类麻烦”:

  • CNI:让 Pod 有 IP、能互通(底层连通);
  • Service:给稳定 VIP + 自带 LB(寻址稳定 + 负载均衡);
  • CoreDNS:把 VIP 变成名字(人类友好、跨环境);
  • Ingress/LB:把内部服务按 L7/L4 暴露出去(对外可达);
  • NetworkPolicy:在 Pod 间加防火墙(最小暴露)。

墨:你看,网络这一大坨,其实就这五件事,各管各的。你之前怕,是因为没人告诉你”它本来就被切成几块、每块只干一样”。


9.9 本章演化线小结

  • 网络三铁律:IP-per-Pod、扁平互通、不 NAT——简单底层假设压低全局复杂度;
  • CNI 把”连通”做成插件,环境差异(云/裸金属)由插件填;
  • Service VIP = iptables/ipvs 规则代称,不是实体;kube-proxy 是规则控制器不是流量代理;
  • 数据面演化:userspace → iptables(线性)→ ipvs(O(1) 哈希)→ eBPF(内核下沉);
  • CoreDNS 取代 kube-dns,单二进制更稳;
  • 对外:NodePort/LB(L4) → Ingress(L7) → Gateway API(更丰富、面向角色);
  • NetworkPolicy 是白名单,能力取决于 CNI,与 RBAC 互补(数据面 vs 控制面)。

9.10 本章思考题

🔧 思考题 1

Service 的 ClusterIP 不是实体(没人监听),但你在 Pod 里 curl 10.96.0.10:80 能通。请画出包的路径:从你的 Pod 进程发起到到达后端 Pod,经过内核的哪些处理?为什么应用代码感知不到”被改了目的 IP”?

【参考答案】 路径:(1) Pod 内进程发往 10.96.0.10:80,包带源 IP=PodA、目的=VIP;(2) 出 Pod 网络栈走到节点内核 netfilter(iptables nat 表 PREROUTING/OUTPUT 链);(3) kube-proxy 写的 KUBE-SERVICES 规则匹配到 VIP:80,命中 KUBE-SVC-XXX 链;(4) 该链用 statistic 模块随机选一后端,做 DNAT:把目的 IP 改写为某 PodB IP(端口可能也改),同时记录 conn-track 连接;(5) 改写后的包按节点路由表发往 PodB(同节点直送、跨节点走 CNI 路由/隧道);(6) PodB 收包,看到的源 IP 仍是 PodA(SNAT 与否取决于是否跨节点——同节点通常不 SNAT,跨节点常 SNAT 成节点 IP,这是另一个话题);(7) 回包经 conn-track 自动反向 DNAT 回 VIP,回到 PodA。应用无感知原因:DNAT 在内核态完成,且 conntrack 保证回包能正确还原,对应用而言”我连的 VIP 有响应”而已,根本不知道中间被改写。这题考你对”Service = 内核 DNAT 规则”的本质理解。

🔧 思考题 2

某集群有 5000 个 Service、每个平均 10 个端点 = 50000 条 Endpoint。kube-proxy 用 iptables 模式。当其中一个 Pod 重启(Endpoint 变),会发生什么性能问题?换成 ipvs 模式后会怎样?为什么 ipvs 是 O(1)?

【参考答案】 iptables 模式问题:(1) 规则总量巨大(5 万端点 × 多链规则,可能数十万条 iptables 规则);(2) 单个包经 KUBE-SERVICES 链要线性匹配所有 Service 规则直到命中,规则越多单包延迟越高;(3) Endpoint 变化时,kube-proxy 要全量重算并重写相关链(即使只变一个端点,相关 Service 的规则整体刷新),引发 CPU 峰值和规则重建抖动,且大规模下刷新耗时显著。ipvs 模式:用内核 哈希表(IPVS 的 virtual service 表),查 VIP:端口是 O(1) 哈希查找,不随规则数线性增长;且 ipvs 支持增量更新(只改变的 virtual service/endpoint),不全量刷。所以 5 万端点下,ipvs 单包转发延迟稳定、Endpoint 变更开销小。本质:iptables 是”链表匹配”,ipvs 是”哈希索引”——数据量一大,索引结构决定性能量级。这也解释了为什么大集群必用 ipvs(或 eBPF)。

🔧 思考题 3

你写了条 NetworkPolicy:“允许 frontend 访问 mysql:3306”。但集群里用的是某 overlay CNI,且没配默认拒绝。结果 frontend 能访问 mysql,但另一个无关 Pod 也能访问 mysql。为什么?怎么让它”只有 frontend 能访问”?

【参考答案】 NetworkPolicy 是白名单(只允许匹配项),但它的”默认行为”取决于 CNI 是否实现”默认拒绝”。如果 CNI 不支持 NetworkPolicy 或没开默认拒绝,不匹配任何 policy 的流量仍按底层默认”全通”放行——所以无关的 Pod 能通。写一条 policy 不会自动把其他流量 ban 掉(K8s 的 NetworkPolicy 语义是”增白名单”,不是”切换成默认禁”)。要”只有 frontend 能访问 mysql”,需:(1) 确认 CNI 支持 NetworkPolicy(Calico/Cilium 等);(2) 配一个 default-deny 策略(如对该 namespace 或 mysql Pod 设 “deny all ingress”,再叠加允许 frontend 的 policy)——白名单 + 默认拒绝双管齐下才是”最小暴露”;(3) 注意 NetworkPolicy 的 podSelector 和 namespaceSelector 要选对 mysql 和 frontend。启示:NetworkPolicy 的效果完全依赖 CNI 实现,“写了 policy”≠“网络被隔离”,务必验证 CNI 支持和默认拒绝策略。

🔧 思考题 4(进阶)

eBPF 方案(如 Cilium)号称能”去掉 kube-proxy、去掉 iptables”。它是怎么做到 Service 转发和 NetworkPolicy 的?从”在内核哪个钩子点拦截”的角度,对比 iptables 方案和 eBPF 方案的本质差异。

【参考答案】 iptables 方案:kube-proxy 用户态进程 watch Endpoint,把规则写进内核 netfilter(iptables) 的 PREROUTING/OUTPUT 链,包走到这些链时被 DNAT。缺点:规则线性匹配、全量刷新、每个包都过通用 netfilter 框架。eBPF 方案:Cilium 用 eBPF 程序直接挂载在内核网络栈的钩子点(如 tc / socket / cgroup),在包处理早期(甚至 socket 层 sockops)就做”VIP→PodIP”的改写和 LB 选择,完全绕过 iptables/netfilter 和 kube-proxy 用户态。本质差异:(1) 位置更靠前、更专用:eBPF 在更精准的钩子做,不必走通用 iptables 链;(2) 程序化、上下文感知:eBPF 程序能用 map 存端点信息,直接 O(1) 查、按需更新,且能做 iptables 难做的复杂逻辑(如基于身份的路由、L3-L7 策略、可观测性埋点);(3) 无 kube-proxy 进程:规则维护由 Cilium agent 通过 eBPF map 增量更新,不再依赖 kube-proxy 刷 iptables。代价:要求较新内核、调试工具链不同。这是”用可编程内核(eBPF)替换固定框架(netfilter)“的演进,趋势是数据面持续内核化、可编程化——和 ipvs 取代 iptables 同方向,但更彻底(连用户态代理/规则维护都省了)。


9.11 小结

  • 网络三铁律(IP-per-Pod / 扁平 / 不 NAT)用简单底层假设压低全局复杂度;
  • CNI 把”连通”做成插件,环境差异交给 Calico/Flannel/Cilium 等;
  • Service VIP = iptables/ipvs 规则代称,kube-proxy 是规则控制器非流量代理;
  • 数据面演化 userspace → iptables → ipvs(O(1)) → eBPF(内核下沉);
  • CoreDNS 管”服务名→VIP”,headless 返回 Pod 列表供精确寻址;
  • 对外:NodePort/LB(L4) → Ingress(L7) → Gateway API(更富、面向角色);
  • NetworkPolicy 是白名单、效果依赖 CNI,与 RBAC 互补(数据面 vs 控制面)。

下一章讲存储——有状态应用的尊严。网络解决了”通”,存储解决”数据不丢”。Pod 会漂,盘怎么跟着走?PVC/PV/SC/CSI 这套又是怎么从”手动申请”演成”自动供给”的。