containerd 之使用 ctr 和 runc 进行底层容器操作与管理
containerd
是目前业界标准的容器运行时,它负责容器生命周期的方方面面,如镜像管理、容器执行、存储和网络等。而 ctr
是 containerd
自带的命令行工具,虽然不如 Docker CLI 用户友好,但它提供了直接与 containerd
API 交互的能力,非常适合调试和学习底层机制。runc
则是遵循 OCI (Open Container Initiative) 规范的最常用的容器运行时实现,containerd
默认使用 runc
来创建和运行容器。
本文将结合实际操作日志,带大家深入了解如何使用 ctr
和 runc
进行一些核心的容器操作,包括:
- 安装
runc
:作为容器执行的基础。 - 使用
ctr
实现数据持久化:通过绑定挂载(Bind Mounts)。 - 使用
ctr
与私有镜像仓库(如 Harbor)交互:推送与拉取镜像。 - 探索 Docker 与
containerd
的关系:理解 Docker 如何利用containerd
。
目标读者:对容器底层原理感兴趣的开发者、系统管理员、SRE、Kubernetes 管理员。
一、安装 runC:OCI 容器运行时的基石
runc
是一个轻量级的工具,用于根据 OCI 规范运行容器。containerd
利用 runc
来启动和管理容器进程。通常 containerd
安装时会自带或依赖 runc
,但了解如何手动安装或更新它也很有用。
1. 下载 runC 二进制文件
您可以从 runc
的 GitHub Releases 页面找到最新的稳定版本。这里我们以下载 v1.1.12 版本为例:
# 切换到合适的工作目录,例如 /usr/local/bin 或 /tmp
# cd /usr/local/bin# 下载适用于 amd64 架构的 runc 二进制文件
[root@docker102 ~]# wget https://github.com/opencontainers/runc/releases/download/v1.1.12/runc.amd64
参考链接: https://github.com/opencontainers/runc/releases
2. 安装与配置 (最佳实践)
下载后,通常需要进行重命名、移动到系统 $PATH
路径下,并赋予执行权限:
# 重命名
mv runc.amd64 runc# 移动到推荐的路径 (如果不在 /usr/local/bin)
# mv runc /usr/local/bin/# 赋予执行权限
chmod +x runc# 验证安装
runc --version
生产环境提示:建议验证下载文件的 SHA256 校验和,确保文件未被篡改。
二、使用 ctr 实现容器数据持久化 (Bind Mount)
容器默认是无状态的,其文件系统是临时的。当容器被删除时,写入的数据也会丢失。为了持久化数据,需要使用卷(Volume)或绑定挂载(Bind Mount)。ctr
支持通过 --mount
参数实现绑定挂载,将宿主机上的目录或文件挂载到容器内部。
1. 准备镜像和 Namespace
containerd
使用 Namespace 来隔离资源。可以为特定的项目或环境创建 Namespace。
# 查看现有 Namespace
[root@docker102:~]# ctr ns ls
NAME LABELS
default# 拉取镜像到指定的 Namespace (如果 Namespace 不存在,会自动创建)
# 我们将镜像拉取到 'oldboyedu-linux92' Namespace
[root@docker102:~]# ctr -n oldboyedu-linux92 image pull registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v2
# ... (省略拉取过程输出)
unpacking linux/amd64 sha256:3ac38ee6161e11f2341eda32be95dcc6746f587880f923d2d24a54c3a525227e...
done: 532.516825ms# 确认 Namespace 已创建
[root@docker102:~]# ctr ns ls
NAME LABELS
default
oldboyedu-linux92# 查看 Namespace 下的镜像
[root@docker102:~]# ctr -n oldboyedu-linux92 images ls
REF TYPE DIGEST SIZE PLATFORMS LABELS
registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v2 application/vnd.docker.distribution.manifest.v2+json sha256:3ac38ee6161e11f2341eda32be95dcc6746f587880f923d2d24a54c3a525227e 9.6 MiB linux/amd64 -
2. 创建带有绑定挂载的容器
使用 ctr container create
命令创建容器定义,并通过 --mount
指定挂载。
# --mount 参数格式: type=bind,src=<host-path>,dst=<container-path>,options=<options>
# options=rbind:rw 表示递归绑定,且读写权限
[root@docker102:~]# ctr -n oldboyedu-linux92 container create \--mount type=bind,src=/oldboyedu/games,dst=/usr/local/nginx/html,options=rbind:rw \registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v2 \xiuxian
注意:此时只是创建了容器的元数据,容器进程还未启动。
3. 准备宿主机目录并启动容器任务
绑定挂载需要宿主机上的源路径 (src
) 确实存在。
# 创建宿主机上的挂载源目录
[root@docker102:~]# mkdir -pv /oldboyedu/games
mkdir: created directory '/oldboyedu/games'# 启动容器任务 (后台运行 -d)
[root@docker102:~]# ctr -n oldboyedu-linux92 task start -d xiuxian
# ... (容器启动日志)
现在,容器 xiuxian
正在运行,并且宿主机的 /oldboyedu/games
目录已经挂载到了容器内的 /usr/local/nginx/html
。
4. 验证数据持久化
我们可以在宿主机和容器内分别操作挂载点,验证数据是否同步和持久。
# 在宿主机上放入一个文件
[root@docker102:~]# cp /etc/os-release /oldboyedu/games/# 进入容器内部查看
# ctr task exec [options] <container-id> <command>
# --exec-id $RANDOM: 为 exec 进程指定一个随机 ID
# -t: 分配一个 TTY
[root@docker102:~]# ctr -n oldboyedu-linux92 tasks exec --exec-id $RANDOM -t xiuxian sh
/ # ls -l /usr/local/nginx/html # 查看挂载点内容,应包含 os-release
total 4
-rw-r--r-- 1 root root 382 Jul 27 08:42 os-release# 在容器内写入数据
/ # echo haha > /usr/local/nginx/html/haha.log
/ # ls -l /usr/local/nginx/html # 确认写入成功
total 8
-rw-r--r-- 1 root root 5 Jul 27 08:45 haha.log
-rw-r--r-- 1 root root 382 Jul 27 08:42 os-release
/ # exit# 返回宿主机,检查宿主机目录,数据应已同步
[root@docker102:~]# ll /oldboyedu/games/
total 16
drwxr-xr-x 2 root root 4096 Jul 27 16:45 ./
drwxr-xr-x 5 root root 4096 Jul 27 16:40 ../
-rw-r--r-- 1 root root 5 Jul 27 16:45 haha.log # 容器内写入的文件
-rw-r--r-- 1 root root 382 Jul 27 16:42 os-release # 宿主机放入的文件
5. 清理容器并验证数据保留
删除容器及其任务,验证宿主机上的数据是否依然存在。
# 停止并删除容器任务 (-f 强制删除)
[root@docker102:~]# ctr -n oldboyedu-linux92 tasks rm -f xiuxian
WARN[0000] task xiuxian exit with non-zero exit code 137# 确认任务已删除
[root@docker102:~]# ctr -n oldboyedu-linux92 tasks ls
TASK PID STATUS# 删除容器定义
[root@docker102:~# ctr -n oldboyedu-linux92 container rm xiuxian# 确认容器已删除
[root@docker102:~# ctr -n oldboyedu-linux92 container ls
CONTAINER IMAGE RUNTIME# 检查宿主机目录,数据仍然存在
[root@docker102:~# ll /oldboyedu/games/
total 16
drwxr-xr-x 2 root root 4096 Jul 27 16:45 ./
drwxr-xr-x 5 root root 4096 Jul 27 16:40 ../
-rw-r--r-- 1 root root 5 Jul 27 16:45 haha.log
-rw-r--r-- 1 root root 382 Jul 27 16:42 os-release
结论:通过绑定挂载,容器的数据成功持久化到了宿主机上,即使容器被删除,数据也不会丢失。这对于需要持久化存储的应用(如数据库、Web 服务器日志、配置文件等)非常关键。
三、使用 ctr 与私有仓库 (Harbor) 交互
在生产环境中,我们通常使用私有镜像仓库(如 Harbor)来存储和分发内部镜像。ctr
也可以配置与这些私有仓库进行交互。
1. 配置 containerd 以信任私有仓库
如果您的私有仓库使用自签名证书或通过 HTTP 提供服务,需要配置 containerd
以信任它。
编辑 containerd
的配置文件 (/etc/containerd/config.toml
):
# /etc/containerd/config.toml
# version = 2 # 确保配置格式版本正确# ... 其他配置 ...# [plugins] # 可能在外层或内层,取决于你的 config.toml 结构
# [plugins."io.containerd.grpc.v1.cri"]
# [plugins."io.containerd.grpc.v1.cri".registry]
# [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
# # 为特定的仓库配置镜像和端点
# [plugins."io.containerd.grpc.v1.cri".registry.mirrors."10.0.0.101"]
# # endpoint 指定仓库地址,如果是 HTTP,需要明确写出 http://
# endpoint = ["http://10.0.0.101"] # 使用 HTTP 协议# 注意: 如果仓库需要认证,还需要配置 auth 部分,例如:
# [plugins."io.containerd.grpc.v1.cri".registry.configs]
# [plugins."io.containerd.grpc.v1.cri".registry.configs."10.0.0.101".auth]
# username = "your_username"
# password = "your_password"
# 或者使用 Docker 配置文件 (~/.docker/config.json) 中的凭证# ... 其他配置 ...
重要提示:
- 生产环境中强烈建议使用 HTTPS。如果必须使用 HTTP,需要像上面那样配置
endpoint
。 - 路径
plugins."io.containerd.grpc.v1.cri"
是针对 CRI 插件的配置,直接使用ctr
时,可能需要配置在不同的路径下,或者使用更通用的方式,例如:
请根据您的[plugins."io.containerd.internal.v1.cri".registry.configs."10.0.0.101".tls]insecure_skip_verify = true # 或者对于 registry host 本身 [plugins."io.containerd.internal.v1.remote".registry.mirrors."10.0.0.101"]endpoint = ["http://10.0.0.101"]
containerd
版本和具体config.toml
结构调整。最简单的方式通常是在[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
下配置。
2. 重启 containerd 服务
修改配置后,需要重启 containerd
使其生效。
[root@docker102:~]# systemctl restart containerd
3. 标记 (Tag) 镜像
将本地已有的镜像标记为目标私有仓库的地址格式。
# ctr image tag <source-image> <target-image>
# 这里我们将之前拉取的 apps:v1 标记为推送到 Harbor 的路径
# 假设 Harbor 地址是 10.0.0.101,项目是 oldboyedu-games,镜像名是 xiuxian,标签是 v1
[root@docker102:~]# ctr i tag registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v1 10.0.0.101/oldboyedu-games/xiuxian:v1
10.0.0.101/oldboyedu-games/xiuxian:v1 # 注意:日志中显示 'already exists',可能是之前操作过。正常应无此提示或直接成功。
4. 推送 (Push) 镜像到 Harbor
使用 ctr image push
命令推送镜像。
# --plain-http: 如果仓库使用 HTTP,需要此参数
# -u <username>:<password>: 如果仓库需要认证,需要此参数 (日志中未显示,但通常是必需的)
[root@docker102:~]# ctr i push --plain-http 10.0.0.101/oldboyedu-games/xiuxian:v1 # -u user:pass (如果需要认证)
# ... (省略推送过程输出)
elapsed: 15.1 s total: 9.4 Mi (636.6 KiB/s)
5. 从 Harbor 拉取 (Pull) 镜像
验证是否可以从私有仓库拉取镜像。
# 先删除本地的对应镜像,以便测试拉取
[root@docker102:~]# ctr i rm 10.0.0.101/oldboyedu-games/xiuxian:v1
# ...# 从 Harbor 拉取
[root@docker102:~]# ctr i pull --plain-http 10.0.0.101/oldboyedu-games/xiuxian:v1 # -u user:pass (如果需要认证)
# ... (省略拉取过程输出)
unpacking linux/amd64 sha256:d9c54b04f7131c9f4e2e18e8d8b438f688533f5df57bce408186f44f01f1f91f...
done
总结:通过修改 containerd
配置并使用 ctr i push/pull
命令,我们可以方便地与私有镜像仓库(包括使用 HTTP 或需要认证的仓库)进行交互。
四、探索 Docker 与 containerd 的关系
现代版本的 Docker 引擎实际上是构建在 containerd
之上的。当我们使用 Docker 命令创建和管理容器时,Docker Daemon 会调用 containerd
API 来完成实际的容器生命周期管理。containerd
将这些 Docker 创建的容器放在一个名为 moby
的特定 Namespace 下。
1. 使用 Docker 运行容器
[root@docker102:~]# docker run -d --name c1 registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v3
dd6a8fe737b21c55ce7b86f374e56c25391724f9240ce0ad2dfc7c78e772424f
2. 在 containerd 的 moby
Namespace 中观察
我们可以使用 ctr
查看 moby
Namespace 中的容器和任务。
# 查看 containerd 的所有 Namespace,确认 'moby' 存在
[root@docker102:~]# ctr ns ls
NAME LABELS
default
moby
oldboyedu-linux92# 查看 'moby' Namespace 下的容器 (Container ID 通常是 Docker 的长 ID)
[root@docker102:~]# ctr -n moby container ls
CONTAINER IMAGE RUNTIME
dd6a8fe737b21c55ce7b86f374e56c25391724f9240ce0ad2dfc7c78e772424f - io.containerd.runc.v2# 查看 'moby' Namespace 下的运行中任务 (Task)
[root@docker102:~]# ctr -n moby task ls
TASK PID STATUS
dd6a8fe737b21c55ce7b86f374e56c25391724f9240ce0ad2dfc7c78e772424f 633467 RUNNING
3. 验证 PID 和网络
可以看到 ctr
列出的 Task PID 与 docker inspect
获取的容器主进程 PID 是一致的。我们甚至可以用 ctr task exec
进入 Docker 管理的容器。
# 获取 Docker 容器的 PID
[root@docker102:~]# docker inspect -f "{{.State.Pid}}" c1
633467# 使用 ctr 进入 Docker 容器执行命令,查看 IP 地址
[root@docker102:~]# ctr -n moby task exec --exec-id $RANDOM dd6a8fe737b21c55ce7b86f374e56c25391724f9240ce0ad2dfc7c78e772424f ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:14:00:01inet addr:172.20.0.1 Bcast:172.20.0.255 Mask:255.255.255.0...# 使用 docker exec 查看 IP 地址 (结果应相同)
[root@docker102:~]# docker exec c1 ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:14:00:01inet addr:172.20.0.1 Bcast:172.20.0.255 Mask:255.255.255.0...
4. Docker 删除容器与 containerd 同步
当使用 Docker 删除容器时,containerd
中对应的 Task 和 Container 也会被清理。
# 使用 Docker 删除容器
[root@docker102:~# docker container rm -f c1
c1# 再次查看 containerd 'moby' Namespace
[root@docker102:~# ctr -n moby task ls
TASK PID STATUS
[root@docker102:~# ctr -n moby c ls
CONTAINER IMAGE RUNTIME
理解这一点的好处:
- 调试 Kubernetes 节点:Kubernetes (使用 Containerd 作为 CRI 时) 管理的 Pod 和容器也可以通过
ctr
在节点的k8s.io
Namespace 下观察到。这对于排查节点上的 Pod 问题非常有帮助。 - 理解架构:清晰地看到 Docker 如何作为
containerd
的一个客户端,有助于理解容器生态系统的分层架构。
五、Docker 数据持久化 (Volume) 与 containerd 观察
Docker 提供了卷(Volume)机制来实现数据持久化,这是比绑定挂载更推荐的方式,因为它由 Docker 管理,更易于备份、迁移和跨平台。
1. 使用 Docker 创建带 Volume 的容器
# -v /oldboyedu-data: 这会创建一个 Docker 管理的匿名卷,并挂载到容器的 /oldboyedu-data 目录
# 注意:这不同于 -v /host/path:/container/path (绑定挂载)
[root@docker102:~]# docker run -d --name c1 -p 81:80 -v /oldboyedu-data registry.cn-hangzhou.aliyuncs.com/yinzhengjie-k8s/apps:v3
6e29d2aa3fdb4781487712a59d80331c0bf07b30488188c6bd0a1dfd8ef40080
2. 在 containerd 中观察 Task
[root@docker102:~]# ctr -n moby task ls
TASK PID STATUS
6e29d2aa3fdb4781487712a59d80331c0bf07b30488188c6bd0a1dfd8ef40080 634196 RUNNING
3. 查找 Docker Volume 在宿主机上的实际路径
Docker Volume 的数据实际存储在宿主机的特定目录下(通常是 /var/lib/docker/volumes/
)。
[root@docker102:~]# docker inspect -f "{{range .Mounts}}{{.Source}}{{end}}" c1
/var/lib/docker/volumes/32022aa9c9904e65a7d37c7ae49e48b73118f3b9795c277576cb1cfad15a589e/_data
4. 验证数据持久化 (通过宿主机和 ctr)
# 在宿主机上向 Volume 目录写入文件
[root@docker102:~]# cp /etc/fstab /var/lib/docker/volumes/32022aa9c9904e65a7d37c7ae49e48b73118f3b9795c277576cb1cfad15a589e/_data# 使用 ctr 进入容器,查看挂载点 /oldboyedu-data
[root@docker102:~]# ctr -n moby task exec -t --exec-id $RANDOM 6e29d2aa3fdb4781487712a59d80331c0bf07b30488188c6bd0a1dfd8ef40080 sh
/ # ls /oldboyedu-data/
fstab
/ # exit
对比:虽然最终效果都是持久化,但 Docker Volume 由 Docker 统一管理,与宿主机文件系统耦合度更低;而 ctr
的绑定挂载则直接将宿主机路径映射进容器,更直接但也需要手动管理宿主机路径。
总结
通过本文的实践,我们深入了解了:
- 如何安装和验证 OCI 运行时
runc
。 - 如何使用
ctr
命令行工具,结合containerd
的 Namespace 概念,管理镜像和容器。 - 如何利用
ctr
的--mount
参数实现基于绑定挂载的数据持久化。 - 如何配置
containerd
并使用ctr
与私有 HTTP 镜像仓库(如 Harbor)进行交互,包括推送和拉取镜像。 - Docker 与
containerd
之间的协作关系,以及如何使用ctr
在moby
Namespace 下观察和操作由 Docker 管理的容器。 - Docker Volume 持久化方式及其在宿主机上的体现。
虽然 ctr
被定位为面向开发和调试的工具,不如 Docker CLI 或 Podman 那样用户友好,但它提供了一种直接、底层的方式来与 containerd
交互。掌握 ctr
对于理解容器运行时的工作原理、排查 Kubernetes 集群节点问题(当 CRI 为 containerd 时)、或进行特定的底层操作非常有价值。