CRI (Container Runtime Interface - 容器运行时接口) 是 Kubernetes (具体说是 kubelet) 定义的一套 gRPC API 标准,用于与容器运行时通信,命令它们启动和管理 Pod/容器。市面上有多重容器运行时,本文尝试厘清他们的关系,这是理解 Kubernetes 节点工作原理的关键。
容器运行时对比表
为了理解它们的依赖关系,我们从 K8s 的 kubelet 开始,自上而下看。
安装运行时
无论您选择哪个运行时,在 Kubernetes 节点上都应执行以下准备步骤:
- 加载内核模块:
sudo modprobe overlay
sudo modprobe br_netfilter
- 使模块持久化:
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
- 设置 sysctl 网络参数:
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
- 应用 sysctl 参数:
安装 containerd
containerd 是最直接、最推荐的 K8s 运行时。
- 安装 containerd:
sudo apt-get update
# containerd 已经包含在 Ubuntu 的默认仓库中
sudo apt-get install containerd -y
- 生成默认配置文件 (关键步骤):
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml
- 配置 cgroup 驱动 (K8s 必须):
为了让
kubelet 和 containerd 使用相同的 cgroup 驱动,您必须修改配置文件,将 containerd 的驱动设为 systemd。
使用 sed 命令自动修改 (或手动编辑):
# 查找 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
# 将 SystemdCgroup = false 改为 SystemdCgroup = true
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
- 重启并启用 containerd:
sudo systemctl restart containerd
sudo systemctl enable containerd
安装 CRI-O (K8s 专用)
cri-o 是专为 K8s 设计的轻量级运行时,需要添加其官方仓库。
- 设置 OS 和 CRI-O 版本 (示例):
# 示例使用 Ubuntu 22.04
export OS=Ubuntu_22.04
# 示例使用 1.28 版本的 K8s (CRI-O 版本通常与之对应)
export VERSION=1.28
- 添加 GPG 密钥和
apt 仓库:
sudo apt-get update
sudo apt-get install -y curl gpg
# 添加密钥
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo gpg --dearmor -o /usr/share/keyrings/libcontainers-archive-keyring.gpg
# 添加仓库
echo "deb [signed-by=/usr/share/keyrings/libcontainers-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
- 安装 CRI-O:
sudo apt-get update
sudo apt-get install cri-o cri-o-runc -y
- 启动并启用 CRI-O:
cri-o 默认已配置为使用 systemd cgroup 驱动,通常无需额外配置。
sudo systemctl daemon-reload
sudo systemctl enable crio
sudo systemctl start crio
安装 Docker Engine (Docker CE)
这是安装完整的 Docker 平台。如果您选择这条路,K8s 还需要 cri-docker (见选项四)。
- 卸载旧版本 (如果存在):
sudo apt-get remove docker docker-engine docker.io containerd runc
- 设置 Docker 的
apt 仓库:
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
# 添加 Docker 的官方 GPG 密钥
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# 添加仓库
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- 安装 Docker Engine:
sudo apt-get update
# 注意:这会安装 docker-ce 并同时安装 containerd.io 作为其依赖
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
- 配置 cgroup 驱动 (K8s 必须):
与
containerd 类似,Docker 默认不使用 systemd cgroup 驱动。
# 创建或修改 /etc/docker/daemon.json
cat <<EOF | sudo tee /etc/docker/daemon.json
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2"
}
EOF
- 重启并启用 Docker:
sudo systemctl enable docker
sudo systemctl daemon-reload
sudo systemctl restart docker
安装 cri-docker (Docker 适配器)
前提条件:必须已经完成了 “安装 Docker Engine”。
cri-docker 是一个适配器,它没有在标准的 apt 仓库中,需要从 GitHub 下载。
- 访问 GitHub Releases 页面:
- 下载
.deb 安装包:
您需要找到与您的架构 (amd64) 和 Ubuntu 版本匹配的最新 .deb 包。
例如,下载 0.3.10 版本 (请检查最新版):
# 检查最新版本替换文件名
wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.10/cri-dockerd_0.3.10.3-0.ubuntu-jammy_amd64.deb
- 安装
.deb 包:
sudo dpkg -i cri-dockerd_0.3.10.3-0.ubuntu-jammy_amd64.deb
- 验证服务:
安装
.deb 包后,systemd 服务会自动配置并启动。
sudo systemctl status cri-docker.service
# 您可能还会看到 cri-docker.socket 也在运行
sudo systemctl status cri-docker.socket
Kubelet 与 CRI 的关系
kubelet 是 K8s 在每个节点上的代理。当需要创建 Pod 时,kubelet 会通过 CRI API 发出指令(例如 “RunPodSandbox”)。它不关心谁在监听这个 API,只要对方能听懂 CRI 就行。
这里就出现了两个主要阵营:
- 原生 CRI 运行时 (主流):
containerd 和 cri-o。
- 通过适配器的运行时:
cri-docker + docker。
运行时阵营分析
containerd
containerd (现代 K8s 的默认选项):
containerd 是一个高级运行时,它本身就包含一个 CRI 插件。kubelet 可以直接与它通信。
containerd 收到指令后,会调用一个低级运行时(如 runc)来创建和运行容器进程。
- 依赖链:
kubelet -> (CRI API) -> containerd -> runc
kubelet 使用使用 containerd (默认/推荐)的依赖关系图:
[ Kubelet ] <-- (CRI API) --> [ containerd ] --> [ runc ] --> (创建容器)
# 调试工具
[ crictl ] <-- (CRI API) --> [ containerd ]
[ ctr ] <-- (原生 API) ---> [ containerd ]
cri-o
cri-o (为 K8s 而生):
cri-o 的设计目标就是只实现 CRI,不多也不少。它非常轻量级。
- 和
containerd 一样,它也需要调用低级运行时(如 runc)来干活。
- 依赖链:
kubelet -> (CRI API) -> cri-o -> runc
docker + cri-docker
docker + cri-docker (传统方式):
docker (即 dockerd 守护进程) 本身并不实现 CRI。它有自己的一套 API。
- 在 K8s 1.24 之前,
kubelet 内部有一个叫 dockershim 的模块来做这个翻译。
- K8s 1.24 移除了
dockershim 后,如果你还想用 Docker Engine,就必须安装 cri-docker 这个外部适配器。
- 讽刺的是: Docker Engine 本身现在也使用
containerd 作为其底层的运行时。
- 依赖链:
kubelet -> (CRI API) -> cri-docker -> (Docker API) -> dockerd -> containerd -> runc
关键点: 正如你所见,docker + cri-docker 的调用链是最长的。这也是为什么社区转向 containerd 和 cri-o 的原因之一:路径更短,更高效。
kubelet 使用使用 cri-docker (为了兼容旧的 Docker Engine)的依赖关系图:
(Docker API) (containerd API)
[ Kubelet ] <-- (CRI API) --> [ cri-docker ] --> [ dockerd ] --> [ containerd ] --> [ runc ]
# 调试工具
[ crictl ] <-- (CRI API) --> [ cri-docker ]
[ docker ] <-- (Docker API) --> [ dockerd ]
命令行工具阵营分析
这是最容易混淆的地方。我们有两个工具 ctr 和 crictl,它们都是用来调试的,但目标完全不同。
容器运行时 Socket 位置速查表
以下是这些组件在标准 Linux 系统上的默认 socket 路径:
| 组件 |
默认 Socket 路径 |
作用 / 客户端 |
| docker |
/var/run/docker.sock |
Docker 引擎的原生 API。docker CLI 使用它。 |
| containerd |
/run/containerd/containerd.sock |
CRI 接口 + 原生 API。kubelet、crictl 和 ctr 都使用它。 |
| cri-o |
/run/crio/crio.sock |
CRI 接口。kubelet 和 crictl 使用它。 |
| cri-docker |
/var/run/cri-dockerd.sock |
CRI 接口 (适配器)。kubelet 和 crictl 使用它。 |
理解这些 socket 文件的位置对于在 Kubernetes 节点上进行调试至关重要,因为它们是 kubelet 和 crictl 等工具与容器运行时通信的连接点。
Docker (docker)
- 路径:
/var/run/docker.sock
- 说明: 这是最广为人知的 socket。它暴露的是 Docker Engine 的原生 API,而不是 Kubernetes 的 CRI。
- 谁在用:
docker 命令行工具 (例如 docker ps),以及所有需要与 Docker 引擎交互的第三方应用(如 CI/CD 工具)。
- 注意:
kubelet (自 1.24 版起) 不能直接使用这个 socket。
containerd
- 路径:
/run/containerd/containerd.sock
- 说明:
containerd 将其所有功能,包括 CRI 插件,都统一暴露在这个 gRPC socket 上。
- 谁在用:
- Kubelet (CRI):
kubelet 将此路径作为 CRI 端点,与其通信以管理 Pod。
- crictl (CRI): 这是
crictl 调试时连接的标准路径。
- ctr (原生):
containerd 的原生调试工具 ctr 也使用这个 socket 来绕过 CRI,直接访问 containerd 的 API。
CRI-O (cri-o)
- 路径:
/run/crio/crio.sock
- 说明:
cri-o 是一个纯粹的 CRI 实现,它创建此 socket 专门用于 kubelet 的连接。
- 谁在用:
- Kubelet (CRI):
kubelet 连接此路径以发出 CRI 指令。
- crictl (CRI):
crictl 在 cri-o 节点上会连接此路径进行调试。
cri-docker (适配器)
-
路径: /var/run/cri-dockerd.sock
-
说明: cri-docker 作为一个翻译器运行。它创建这个 socket 来假装自己是一个 CRI 运行时。
-
谁在用:
-
Kubelet (CRI): kubelet 以为自己正在与一个标准 CRI 运行时对话。
-
crictl (CRI): crictl 也可以连接到这个 socket。
-
工作流: 当 kubelet 向 /var/run/cri-dockerd.sock 发送 CRI 请求时,cri-docker 会将该请求翻译成 Docker 原生 API,然后再发送给 /var/run/docker.sock。
crictl 如何与不同容器运行通信
crictl 工具是 Kubernetes 管理员的瑞士军刀,它需要知道要连接到哪个 CRI socket。
如果不指定,crictl 会按顺序尝试连接一个默认列表,这个列表正好包括了上述的 CRI 运行时:
unix:///run/containerd/containerd.sock
unix:///run/crio/crio.sock
unix:///var/run/cri-dockerd.sock
unix:///var/run/dockershim.sock (已被废弃的 K8s 1.24 之前的内置 Docker 适配器)
可以通过编辑配置文件 /etc/crictl.yaml 或设置环境变量 CONTAINER_RUNTIME_ENDPOINT 来显式指定正确的 socket 路径。
总结
containerd 和 cri-o 是实现了 CRI 接口的运行时。
cri-docker 是一个适配器,让 K8s 能通过 CRI 与不实现 CRI 的 docker 引擎对话。
crictl 是 K8s 标准的 CRI 调试工具,能和所有 CRI 运行时工作。
ctr 是 containerd 专用的原生调试工具,它不关心 CRI。