这篇文章将会完整的叙述出以下内容:从用户提交 yaml 文件到集群Pod 实际创建的过程,剖析 K8s 核心机制,有助于读者理清 K8s 内部组件之间的交互逻辑
为了拆解从 Yaml 到 Pod 创建这个复杂的过程,文章分为三个章节来介绍:
API Server 、etcd 、Admission Controller 是如何协同工作的 Scheduler 如何选择 node ,以及 Kubelet 和 Container Runtime 如何在 node 上拉起 PodDevice Plugin 是如何介入 Pod 创建流程当你通过 kubectl 输入 kubectl -kubeconfig=... apply -f pod.yaml 之后,
dynamic admission contrl 扩展机制,方便用户自定义Admission 阶段,该过程如下:我们要创建的Pod 对象已经存储 etcd 中,但系统还不知道哪个 node 来运行这个 Pod。此时需要调度器(scheduler)登场发挥作用了。
Scheduler (调度器) 中央调度员”。它通过informer 机制 watch API ServerAPI Server 发现 etcd 里出现了一个新的 Pod,这个 Pod 的 spec.nodeName 字段是空的Scheduler 会查看集群里所有的 Node。它会过滤掉不满足 Pod 要求的 Node。比如:Scheduler 会给它们打分。比如,Node D 负载很低(得分高),Node E 负载很高(得分低)。Scheduler 选择得分最高的那个 Node(比如 Node D)。API Server,更新那个 Pod 对象的 spec.nodeName 字段,把它从“空”改成 “Node D”。
注意:Scheduler 只负责决策,不负责执行。它的结果只是修改了 etcd 里的记录,Pod 应该去 Node xAPI ServerScheduler 调度完成之后。Node D 上的 Kubelet 发现 Pod spec. Nodename = Node D,Kubelet 立刻从 API Server 那里拿到这个 Pod 的完整定义Container Runtime 去拉取镜像, CRI 相关。Kubelet 不会自己去启动容器,它会把这个工作交给 Container Runtime。
注意:Kubelet 负责 1. 对 API-server 上报 Pod 状态,2. setup 环境,但具体运行容器并不是它的职责CSI 共有两个阶段,分别是 attach 和 Mount
csi-controller (它也在 Watch API Server) 看到 Pod 被调度到 Node C,它会立刻发出一个 API 请求,把那块“云盘”(Attach)到 Node C 这个虚拟机上。这是 Kubelet 之外发生的。Kubelet 看到 Pod 的 spec.volumes 里定义了一个 PersistentVolumeClaim (PVC)。CSI Node Plugin(一个在 Node C 上的 DaemonSet Pod):“嗨 CSI 插件,请把那个(刚 Attach 过来)的云盘,挂载到宿主机的一个特定目录,以便我稍后给 Pod 用。”mount 这个设备到宿主机上的一个临时路径(比如 /var/lib/kubelet/pods/.../volumes/my-pvc)。Kubelet:“OK,地基打好了,路径是这个。”Kubelet 通过一个标准接口 (CRI - Container Runtime Interface) 对 Container Runtime 说:“启动这个容器,使用这个镜像,挂载这些卷”。Container Runtime 负责拉取镜像(如果本地没有,如果本地有的话,会先走 cache)。Container Runtime 把容器的状态(比如 "Running")告诉 Kubelet。Kubelet 再上报:Kubelet 看到容器跑起来了,就高兴地跑去 API Server 那里更新 Pod 的状态(status 字段),改成 Running。Kubelet 通过 CRI 接口对 containerd (或 Docker) 说:“启动这个 Pod Sandbox (沙箱)!”/var/lib/kubelet/pods/.../volumes/my-pvc)和第 2 步(Device Plugin)拿到的设备路径(/dev/nvidia0)一起传给 Container Runtime。Container Runtime 会把这些路径挂载 (bind mount) 到容器内部。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)。此刻,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,所以这里需要解决两个问题:
这时候就需要 Device Plugin 的机制发挥作用了,它允许第三方厂商(比如 NVIDIA、Intel、Mellanox 等)来告诉 Kubelet 如何管理他们的特定硬件
具体的作用机制是:
DaemonSet 的形式部署一个 Device Plugin Pod(例如 nvidia-device-plugin-ds)。nvidia-device-plugin Pod 启动后,它会去侦测宿主机(Node C)上有多少块 NVIDIA GPU。假设它发现了 2 块 GPU(/dev/nvidia0, /dev/nvidia1)。gRPC 接口(在 Node 本地,通常是一个 Unix socket 文件)主动联系本机的 Kubelet:“你好 Kubelet,我是 nvidia.com/gpu 插件,我在这里发现了 2 块 GPU 资源。”Kubelet 收到这个注册信息后,它立刻向 API Server 上报自己的状态 (Node Status)。allocatable (可分配资源) 的字段,在 cpu 和 memory 旁边,加上一行:allocatable: { ... , nvidia.com/gpu: 2 }。API Server 和 etcd 就知道了:“Node C 有 2 块 GPU 库存”。Scheduler 看到那个要求 nvidia.com/gpu: 1 的 Pod 时,它会去查看所有 Node 的 allocatable 库存。nvidia.com/gpu: 2(库存 > 1),所以(在 Filtering 阶段)Node C 是一个合格的节点。Scheduler 于是把 Pod 分配给了 Node C。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 挂载进容器”)。Kubelet 最后去调用 Container Runtime (比如 containerd)。nvidia/cuda:11.0-base 镜像,并且,把宿主机的 /dev/nvidia0 设备注入 (mount) 到这个新容器的内部。Container Runtime 照做。Kubelet “我管理这种硬件”。Kubelet 告诉 Scheduler “我有多少这种硬件”。Kubelet “请把_这块_具体的硬件给它”。device plugin 这一设计,
kube-proxy 负责的网络,特别是 Service 的实现kube-proxy 的工作是确保 K8s 的 Service 能够正常的工作Service 和 EndpointsService 被创建,或者一个 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)上。”]]>题后记:我个人不太喜欢这种“概念化”的面试风格,原因是这些流程式的概念知道就是知道,不知道的话很难在面试的时间内通过某些线索去推敲出结论来,属于很难考察出来候选人思维的题目。但是,anyway,弄清楚这一问题确实帮助我理清了这里面的架构关系,仅此写一篇 blog 作为记录