Kubernetes
Kubernetes 的抽象并不是“把 Docker 放到多台机器上”。它至少要同时虚拟化三件事:计算如何运行、网络如何连通、存储如何挂载。CRI、CNI、CSI 的价值在于:kubelet 和控制面只描述意图,具体实现可以替换。
从 PodSpec 到一个真正可访问的应用,可以把过程看成三条并行但有依赖的链路。
PodSpec
├─ CRI:创建 sandbox 与业务容器
├─ CNI:为 sandbox 配置网卡、IP、路由与策略
└─ CSI:attach / mount 所需 volume

这张总览图里最容易被忽略的是 kubelet 的位置:控制面不直接在节点上创建容器或挂载卷,而是通过 API Server 写入期望状态;scheduler 选择节点后,节点 kubelet 再协调 CRI、CNI、CSI 等插件把这个 Pod 真正落地。下面三条链路,都是从 kubelet 所在的节点向下展开的。
CRI:把“运行容器”从 kubelet 中拆出去
kubelet 是节点上的期望状态执行者:它接收分配给本节点的 PodSpec,并确保 Pod 处于期望状态。但 kubelet 不应该内嵌某一家 runtime,因此它通过 gRPC 的 Container Runtime Interface 调用 containerd、CRI-O 等运行时。Kubernetes 已移除内置 dockershim;现代集群要求节点 runtime 实现 CRI。
一个关键顺序是:先 RunPodSandbox,再 CreateContainer 和 StartContainer。sandbox 先建立 Pod 的共享环境,尤其是 network namespace;业务容器随后加入它。这正是 Pod 能拥有稳定 IP、sidecar 能通过 localhost 通讯的原因。

图中的 pause 是对 sandbox/infra container 的常见直观表达。重点不在 pause 进程本身做了什么,而在于它先把 network namespace 固定下来:container 1 和 container 2 看到的是同一张网卡、同一个 Pod IP、同一个端口空间。因此同一 Pod 内的 sidecar 可以访问 localhost,而两个不同 Pod 即使都监听 8080,也不会冲突。
CRI 处理的是“进程在哪里、以什么隔离与资源限制运行”;它不直接决定跨节点网络或持久卷,这两件事交给 CNI 与 CSI。
CNI:把 Pod 变成网络中的一等公民
runtime 创建 sandbox 后会调用 CNI 插件。CNI 的基本职责很小:给某个 network namespace 配置接口、地址、路由,并在删除时清理。CNI 不是 Kubernetes 专有协议,因此它能让不同网络实现挂接到同一套生命周期。
从默认 bridge 开始,理解 Kubernetes 为什么还需要 CNI
下面的图很像刚接触 Docker 时看到的网络:每个节点上都有 docker0,容器通过 veth 接入 bridge。单机内这没有问题;但若所有节点都各自使用相同的 172.17.0.0/16,上层路由器无法判断去往 172.17.0.2 的包究竟应该送到哪一台节点。它只能解决“容器接到本机网络”,还没有解决“Pod 在集群网络里可达”。

Kubernetes 的网络模型要求每个 Pod 有集群内可达的 IP,并尽量让 Pod-to-Pod 通信不需要经过额外的地址转换。因此 CNI 首先要处理的就是:为节点分配不重叠的 Pod CIDR,并把去往其他 Pod CIDR 的路由装进节点或底层网络。
常见的跨节点模型有两类:
- underlay:Pod IP 直接来自物理/云网络可路由的地址空间,网络设备或云路由知道 Pod 网段;
- overlay:Pod 包被封装在节点间可达的 underlay 包中,例如 VXLAN/Geneve。底层只需知道节点地址,代价是封装开销与 MTU 管理。
在 underlay/路由模式中,每个节点拿到一段不同的 Pod CIDR,例如图中的 10.0.1.0/24 与 10.0.2.0/24。节点的 cbr0 把本机 Pod 接入该网段;上层 router 或云路由表保存“目的 Pod CIDR → 下一跳节点 IP”的映射。包从一个 Pod 发出时,外层并不需要封装,只是按路由转发到持有目标 Pod 网段的节点。

