
上一篇我们已经把 GPU 从Host ↓ Docker Container真正跑通了。通过docker run --gpus all ...我们知道了 Docker Container 怎样通过 NVIDIA Container Toolkit 使用宿主机 GPU。但是继续往 AI Infra 走一个新的问题马上出现。以后我们不会只运行一个 Docker Container而是会进入Kubernetes ↓ Pod ↓ Container这时候一个很基础、但也很容易被忽略的问题出现了执行kubectl apply以后Pod 里面的 Container 到底是谁启动的以前我们很熟悉docker run nginx因为Docker清清楚楚地站在那里。但 Kubernetes 里面kubectl apply -f pod.yaml然后kubectl get pods过一会儿就变成Running中间发生了什么这一篇我们先不碰 GPU也不讨论 Device Plugin。先搭一个单节点 Kubernetes但和我之前写过的单节点 K8S 系列不同这一次不再走Docker cri-dockerd而是直接让kubelet ↓ CRI ↓ containerd这样也正好为下一篇Kubernetes GPU做准备。一、为什么又写一次单节点 Kubernetes之前的《从零搭建一个单节点 K8S 可观测实验室二安装单节点 Kubernetes》中我已经完整介绍过Ubuntu ↓ Docker Engine ↓ cri-dockerd ↓ kubelet ↓ kubeadm ↓ Flannel那套环境主要服务于Prometheus Grafana Loki Tempo OpenTelemetry等可观测实验。当时选择Docker cri-dockerd主要是因为 Docker 更熟悉个人实验也比较直观。但 AI Infra 系列现在已经讲到了Container Runtime NVIDIA Container Toolkit CDI GPU Device继续往 Kubernetes 走如果还经过cri-dockerd ↓ Docker Engine反而会多出两层。所以这次直接使用kubelet │ │ CRI ▼ containerd │ ▼ runc整个 Runtime Path 会清楚很多。二、上一篇装好的 Docker 要删掉吗不用。这一点其实很值得理解。我们的实验机完全可以同时存在Docker和Kubernetes只是它们走不同的入口。Docker 继续用于docker pull docker build docker run docker compose例如上一篇docker run --gpus all ...仍然照常使用。而 Kubernetes 则走kubectl ↓ API Server ↓ kubelet ↓ CRI ↓ containerd所以Kubernetes 并不要求机器必须通过 Docker Engine 运行 Container。这也是从 Docker 进入 Kubernetes 时非常重要的一次认知变化。三、先搞清楚三个组件Docker、containerd、runc这三个名字经常同时出现。可以先粗略理解成三个层级。Docker我们最熟悉docker run nginxDocker Engine 提供了一整套Image Container Network Volume Build API使用体验。它更像一个完整的Container Platformcontainerdcontainerd 更靠下一层。它负责Image Snapshot Container Lifecycle Runtime更重要的是containerd 可以直接提供 CRI所以 Kubernetes 的 kubelet 可以直接和它通信。runc再往下是runc它更接近真正创建 Linux Container 的那一层。最终containerd ↓ runc ↓ Linux Kernel ↓ Namespace / cgroup / mount ↓ Process也就是说Container最后仍然只是Linux Process只是被 Namespace、cgroup 等机制隔离起来。四、CRI 到底是什么这篇真正需要记住的新概念只有一个CRI Container Runtime Interface它可以简单理解成kubelet 和 Container Runtime 之间的一套标准接口。例如kubelet │ │ CRI ▼ ┌─────┴─────┐ │ │ containerd CRI-Okubelet 不需要关心containerd 内部怎么创建 Container它只通过 CRI 请求拉取 Image 创建 Pod Sandbox 创建 Container 启动 Container 停止 Container具体怎么完成由 Runtime 自己负责。五、那以前的 dockershim 和 cri-dockerd 又是什么Docker Engine 本身并不直接实现 Kubernetes CRI。所以早期 Kubernetes 曾经在 kubelet 内部放了一层dockershim大致是kubelet ↓ dockershim ↓ Docker Engine后来 Kubernetes 移除了内置 dockershim。如果现在仍然希望 kubelet 使用 Docker Engine就需要cri-dockerd于是kubelet ↓ CRI ↓ cri-dockerd ↓ Docker Engine ↓ containerd ↓ runc而这一篇我们直接变成kubelet ↓ CRI ↓ containerd ↓ runc这就是这次实验最大的变化。六、Pod 到底是谁启动的现在可以先把答案说出来。完整链路大致是kubectl ↓ API Server ↓ Scheduler ↓ kubelet ↓ CRI ↓ containerd ↓ runc ↓ Linux Kernel其中kubectl只是 Client。它把我希望存在一个 Pod提交给 API Server。Scheduler 负责这个 Pod 应该去哪台 Node例如Pod A ↓ Node 1它并不真正创建 Container。真正运行在每台 Node 上、不断观察有哪些 Pod 应该运行在我这里的是kubelet当 kubelet 发现这个 Pod 应该运行在本机就通过CRI告诉 containerd把它运行起来最后containerd ↓ runc真正创建 Linux Container。所以如果非要回答Pod 是谁启动的可以说kubelet 负责驱动 Pod 在 Node 上变成现实而真正创建 Container 的 Runtime 是 containerd / runc。七、实验环境继续使用 AI Infra 实验机Ubuntu 24.04 Server Intel Core i5-10400F NVIDIA GeForce RTX 3060 12GB VRAM上一篇已经安装Docker NVIDIA Driver NVIDIA Container Toolkit这篇不删除任何东西。新增containerd CRI kubeadm kubelet kubectl Flannel八、先看看现有 containerd因为上一章已经安装 Docker所以机器上通常已经存在 containerd。先看containerd --version再看systemctl status containerd --no-pager以及runc --version如果这些已经存在不要重复安装另一套 containerd尤其不要看到 containerd 已经存在以后又随手安装不同来源的软件包。先沿用当前 Docker 安装带来的 containerd 即可。九、把 containerd 调整成 Kubernetes 可以使用的 CRI Runtime这一点是整篇最重要的准备工作。先看cat /etc/containerd/config.toml我在实测时Docker 安装带来的配置非常精简而且直接出现disabled_plugins [cri]这意味着containerd 虽然在运行 但 CRI Plugin 被关闭了Docker 可以正常工作但是Kubernetes 不能直接把它当 CRI Runtime如果当前配置比较精简最简单的方法是先备份sudo cp \ /etc/containerd/config.toml \ /etc/containerd/config.toml.bak然后重新生成完整默认配置containerd config default \ | sudo tee \ /etc/containerd/config.toml \ /dev/null十、确认 CRI 没有被禁用检查grep -n disabled_plugins \ /etc/containerd/config.toml如果仍然看到disabled_plugins [cri]就需要删掉cri例如改成disabled_plugins []如果默认配置里根本没有禁用 CRI则不用处理。十一、配置SystemdCgroup true继续看grep -n SystemdCgroup \ /etc/containerd/config.toml如果是SystemdCgroup false改成SystemdCgroup true例如sudo sed -i \ s/SystemdCgroup false/SystemdCgroup true/ \ /etc/containerd/config.toml现代 Ubuntu 使用systemd cgroup v2让kubelet和containerd都使用 systemd 管理 cgroup可以减少很多奇怪问题。然后sudo systemctl restart containerd确认systemctl status containerd --no-pager正常。十二、准备 Kubernetes 的基本系统参数加载overlay br_netfiltersudo tee /etc/modules-load.d/k8s.conf EOF overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter再设置sudo tee /etc/sysctl.d/k8s.conf EOF net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sudo sysctl --system确认sysctl net.ipv4.ip_forward结果net.ipv4.ip_forward 1即可。这部分和以前单节点 Kubernetes 实验基本一样不再展开。十三、关闭 Swap个人实验环境继续采用最简单方式sudo swapoff -a检查free -h确认 Swap 为0如果希望重启后继续关闭再把/etc/fstab里的 Swap Entry 注释掉。十四、安装 kubeadm、kubelet、kubectl本次实测使用Kubernetes 1.37.1加入 Kubernetes v1.37 Repositorysudo apt-get update sudo apt-get install -y \ apt-transport-https \ ca-certificates \ curl \ gpg导入 Keysudo mkdir -p -m 755 \ /etc/apt/keyrings curl -fsSL \ https://pkgs.k8s.io/core:/stable:/v1.37/deb/Release.key \ | sudo gpg --dearmor \ -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg添加 Repositoryecho \ deb [signed-by/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.37/deb/ / \ | sudo tee \ /etc/apt/sources.list.d/kubernetes.list安装sudo apt-get update sudo apt-get install -y \ kubelet \ kubeadm \ kubectl然后锁定版本sudo apt-mark hold \ kubelet \ kubeadm \ kubectl查看kubeadm version kubectl version --client kubelet --version十五、可选安装crictl这里有一个实测时发现的小坑安装 kubeadm / kubelet / kubectl并不会自动保证crictl存在。crictl属于cri-tools它非常适合后面观察 Kubernetes Runtime。安装后可以配置sudo tee /etc/crictl.yaml EOF runtime-endpoint: unix:///var/run/containerd/containerd.sock image-endpoint: unix:///var/run/containerd/containerd.sock timeout: 10 debug: false EOF然后sudo crictl info如果能正常返回 Runtime 信息就说明crictl ↓ CRI ↓ containerd已经打通。十六、提前拉取 Kubernetes Image可以先sudo kubeadm config images pull \ --cri-socket \ unix:///var/run/containerd/containerd.sock这里我在 VirtualBox 测试时还碰到一个很典型的问题Docker 可以访问网络 但 containerd 拉 registry.k8s.io 超时原因也很简单Docker Proxy和containerd Proxy不是一回事。如果你的环境需要代理需要单独给containerd.service配置sudo mkdir -p /etc/systemd/system/containerd.service.d sudo nano /etc/systemd/system/containerd.service.d/http-proxy.conf ------------------------------------------------------------------------- [Service] EnvironmentHTTP_PROXYhttp://PROXY_IP_ADDRESS:PROXY_PORT EnvironmentHTTPS_PROXYhttp://PROXY_IP_ADDRESS:PROXY_PORT EnvironmentNO_PROXY127.0.0.1,localhost,10.96.0.0/12,10.244.0.0/16,192.168.56.0/24,192.168.31.0/24 ------------------------------------------------------------------------- sudo systemctl daemon-reload sudo systemctl restart containerd这也再次说明docker pull 正常并不能证明Kubernetes Runtime 拉镜像也正常十七、初始化单节点 Kubernetes找到当前服务器管理 IPip addr假设192.168.x.x初始化sudo kubeadm init \ --apiserver-advertise-address192.168.x.x \ --pod-network-cidr10.244.0.0/16 \ --cri-socketunix:///var/run/containerd/containerd.sock这里最值得注意的是--cri-socket它明确告诉 kubeadmKubernetes Runtime containerd而不是Docker cri-dockerd十八、配置 kubectl初始化完成后mkdir -p $HOME/.kube sudo cp -i \ /etc/kubernetes/admin.conf \ $HOME/.kube/config sudo chown \ $(id -u):$(id -g) \ $HOME/.kube/config现在kubectl get nodes可能还是NotReady因为还没有安装 CNI。十九、继续使用 Flannel为了让这篇只改变Container Runtime这一项我继续使用之前熟悉的FlannelPod CIDR10.244.0.0/16前面已经在 kubeadm init 中配置好了。安装kubectl apply -f \ https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml观察watch kubectl get pods -A等系统组件逐渐进入Running二十、让单节点可以运行普通 Podkubeadm 创建的 Control Plane 默认带NoScheduleTaint。但我们这里只有一台机器Control Plane Worker 以后要使用的 GPU Node所以执行kubectl taint nodes --all \ node-role.kubernetes.io/control-plane-然后kubectl get nodes最终应该看到Ready二十一、确认 Kubernetes 真正使用的是 containerd这一步很重要。运行kubectl get nodes -o wide看CONTAINER-RUNTIME应该类似containerd://...也可以kubectl get node \ -o jsonpath{.items[0].status.nodeInfo.containerRuntimeVersion}{\n}如果看到containerd://...说明Docker 仍然存在 但是 Kubernetes 已经直接使用 containerd二十二、运行第一个普通 Pod先创建一个最简单的kubectl run runtime-test \ --imagenginx:alpine观察kubectl get pod -w应该经历Pending ↓ ContainerCreating ↓ Running到这里kubectl ↓ API Server ↓ Scheduler ↓ kubelet ↓ CRI ↓ containerd ↓ runc整条链路就已经真正跑通。二十三、一个很直观的小实验docker psvscrictl ps现在执行docker ps通常找不到刚才的runtime-test因为它根本不是Docker Engine创建的。再执行sudo crictl ps则可以看到 Kubernetes Container。也可以sudo crictl pods看到对应的Pod Sandbox这几个命令非常直观地证明docker ps观察的是Docker Engine而crictl ps观察的是CRI Runtime这也是这篇实验最值得保留的一组现象。二十四、所以 Pod 到底是谁启动的现在重新回答标题。执行kubectl apply以后kubectl只是把 Desired State 提交给API ServerScheduler 决定Pod 应该去哪台 Node到了 Nodekubelet发现这个 Pod 应该运行在这里于是kubelet │ │ CRI ▼ containerd │ ▼ runc │ ▼ Linux Kernel真正创建 Container。所以最值得记住的是kubectl ↓ API Server ↓ Scheduler ↓ kubelet ↓ CRI ↓ containerd ↓ runc ↓ Container二十五、和之前 Docker cri-dockerd 有什么不同之前kubelet ↓ CRI ↓ cri-dockerd ↓ Docker Engine ↓ containerd ↓ runc现在kubelet ↓ CRI ↓ containerd ↓ runc少了cri-dockerd Docker Engine但 Docker 本身没有消失。以后仍然可以docker build docker run只是它不再位于Kubernetes Pod Runtime Path里。对于后面继续学习GPU Device Plugin NVIDIA Container Toolkit GPU Operator这条 Runtime Path 会更加直接。二十六、下一篇为什么出现现在我们已经有Ubuntu ↓ containerd ↓ Kubernetes ↓ Pod机器本身还有RTX 3060并且nvidia-smi正常。上一篇docker run --gpus all ...也已经证明Docker Container能够使用 GPU。看起来GPU Container Kubernetes全都有了。但现在执行kubectl describe node看Capacity Allocatable会发现cpu memory pods ...都有。却没有nvidia.com/gpu于是问题又来了Linux 明明知道这里有 RTX 3060 Docker 也已经能使用 RTX 3060 但是 Kubernetes 为什么完全不知道这台 Node 有 GPU这就不是 Container Runtime 的问题了。而是Kubernetes Resource的问题。于是下一篇继续《从零搭建一个 AI Infra 实验室⑭Kubernetes 如何认识一张 GPU——Extended Resource、NVIDIA Device Plugin 与 nvidia.com/gpu》这一篇我们解决了Kubernetes 怎样运行 Container下一篇再解决Kubernetes 怎样第一次知道 这台机器 有一张 GPU。