阶段一 · 网络:从一个容器讲起

容器网络的基石

一句话总结

容器的"独立网络"不是魔法, 而是 Linux 内核三个原语的组合: network namespace 给容器一个隔离的网络栈, veth pair 像一根虚拟网线把它连出来, 网桥像一台虚拟交换机让同主机的容器互通.
搞懂这三件事, Pod 网络就不再是黑盒.

这个系列从哪讲起

Kubernetes 分类里我们已经会用 Service, 会读 Pod IP. 但那是"会用"--本系列要回答"底层到底怎么实现的".

一切的起点是一个最朴素的问题: 一台主机上的两个容器, 各自以为自己独占网络, 它们之间到底是怎么通信的? 把这个搞透, 后面跨节点, CNI, eBPF 才有地基.

从一个最基本的问题出发

先抛开 Kubernetes, 就看一台 Linux 主机上的两个容器:

容器 A 执行 ip addr, 看到自己有 eth0, IP 是 10.244.0.2.
容器 B 执行 ip addr, 看到自己也有 eth0, IP 是 10.244.0.3.

诡异之处:

  1. 两个容器都有自己独立的网卡, IP, 路由表, 防火墙规则
  2. 但它们明明跑在同一台主机上, 共用一个内核, 一张物理网卡
  3. 它们还能互相 ping 通

这怎么做到的? 容器的"独立网络"是真的独立网卡吗?(不是)

答案藏在 Linux 内核里. 容器的网络隔离, 完全是内核提供的几个机制"拼"出来的假象--而理解这个"拼"的过程, 就是理解容器网络的全部.

原语一: network namespace -- 隔离的网络栈

network namespace(网络命名空间) 是 Linux 内核的隔离机制: 它让一组进程拥有完全独立的一套网络栈--独立的网卡, IP 地址, 路由表, iptables 规则, 端口空间.

默认情况下, 整台主机共享一个(默认)network namespace. 但可以创建新的: 每个新 namespace 都是一张"白纸", 里面只有一个 loopback, 看不到主机的网卡, 也看不到别的 namespace.

# 创建一个名为 ns1 的 network namespace
$ ip netns add ns1

# 在 ns1 里执行命令: 它看到的是一套全新的, 隔离的网络
$ ip netns exec ns1 ip addr
1: lo: <LOOPBACK> ... # 只有 loopback, 什么网卡都没有, 完全隔离

关键认知: 容器的'独立网络'就是一个 network namespace

所谓"容器有自己的网络", 本质就是容器运行时为它创建了一个独立的 network namespace, 容器里的进程都跑在这个 namespace 里. 它看到的 eth0, IP, 路由, 全是这个 namespace 内部的, 和主机, 和别的容器互不干扰.

namespace 是容器隔离的基石(Docker 分类里的老朋友, 这里聚焦它的"网络"那一面).

但隔离带来一个新问题: namespace 是一张白纸, 与世隔绝, 容器怎么和外界通信? 需要一根"线"把它连出来.

原语二: veth pair -- 一根虚拟网线

veth pair(虚拟以太网设备对) 是一对总是成对出现的虚拟网卡, 它俩像一根网线的两端: 从一端进去的数据包, 一定从另一端出来.

把 veth 的一端放进容器的 namespace, 另一端留在主机, 这根"虚拟网线"就把隔离的容器连到了外面:

容器 namespace (ns1)          主机 namespace
┌─────────────────┐ ┌──────────────────┐
│ eth0 ●━━━━━━━━━┿━━veth━━━┿━● vethXXXX │
│ (veth 一端) │ 虚拟网线 │ (veth 另一端) │
└─────────────────┘ └──────────────────┘
从 eth0 发出的包, 必从 vethXXXX 出来, 反之亦然
# 创建一对 veth: veth0 和 veth1
$ ip link add veth0 type veth peer name veth1

# 把 veth1 这一端"塞进" ns1 命名空间
$ ip link set veth1 netns ns1
# 现在: veth0 在主机, veth1 在容器里 -- 一根线连通了内外

network namespace 像一间没有窗户的密室(完全隔离).
veth pair 像在墙上凿洞穿过去的一根管道: 管道一头在密室内, 一头在外面, 东西从一头塞进去就从另一头出来. 容器要联网, 先得有这根管道通向外界.

现在一个容器能连到主机了. 但如果有很多容器, 每个都拉一根 veth 到主机, 它们彼此之间怎么通信? 总不能两两都连一根线. 这就需要第三个原语.

原语三: 网桥 -- 一台虚拟交换机

Linux bridge(网桥) 是内核里的一个虚拟二层交换机. 把多个容器的 veth 主机端都"插"到同一个网桥上, 这些容器就像插在同一台交换机上的电脑, 可以二层互通.

网桥让同主机的多个容器互通(这正是 Docker 默认的 docker0 网桥做的事):

容器A(ns1)        容器B(ns2)        容器C(ns3)
eth0 eth0 eth0
│veth │veth │veth
▼ ▼ ▼
┌──────────────────────────────────────────┐
│ 网桥 (如 cni0 / docker0) │ ← 虚拟交换机
└────────────────────┬─────────────────────┘

主机物理网卡 eth0 ──▶ 外部网络

有了网桥, 同主机容器互通的链路就完整了: 容器A 的包 → 它的 veth → 网桥 → 转发到 容器B 的 veth → 容器B. 这三个原语组合起来, 就是"同节点容器网络"的全部机制. Docker 的默认网络模式 docker0 就是这么实现的.

