这个标题拆开看很有意思:前半截“Gemini永久会员”跟技术本身没有一毛钱关系,大概率是蹭热搜的标题党写法;真正值得反复琢磨的是后半句——在Kubernetes里创建Pod,系统一定是先创建pause容器,然后才轮到CNI插件把网络配置到位。这句话听起来简单,但它背后串起了kubelet、CRI、containerd、网络命名空间、CNI规范一整条链路。今天这篇文章就把这条链路从头到尾掰碎了讲。
这篇文章适合两类人。第一类是刚入门Kubernetes,想搞清楚Pod到底是怎么“攒”出来的新手;第二类是集群已经跑起来了,但遇到Pod网络时通时不通、DNS偶尔抽风、IP没法分配这类问题,想系统搞懂底层原理的人。把“先有pause,再有网络,然后才是业务容器”这条顺序刻进脑子里,你排查Pod网络问题的速度会比别人快一个量级。
1. 一个Pod的诞生流程:别把“先pause再CNI”当成一句话
1.1 调用链从kubelet开始
Kubernetes集群里,所有对Pod的操作本质上都是对API Server的描述性请求。当你执行kubectl apply提交一个Deployment后,经过控制器、调度器一系列流转,最终会有一个Pod对象被分配到某个节点上。节点上真正干活的组件叫kubelet,它通过Watch机制发现“这个Pod归我了”,然后开启创建流程。
kubelet本身不会直接创建容器。它通过CRI(Container Runtime Interface,容器运行时接口)和容器运行时通信。CRI是Kubernetes定义的一套gRPC接口协议,把“拉镜像”“创建容器”“启停容器”这些底层操作全部抽象成标准方法。目前最常见的运行时是containerd,也有部分环境还在用CRI-O。kubelet把Pod定义翻译成CRI请求,发给containerd,containerd再想办法把容器创建出来。
这里有个容易混淆的点:CRI有两个重要的服务,一个是ImageService,负责拉取和管理镜像;另一个是RuntimeService,负责容器和Sandbox的生命周期管理。创建Pod时,kubelet调用的核心接口是RuntimeService.RunPodSandbox。这个接口的名字就暗示了:创建Pod的第一步不是直接拉业务镜像,而是先创建一个“沙箱”。
1.2 RunPodSandbox这一步到底做了什么
RunPodSandbox是理解整条链路的钥匙。在containerd里,Sandbox这个词就是Pod的意思,而Sandbox对应的实体容器正是pause容器。containerd收到RunPodSandbox请求后,会依次做这么几件事:
第一,准备Sandbox的配置,包括Pod的ID、命名空间、资源限制等元数据;第二,通过CRI的ImageService调用确保pause镜像存在本机,如果不存在就拉取;第三,基于pause镜像创建一个容器实例,这个实例就是我们在节点上用crictl ps看到的那种POD形态的容器;第四,启动pause容器,通过shim进程把pause跑起来;第五,在启动Sandbox的过程中,调用CNI插件执行网络配置。
整个过程用一句话概括:pause容器先被创建并启动,这一个容器就是整个Pod的网络基座;随后CNI在这个基座上架桥铺路;最后kubelet才逐个创建业务容器。
你可以把Pod理解成一栋公寓楼。pause容器是这栋楼的地基和公共管廊,CNI负责把水电网络接到楼里,业务容器则是住进楼里的住户。没有地基,住户不可能入住;没有网络接入,住户住进来也是断网状态。Kubernetes不允许业务容器先于pause容器出现,这是由CRI接口的设计和kubelet的调用顺序双重保证的。
1.3 网络配置为什么必须卡在pause之后、业务容器之前
在containerd的实现里,网络配置发生在Sandbox启动阶段。也就是说,当RunPodSandbox返回成功时,pause容器已经启动,CNI已经执行完毕,Pod的IP已经分配好,整个网络命名空间已经处于可用状态。
为什么必须在这个时间点做?因为业务容器在创建时,需要直接“加入”pause容器的网络命名空间。Kubernetes要求一个Pod内的所有容器共享同一个网络命名空间,这意味着它们共用同一个IP、同一套端口空间、同一个路由表。如果CNI配置拖到业务容器创建之后再做,业务容器启动时连IP都拿不到,进程起来也不知道该监听的网络环境是什么样。
另外,CNI的配置结果还要通过CRI返回给kubelet,kubelet会把这些信息(如Pod IP)更新到API Server的状态里。如果网络配置失败,RunPodSandbox会直接报错,kubelet不会继续执行后续的创建容器步骤,Pod就会停留在ContainerCreating状态。这也是为什么排障时看到Pod卡在ContainerCreating,第一反应应该去看Sandbox和CNI,而不是盯着业务容器。
2. pause容器:整张网络拓扑的“地基”
2.1 pause到底是什么,为什么只有几百KB
pause容器的全称叫“infrastructure container”,中文圈子里常叫“基础容器”“沙箱容器”。它使用的镜像通常是registry.k8s.io/pause:3.9,镜像体积只有几百KB,里面只有一个静态编译的pause二进制文件。
这个二进制做的事情简单到令人发指:启动后先拉起PID 1进程,然后不断处理信号,支撑整个Pod的生命周期。它不承载任何业务逻辑,不监听端口,不写日志,唯一的任务就是“以容器形态存在”。但恰恰是这个“空壳”,成了整个Pod的基石。
为什么不用业务容器来承担这个角色?因为业务容器的镜像体积大、启动慢、生命周期不稳定。Pod需要的是一个能最早启动、最晚退出、绝对不会崩溃的“占位符”。pause镜像设计目标就是极小、极稳、启动极快,这样才能保证在业务容器就绪之前,整个网络拓扑已经有了一个可靠的锚点。
2.2 三大职责拆解
pause容器的职责可以拆成三块。
第一,持有网络命名空间。pause容器是整个Pod网络命名空间的“业主”,业务容器都是“租客”。租客搬进来要开通水电,业主必须先签好房子。CNI配置网络时,操作的目标就是pause容器的网络命名空间。
第二,作为PID命名空间里的PID 1。Pod内所有容器共享PID命名空间时,pause容器是第一个进程,也是孤儿进程回收者。当某个业务容器的子进程因为父进程退出而变成孤儿时,这些进程会被过继给PID 1,也就是pause进程,由它负责回收和清理,避免僵尸进程堆积。
第三,信号管理的总闸。用户执行docker stop或kubectl delete pod时,最终会向Pod发送信号。pause作为主进程接收这些信号,然后决定整个Pod如何退出。业务容器各自退出时产生的信号也会统一由pause兜底处理。这个设计让Pod作为一个整体可以被优雅终止,而不是各容器各跑各的。
2.3 Pod内共享哪些命名空间
一个Pod里的容器并非共享所有内核命名空间,而是有选择地共享。最常见的共享目标是网络命名空间(network namespace),所有容器看到同一个网卡、同一个IP、同一个路由表。其次是UTS命名空间,共享同一个主机名;还有IPC命名空间,共享System V IPC和POSIX消息队列,这保证了容器间可以通过共享内存通信。
PID命名空间默认在Kubernetes里也是共享的(在Pod维度为true时),这也是pause成为PID 1的条件。至于mount命名空间,考虑到安全和隔离需求,通常是各容器独立的,不会共享文件系统挂载视图。理解这个表后,你就会明白为什么Pod内两个容器可以用localhost互相访问——因为它们在同一个网络命名空间里。
3. CNI交互:组装Pod网络的幕后流程
3.1 CRI与CNI怎么握手
CRI和CNI是Kubernetes生态里两个容易混淆的接口。CRI管容器的生命周期,CNI管容器网络的配置。两者在一条调用链上协作:kubelet通过CRI请求运行时创建Sandbox,运行时在Sandbox启动过程中调用CNI插件完成网络配置。
containerd内部对CNI的接入非常成熟。它默认会在节点上的/etc/cni/net.d/读取网络配置文件,在/opt/cni/bin/读取CNI插件二进制。kubelet启动时也可以通过--cni-bin-dir和--cni-conf-dir指定这两个目录。生产环境中,Calico、Cilium、Flannel等网络方案安装后,都会在这两个目录里写入自己的配置和插件。
整个流程可以概括为:containerd启动Sandbox时,先从配置目录读取CNI网络配置列表(conflist),然后按配置里的插件链逐个调用插件二进制,比如先调用calico插件,再调用portmap插件。每个插件执行ADD操作,把网卡、IP、路由这些网络要素装配到pause容器的网络命名空间里。
3.2 CNI ADD的输入输出全解析
CNI插件的执行方式很有意思:它不是通过HTTP调用,而是runtime直接执行插件二进制文件,通过环境变量传入参数,通过标准输入传入网络配置JSON,通过标准输出返回执行结果。这种方式轻量、通明,也方便调试。
ADD操作必须传入的环境变量包括:
CNI_COMMAND:操作类型,ADD、DEL、CHECK、GC、VERSION等。CNI_CONTAINERID:容器ID,在Kubernetes场景下就是Sandbox的ID。CNI_NETNS:网络命名空间的路径,形如/proc/<pid>/ns/net,其中pid是pause容器的进程ID。CNI_IFNAME:要创建的网卡名称,通常是eth0。CNI_PATH:插件被调用的路径列表,方便插件调用链。
网络配置JSON则会从标准输入传入,格式大致如下:
{ "cniVersion": "0.3.1", "name": "k8s-pod-network", "type": "calico", "ipam": { "type": "host-local", "subnet": "10.244.0.0/16" } }插件执行ADD成功后,会往标准输出返回一段JSON,包含分配的IP、网卡MAC、网关、DNS等信息。例如:
{ "cniVersion": "0.3.1", "interfaces": [ {"name": "eth0", "mac": "aa:bb:cc:dd:ee:ff", "sandbox": "/proc/1234/ns/net"} ], "ips": [ {"version": "4", "address": "10.244.5.7/24", "gateway": "10.244.5.1"} ], "dns": {"nameservers": ["10.96.0.10"]} }这些信息会被containerd解析,最终反映到Pod的Status里,比如kubectl get pod -o wide看到的IP就是从这里来的。需要注意的是,CNI插件并不是只能创建一个网卡,链式插件会在同一命名空间内依次执行,每个插件都可以修改网络配置。
3.3 常见CNI插件如何接入
生产环境里最常见的CNI方案有Flannel、Calico、Cilium。它们的网络模型不同,但对接CNI的方式完全一致——都遵循CNI规范,都通过插件二进制的方式被运行时调用。
Flannel走的是VXLAN或host-gw模式,偏向简单易用。它的CNI插件会创建veth对,一端在pause容器的网络命名空间里,另一端接到cni0网桥;随后通过flanneld维护的路由表把数据送到对端节点。
Calico走的是纯三层BGP路由模式,性能更优。它的CNI插件同样创建veth对,但不会依赖cni0网桥,而是把宿主机这一端的veth作为BGP的参与接口,通过BGP协议把路由分发到其他节点。配合IPPool的IPAM机制,Calico能给Pod分配独立的IP块。
Cilium则基于eBPF实现了更细粒度的网络策略和安全能力。它的CNI插件会创建veth对,然后通过eBPF程序在网络路径上挂接钩子,实现L3/L4甚至L7的策略控制。不管插件内部怎么实现,对于containerd来说,它们都只是标准输入输出协议下的一个二进制。
4. 实操:一步步复现“pause先起,CNI后配”
4.1 在节点上找到pause容器
没有比亲手在节点上看到这条链路更能加深理解了。如果你的集群使用containerd,节点上安装好crictl工具后,可以先用crictl ps -a查看所有容器。你会注意到,每个Pod都对应一个名字带Pod字样、镜像为pause的容器。它比同Pod的业务容器创建时间更早,状态通常是Running。
crictl ps -a | grep -i pause输出大致是:
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID 2f8a3b6c1d9e registry.k8s.io/pause:3.9 5 minutes ago Running kube-flannel-ds-7lw2t 1 2f8a3b6c1d9e如果你想只看某个Pod的pause容器,可以先拿到Pod的UID,再用crictl pods按Pod名称过滤。实际操作中我经常用crictl inspect <pause容器ID>拿到完整的容器元数据,包括PID、网络命名空间路径等。这一步能帮你确认Sandbox到底起了什么。
4.2 crictl runp手动走一遍Sandbox创建
如果集群里没有正在创建中的Pod,你甚至可以手动模拟一次Sandbox创建。crictl提供了一个叫runp的命令,专门用来跑一个裸的Pod Sandbox。先准备一个Sandbox配置:
# pod-sandbox.yaml metadata: name: my-sandbox log_directory: /tmp/logs linux: security_context: namespace_options: network: NODE然后执行:
crictl runp pod-sandbox.yaml这个命令会直接调用containerd的RunPodSandbox接口,效果就是创建一个pause容器,并且触发CNI插件执行网络配置。跑完之后再执行crictl ps | grep my-sandbox,就能看到多了一个pause容器。你可以把它删掉(crictl stopp+crictl rmp),不影响集群正常使用。
需要说明的是,集群默认的CNI配置会接管这个Sandbox,所以如果你在装有Calico的节点上跑这个命令,这个裸Sandbox也会拿到一个IP。这正是CNI链路在独立Sandbox上工作的直观验证。
4.3 手动执行CNI插件验证ADD逻辑
理解CNI最直接的方式是自己动手跑一次插件。找一台测试节点,进入/opt/cni/bin目录,随便选一个简单的插件,比如bridge,模拟runtime调用。
首先找到pause容器的PID:
PAUSE_PID=$(crictl inspect $(crictl ps --name=my-sandbox -q) | grep -o '"pid": [0-9]*' | head -1 | awk '{print $2}')然后设置CNI环境变量并手动执行ADD:
export CNI_COMMAND=ADD export CNI_CONTAINERID=$(crictl ps --name=my-sandbox -q) export CNI_NETNS=/proc/$PAUSE_PID/ns/net export CNI_IFNAME=eth0 export CNI_PATH=/opt/cni/bin echo '{ "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "cni0", "ipam": {"type": "host-local", "subnet": "10.244.0.0/24"} }' | /opt/cni/bin/bridge如果一切正常,插件会返回一段JSON,里面包含分配的IP和网桥信息。执行完以后,再进入pause容器的网络命名空间,查看网卡是否已经存在:
nsenter -t $PAUSE_PID -n ip addr你会发现eth0已经存在,而且带着刚分配的IP。这个手动过程完全还原了containerd在Sandbox启动时做的事情,对理解CNI协议特别有帮助。
4.4 进netns检查配置结果
网络配置是否生效,最终要看网络命名空间内部的状态。Pause容器的网络命名空间路径是/proc/<pause_pid>/ns/net。可以用nsenter直接进入这个命名空间执行任意网络命令。
nsenter -t $PAUSE_PID -n ip route nsenter -t $PAUSE_PID -n curl ifconfig.me你会看到,在pause的网络命名空间里,不仅有分配的IP,还有默认路由、网关等一整套网络配置。这套配置就是Pod内所有业务容器共享的“网络视图”。业务容器启动时,不过是在同一命名空间里打开自己的文件描述符而已。
5. 问题排查:Pod网络异常时先查谁
5.1 与排查速查表
我在实际支持别人排查问题时发现,绝大多数网络问题都可以沿着“pause状态 -> CNI插件 -> 网络命名空间内容 -> 业务容器内网络”这条顺序定位。下面是几个高频问题的排查思路速查表。
| 现象 | 优先检查方向 | 常用命令/方法 |
|---|---|---|
| Pod长时间ContainerCreating | Sandbox创建失败或CNI执行失败 | crictl ps -a;crictl logs;journalctl -u kubelet;查看containerd日志 |
| pause容器起不来 | pause镜像不存在、运行时配置错误 | crictl pull registry.k8s.io/pause:3.9;检查containerd配置 |
| Pod有IP,但ping不同网关 | 网络路由、节点防火墙、Calico状态异常 | nsenter -t <pause_pid> -n ip route;calicoctl node status |
| Pod之间通,但访问Service不稳定 | kube-proxy/iptables/IPVS规则异常,或conntrack表刷新 | iptables -L -t nat;ipvsadm -Ln;重启kube-proxy后重测 |
| DNS解析失败或超时 | CoreDNS与Pod网络连通性、上游DNS配置 | 进入Pod执行nslookup;kubectl -n kube-system logs -l k8s-app=kube-dns |
| CNI插件报错,找不到网卡 | CNI插件版本与内核不兼容、Pod Sandbox被误删 | 手动执行CNI ADD复现;查看插件目录权限;确认pause PID对应命名空间存在 |
| Pod IP一直变化或分配冲突 | IPAM池耗尽、host-local数据残留 | 查看节点上/var/lib/cni/networks/下的残留记录;calicoctl ipam show |
这里要给一个最关键的技巧:排障第一步永远是确认pause容器的网络命名空间是否健康。如果pause根本没起来,所有依赖网络的排查都是空中楼阁。
5.2 两个典型案子的复盘
第一个案子:有一次同事反馈某个应用的Pod一直处于ContainerCreating状态,业务容器日志完全没有输出。我先查crictl ps -a,发现pause容器根本没出现。再翻containerd日志,看到CNI网络配置加载失败,原因是/etc/cni/net.d/目录下同时存在Calico和Flannel两份冲突配置。CNI规范要求节点上只能有一个网络配置,多个配置会导致运行时不知道选哪个,直接报错。解决办法是删掉不用的配置,重新创建Pod后一切正常。
第二个案子:某个节点上的Pod能拿到IP,但跨节点通信不通。进入pause容器的网络命名空间看路由,发现默认路由的网关指向了一个不存在的下一跳。实际原因是Calico的BGP会话断掉,路由没有被正确分发。通过calicoctl node status确认BGP peer状态异常,重启Calico的calico-node组件后问题解决。这类问题如果不从pause的网络命名空间入手,很容易在业务容器里绕圈子。
需要注意的是,很多网络排查工具和数据都来自pause容器的命名空间。业务容器里可能没有ip、route、ping这些命令的镜像,但宿主机上的nsenter可以无视这些限制直接进入pause的命名空间操作。这是排查时最顺手的一个工具。
最后分享一点个人体会
我平时排查Pod网络问题有个习惯:不管问题表象多复杂,先到节点上看一眼pause容器状态,再进它的网络命名空间看一眼IP和路由。只要这两步是好的,业务容器基本不会出大问题;只要这两步有问题,业务容器再正常也没用。
有一次在客户现场,同事查了将近一小时,反复看业务容器日志,发现某个服务一直在重启。最后我过去一查——pause容器的Sandbox网络命名空间被手工操作误删了,业务容器起来以后根本没有网络,健康检查失败,自然反复重启。把这个“先pause后CNI”的顺序刻进条件反射里,很多看似诡异的网络问题都能快速落地,不会在表象里打转。