overlay 则换了一种取舍:底层网络不认识每个 Pod CIDR,只需要认识节点 IP。以图中的 Flannel/VXLAN 思路为例,原始包仍保持 src=pod1、dst=pod4;节点 1 在 flannel0 处把它封进 UDP/VXLAN 报文,外层地址改为 src=node1、dst=node2,到达节点 2 后再解封。这样能在底层路由不可控时工作,但要为封装预留 MTU,并接受额外的封装与解封路径。

下面这张动态图把上述过程放回节点网络命名空间:Pod 的 eth0 经 veth 接入 cbr0,再由 flannel0 走向另一台节点;具体实现的设备名会不同,但“Pod namespace → 节点数据平面 → 节点间网络 → 目标 Pod namespace”的层次相同。

无论哪种模型,Kubernetes 关心的语义相同:每个 Pod 有自己的 IP;Pod 之间可以直接通信;节点代理不应篡改 Pod-to-Pod 流量。实际 CNI 还会增加 NetworkPolicy、eBPF 数据平面、多网卡等能力。Cilium、Multus 分别是后两类扩展的代表。
Service:虚拟 IP 并不是真实网卡
Pod IP 会随重建变化,Service 的 ClusterIP 解决的是“服务地址稳定、后端可变”。它通常不存在于某块网卡上;数据平面把发往 VIP:port 的连接转换到某个 Endpoint。
client → ClusterIP:443
→ 选择一个 endpoint
→ DNAT 为 PodIP:targetPort
早期 userspace kube-proxy 自己收包并转发,路径长且有额外拷贝。iptables 模式把选择和 DNAT 编译为内核规则;IPVS 用内核负载均衡表改善大规模 Service 的规则管理。较新的 kube-proxy 也提供 nftables 模式;而 eBPF 实现可以在 CNI 数据平面中绕开部分传统规则路径。它们解决的是同一问题,只是“把服务发现编译到哪里”的不同答案。官方文档也明确区分 iptables、IPVS 与 nftables 模式。
要特别区分 DNAT 与 SNAT:DNAT 改目标,让请求抵达后端;SNAT 改源,常用于回程路由不对称或出集群流量。错误理解这两者,是排查 externalTrafficPolicy、客户端源 IP 丢失和回包失败时最常见的根源。
CSI:把“卷”变成可调度的声明
CSI 将存储供应商实现从 Kubernetes 核心中移出。典型链路分为控制面和节点面:控制器负责创建/删除 volume、必要时 attach;节点插件负责把设备 mount 到节点,再把它 bind mount 到 Pod。对于网络文件系统或临时卷,并不一定发生 attach;对于块存储,attach 和 mount 通常是两步。
PVC/PV/StorageClass 的关键不是对象名,而是把“我需要什么样的容量与访问模式”交给调度和供应系统。ReadWriteOnce、ReadWriteMany、拓扑限制、扩容与快照,都是存储语义的一部分,不能靠容器镜像解决。
这里还有一个很实际的先后关系:如果 volume 有可用区、节点或磁盘类型的拓扑限制,调度器不能先随便挑一台节点、再希望 CSI 临时解决。StorageClass 的绑定模式会影响“先为 PVC 供给卷”还是“等 Pod 被调度到合适的节点后再供给”。遇到 Pod 一直 Pending、卷却已经创建的情况时,检查 PVC/PV 事件、NodeAffinity 与 CSI controller 日志,通常比反复重建 Pod 更有效。
用一条请求来验证三种虚拟化
当一个 Service 访问失败时,按链路验证最有效:
- kubelet/CRI 是否真的创建并启动了 sandbox 与 container?
- CNI 是否给 Pod 配了正确的 IP、路由和策略?
- EndpointSlice 是否包含 ready 的 Pod?
- Service 的转发规则是否把 VIP 正确映射到 endpoint?
- 若应用依赖数据,CSI volume 是否已 attach/mount 且权限正确?
References
Notion 相关零散笔记
- Kubernetes
- Docker
- cilium
- Multus
- iptables
- NAT
- Storage
- Block storage