从Yaml到Pod创建过程详解
这篇文章将会完整的叙述出以下内容:
从用户提交 yaml 文件到集群Pod 实际创建的过程,剖析 K8s 核心机制,有助于读者理清 K8s 内部组件之间的交互逻辑
为了拆解从 Yaml 到 Pod 创建这个复杂的过程,文章分为三个章节来介绍:
- 控制面:
API Server、etcd、Admission Controller是如何协同工作的 - 调度与执行: 介绍
Scheduler如何选择 node ,以及Kubelet和Container Runtime如何在 node 上拉起 Pod - 外挂系统:核心介绍
Device Plugin是如何介入 Pod 创建流程
控制面
当你通过 kubectl 输入 kubectl -kubeconfig=... apply -f pod.yaml 之后,
- API Sever 会收到创建 Pod 的请求。在收到创建 Pod请求之后,API server 首先会对请求进行鉴权,认证请求来源是谁,有没有权限创建 Pod。
- 在完成鉴权之后会进入Admission Ctrl 阶段,Addmission Ctrl 会请求进行修改(mutation) 和验证(validate),值得注意的是 K8s 提供了
dynamic admission contrl扩展机制,方便用户自定义Admission 阶段,该过程如下:- A. 运行内置的 mutation :比如 DefaultNamespace 插件,如果提交的请求namespace 为空,就修改成 default
- B. 运行自定义的 mutation:API server 会查看集群里面注册的MutationWebhookConfiguration,把已经修改过的请求发给外部的Webhook服务,比如 Istio 就是在这里注入一个 sidecar
- C. 运行内置的 validate
- D. 运行自定义的validate:API server 会查看集群里面注册的ValidatingWebhookConfiguration,比如:自定义的validate不允许创建带某个 label,API server 就会拒绝这个请求
- 持久化,经过程上述过程后,会把最后的 Pod 对象写入 etcd,此时 Pod 状态为 Pending
调度与执行
我们要创建的Pod 对象已经存储 etcd 中,但系统还不知道哪个 node 来运行这个 Pod。此时需要调度器(scheduler)登场发挥作用了。
Scheduler
- 它是谁?
Scheduler(调度器) 中央调度员”。它通过informer 机制 watchAPI Server - 它做什么?
- 发现新工作:它通过
API Server发现etcd里出现了一个新的 Pod,这个 Pod 的spec.nodeName字段是空的 - “海选” (Filtering - 过滤):
Scheduler会查看集群里所有的 Node。它会过滤掉不满足 Pod 要求的 Node。比如:- Pod 需要 2核 CPU,Node A 只剩 1核了(淘汰!)。
- Pod 需要 GPU,Node B 根本没有 GPU(淘汰!)。
- Pod 要求必须在“华东区”(有特定标签),Node C 在“华北区”(淘汰!)。
- “打分” (Scoring - 排序):从“海选”通过的 Node 中(比如剩下了 Node D 和 Node E),
Scheduler会给它们打分。比如,Node D 负载很低(得分高),Node E 负载很高(得分低)。 - 最终决定:
Scheduler选择得分最高的那个 Node(比如 Node D)。 - 更新订单:它不会自己去联系 Node D。它只会回到
API Server,更新那个 Pod 对象的spec.nodeName字段,把它从“空”改成 “Node D”。 注意:Scheduler只负责决策,不负责执行。它的结果只是修改了 etcd 里的记录,Pod 应该去 Node x
- 发现新工作:它通过
Kubelet
- 它是谁? 它是控制平面(Control Plane)安插在每个工作节点(Node)上的“代理人”。它也通过 informer 机制 watch
API Server - 它做什么?
- 发现新任务:在
Scheduler调度完成之后。Node D 上的Kubelet发现 Pod spec. Nodename = Node D, - 领任务:
Kubelet立刻从API Server那里拿到这个 Pod 的完整定义 - 准备环境:它开始准备 Pod 运行所需的一切,比如:
- 创建数据卷(Volumes),这里可能会涉及 CSI 相关。
- 如果需要特殊设备,需要device plugin,这点后面会讲到
- (如果是第一次)通知
Container Runtime去拉取镜像, CRI 相关。 - 配置网络,这里需要CNI 插件。
- 下达执行命令:
Kubelet不会自己去启动容器,它会把这个工作交给 Container Runtime。 注意:Kubelet负责 1. 对 API-server 上报 Pod 状态,2. setup 环境,但具体运行容器并不是它的职责
- 发现新任务:在
CSI(Container Storage Interface)
CSI 共有两个阶段,分别是 attach 和 Mount
- Attach (附加) - 控制平面:如果 Pod 用的是“网络存储”(比如 AWS EBS, 谷歌云盘),一个在控制平面运行的
csi-controller(它也在 Watch API Server) 看到 Pod 被调度到 Node C,它会立刻发出一个 API 请求,把那块“云盘”(Attach)到 Node C 这个虚拟机上。这是 Kubelet 之外发生的。 - Mount (挂载) - Node 级别 (Kubelet 在此介入):
Kubelet看到 Pod 的spec.volumes里定义了一个PersistentVolumeClaim(PVC)。- 它首先会调用本机的
CSI Node Plugin(一个在 Node C 上的DaemonSetPod):“嗨 CSI 插件,请把那个(刚 Attach 过来)的云盘,挂载到宿主机的一个特定目录,以便我稍后给 Pod 用。” - CSI 插件负责格式化(如果是新的)、
mount这个设备到宿主机上的一个临时路径(比如/var/lib/kubelet/pods/.../volumes/my-pvc)。 - CSI 插件完成后,告诉
Kubelet:“OK,地基打好了,路径是这个。”
Container Runtime
- 它是谁? 这就是真正在节点上跑容器的东西,比如 Docker、containerd 或 CRI-O。
- 它做什么?
Kubelet通过一个标准接口 (CRI - Container Runtime Interface) 对Container Runtime说:“启动这个容器,使用这个镜像,挂载这些卷”。Container Runtime负责拉取镜像(如果本地没有,如果本地有的话,会先走 cache)。- 它负责使用 Linux 内核功能(如 Cgroups 和 Namespaces)来创建和启动容器。
- 容器真正跑起来了!
- 状态上报:
Container Runtime把容器的状态(比如 "Running")告诉Kubelet。 Kubelet再上报:Kubelet看到容器跑起来了,就高兴地跑去API Server那里更新 Pod 的状态(status字段),改成 Running。
CRI&CNI 运行时&网络
- Kubelet 调用 CRI:
Kubelet通过 CRI 接口对containerd(或 Docker) 说:“启动这个 Pod Sandbox (沙箱)!”- 传递参数:它会把第 1 步(CSI)拿到的存储路径(
/var/lib/kubelet/pods/.../volumes/my-pvc)和第 2 步(Device Plugin)拿到的设备路径(/dev/nvidia0)一起传给Container Runtime。 Container Runtime会把这些路径挂载 (bind mount) 到容器内部。
- 传递参数:它会把第 1 步(CSI)拿到的存储路径(
- Container Runtime 调用 CNI (网络):
容器运行时调整 CNI(网络) :Container Runtime创建了容器的进程和命名空间(PID, Mount, ...),但此时容器还没有 IP 地址,它还在一个“网络隔离”的状态。- 此时,
Container Runtime(根据Kubelet的配置)会调用本机上的 CNI 插件(比如 Calico, Flannel,它们通常也是DaemonSet)。 CNI插件执行以下操作: a. 从 CNI 的“IP 地址管理器”(IPAM) 那里申请一个 IP (比如10.244.2.5)。 b. 在宿主机(Node C)上创建一个“虚拟网线对”(veth pair)。 c. 一端留在宿主机上(插到“交换机” - Linux Bridge 上),另一端插到容器的“网络命名空间”里,并配置上10.244.2.5这个 IP 地址和路由规则。CNI插件完成后,把分配的 IP 地址(10.244.2.5)返回给Container Runtime。
- 启动!
Container Runtime看到网络也 OK 了,最后才启动容器的主进程(CMD/ENTRYPOINT)。
外挂系统
Device Plugin
此刻,Pod 已经被调度上了正确的 Node 上,并且 kubelet 已经准备好启动它了,但是 Pod 订单上有一个特殊要求:
spec:
containers:
- name: cuda-container
image: nvidia/cuda:11.0-base
resources:
limits:
nvidia.com/gpu: 1
kubelet 自己并不知道 nvidia.com/gpu 是什么,它只认识 cpu、memory,所以这里需要解决两个问题:
- scheduler需要知道Node 上有几块GPU
- 怎么Node 上的一块 GPU 分配给 Pod
这时候就需要 Device Plugin 的机制发挥作用了,它允许第三方厂商(比如 NVIDIA、Intel、Mellanox 等)来告诉 Kubelet 如何管理他们的特定硬件
具体的作用机制是:
- 1. 注册 (Registration)
- 首先,集群管理员会在每一个需要特殊硬件的 Node 上(比如 Node C),以
DaemonSet的形式部署一个 Device Plugin Pod(例如nvidia-device-plugin-ds)。 - 这个
nvidia-device-pluginPod 启动后,它会去侦测宿主机(Node C)上有多少块 NVIDIA GPU。假设它发现了 2 块 GPU(/dev/nvidia0,/dev/nvidia1)。 - 它通过一个
gRPC接口(在 Node 本地,通常是一个 Unix socket 文件)主动联系本机的Kubelet:“你好Kubelet,我是nvidia.com/gpu插件,我在这里发现了 2 块 GPU 资源。”
- 首先,集群管理员会在每一个需要特殊硬件的 Node 上(比如 Node C),以
- 2. Kubelet 向 API Server上报库存 (Advertising)
Kubelet收到这个注册信息后,它立刻向API Server上报自己的状态 (Node Status)。- 它会在自己的 Node 对象里,更新一个叫做
allocatable(可分配资源) 的字段,在cpu和memory旁边,加上一行:allocatable: { ... , nvidia.com/gpu: 2 }。 - 现在,
API Server和etcd就知道了:“Node C 有 2 块 GPU 库存”。
- 3. 调度 (Scheduling)
- 现在回到我们的第二站!当
Scheduler看到那个要求nvidia.com/gpu: 1的 Pod 时,它会去查看所有 Node 的allocatable库存。 - 它会发现 Node C 有
nvidia.com/gpu: 2(库存 > 1),所以(在 Filtering 阶段)Node C 是一个合格的节点。 Scheduler于是把 Pod 分配给了 Node C。
- 现在回到我们的第二站!当
- 4. 分配 (Allocation)
Kubelet(在 Node C 上)拿到了这个 Pod。它看到了nvidia.com/gpu: 1的请求。Kubelet不会自己去挑 GPU。它会再次通过gRPC联系nvidia-device-plugin分配具体的 GPUnvidia-device-plugin负责管理 GPU 的“分配账本”。它说:“好的,我把/dev/nvidia0分配给 Pod A。” 然后它把这个设备路径(以及可能需要的环境变量或挂载)回复给Kubelet。Kubelet拿到了这个具体信息(比如“把/dev/nvidia0挂载进容器”)。
- 5. 启动 (Container Runtime)
Kubelet最后去调用Container Runtime(比如containerd)。- 启动这个
nvidia/cuda:11.0-base镜像,并且,把宿主机的/dev/nvidia0设备注入 (mount) 到这个新容器的内部。 Container Runtime照做。
最终
- 向 Kubelet 注册 (Register):告诉
Kubelet“我管理这种硬件”。 - 帮助 Kubelet 上报 (Advertise):让
Kubelet告诉Scheduler“我有多少这种硬件”。 - 替 Kubelet 分配 (Allocate):当 Pod 被调度过来后,告诉
Kubelet“请把_这块_具体的硬件给它”。
device plugin 这一设计,
- 扩展性极强,让 K8s 可以“即插即用”地支持任何未来的新硬件
- 插件管理分配,让插件开发者可以自由的虚拟化设备
- 上报的功能,要求插件开发者需要管理健康检查
其他组件
Kube-proxy
- 他是谁?
kube-proxy负责的网络,特别是Service的实现 - 职责:
kube-proxy的工作是确保 K8s 的Service能够正常的工作 - 它做什么?
- Watch API server,但它关心的是
Service和Endpoints - 当一个新的
Service被创建,或者一个 Pod 变得 "Running" 并且 "Ready"(Kubelet会上报这个状态),这个 Pod 的 IP 就会被添加到Endpoints列表里。 kube-proxy看到这个变化后,就会在它所在的 Node 上修改网络规则(比如iptables或ipvs)。- 这些规则的作用是:“如果有人想访问
Service的虚拟 IP(比如10.96.0.10),我就把这个请求(做-网络地址转换-NAT)转发到它后面某个真实 Pod 的 IP(比如192.168.1.5)上。”
- Watch API server,但它关心的是
一图胜千言
题后记:我个人不太喜欢这种“概念化”的面试风格,原因是这些流程式的概念知道就是知道,不知道的话很难在面试的时间内通过某些线索去推敲出结论来,属于很难考察出来候选人思维的题目。但是,anyway,弄清楚这一问题确实帮助我理清了这里面的架构关系,仅此写一篇 blog 作为记录