把三个原语串起来: 同节点 Pod 通信全流程

在 Kubernetes 里, 同一节点上两个 Pod 通信, 走的正是这条路(CNI 插件在 Pod 创建时自动搭好这些):

Pod A (10.244.0.2) 想访问 Pod B (10.244.0.3), 同一节点:
1. Pod A 的进程发包, 目标 10.244.0.3
2. 包从 Pod A 的 eth0(veth 一端)出去
3. 到达主机端的 veth, 被送上网桥 cni0
4. 网桥发现 10.244.0.3 对应 Pod B 的 veth → 直接二层转发
5. 包从 Pod B 的 veth 进入 Pod B 的 eth0 → 到达 Pod B
全程不出主机, 就是"veth → 网桥 → veth"的二层交换

Kubernetes 的网络模型: 四条规则

上面是"机制". Kubernetes 在此之上规定了一套网络模型(策略)--它不关心用什么实现, 但要求任何实现都必须满足四条规则:

规则 含义
每个 Pod 有独立 IP Pod 是网络的基本单元, 有自己的 IP(不是借用节点的)
Pod 间直接互通, 无需 NAT 任意两个 Pod 用各自的 IP 直接通信, 中间不做地址转换
Pod 看到的自己的 IP = 别人看到它的 IP Pod 自报的 IP 和外界访问它用的 IP 一致(无 NAT 伪装)
节点上的 agent 能和本节点所有 Pod 通信 kubelet 等能直接访问本节点的 Pod

这套模型的精髓: 一个扁平的, 无 NAT 的网络

核心是**"扁平网络(Flat Network)"**--整个集群所有 Pod 仿佛在同一个大局域网里, 每个 Pod 都能用对方的 IP 直接访问, 不用关心对方在哪个节点, 中间有没有 NAT.

这极大简化了应用开发: 服务调另一个服务, 就像在一台机器上调本地端口一样, 网络拓扑对应用透明. 但"无 NAT, 人人直连"在跨节点时极难实现--这正是第 02 篇要攻克的难题.

注意: K8s 只定义模型, 不提供实现

Kubernetes 自己不实现上面这套网络--它只规定"必须满足这四条规则", 至于具体用 veth+网桥怎么搭, 跨节点怎么连, 全部外包给 CNI 插件(第 03 篇).

这是本系列的核心主线: K8s 定义机制接口, 实现交给可插拔的插件. 同节点通信简单, 各插件做法大同小异; 真正拉开插件差异的, 是跨节点--下一篇见分晓.

快速回顾

  • 容器的"独立网络"不是真硬件, 是内核原语拼出来的
  • network namespace: 给容器一套隔离的网络栈(独立网卡/IP/路由/iptables)--容器网络隔离的基石
  • veth pair: 成对的虚拟网卡, 像一根两头网线, 一端在容器, 一端在主机, 打通隔离的 namespace
  • 网桥: 虚拟二层交换机, 把多个容器的 veth 主机端插上去, 实现同主机容器互通
  • 同节点 Pod 通信: veth → 网桥 → veth 的二层转发, 全程不出主机
  • K8s 网络模型四规则: 核心是"扁平网络 + 无 NAT", 所有 Pod 像在一个大局域网, IP 直连; 但 K8s 只定义模型, 实现外包给 CNI

一个 Pod 里有多个容器, 它们的网络是怎样的?

同一个 Pod 内的多个容器共享同一个 network namespace--也就是共享同一个 IP, 同一套端口空间. 所以它们之间可以直接用 localhost 互相访问(就像在一台机器上), 但要注意端口不能冲突.

这是靠一个叫 pause 容器的"占位容器"先创建好 namespace, 其余容器再加入它来实现的(Kubernetes 笔记 Pod 详解里讲过). 所以"Pod 有一个 IP"而非"每个容器一个 IP", 根源就在这里.

这些 veth, 网桥都要手动配吗?

不用. 本篇用 ip netns, ip link 手动操作, 只是为了看清底层到底发生了什么. 实际运行时, 这些全由 CNI 插件在 Pod 创建的瞬间自动完成(第 03 篇): kubelet 起 Pod 时调用 CNI, CNI 负责建 namespace, 拉 veth, 插网桥, 分配 IP, 配路由.

理解手动过程, 是为了看懂 CNI 自动做了什么--黑盒就变成了白盒.

动手练习

  1. 创建两个 namespace: 用 ip netns add 创建 ns1 和 ns2, 分别 ip netns exec 进去看它们各自隔离的网络栈
  2. 拉 veth 连线: 创建两对 veth, 把每对的一端分别塞进 ns1, ns2, 另一端留在主机, 给容器端的网卡配上同网段 IP
  3. 搭网桥验证互通: 创建一个 Linux bridge, 把两个 veth 的主机端都插上去, 启用网桥; 然后从 ns1 里 ping ns2 的 IP--亲手让两个"容器"互通
  4. 对照 Docker 默认网络: 用 docker network inspect bridge 看 Docker 的默认网桥, 再 brctl show(或 ip link)观察 docker0 上挂了哪些 veth--印证手搭的就是 Docker 在做的事
  5. 对照 K8s 网络模型: 对照四规则, 想一想: 手搭的"同主机网络"满足前三条吗?(满足); 如果 ns1, ns2 在不同主机上, 现在的办法还能让它们互通吗?(不能--这就是下一篇的问题)