容器技术-容器运行时之 containerd

1,877 阅读12分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天。

Containerd

Containerd 是一个高度模块化的高级运行时,所有模块均可插拔,模块均以 RPC service 形式注册并调用(gRPC 或者 TTRPC)。不同插件通过声明互相依赖,由 Containerd 核心实现统一加载,使用方可以自行实现插件以实现定制化的功能。当然这种设计虽然使得 Containerd 拥有强大的跨平台、可插拔的能力,同时也带来一些缺点,模块之间功能互调必须通过 RPC 调用。

注: TTRPC 是一种基于 gRPC 的裁剪版通信协议。

containerd 是一个工业级标准的容器运行时,它强调简单性健壮性可移植性,containerd 可以负责干下面这些事情:

  • 管理容器的生命周期(从创建容器到销毁容器)
  • 拉取/推送容器镜像
  • 存储管理(管理镜像及容器数据的存储)
  • 调用 runc 运行容器(与 runc 等容器运行时交互)
  • 管理容器网络接口及网络

image.png

Containerd 的设计是一个大的插件系统,图中每一个虚线框其实就对应一个插件。

从下往上看,底层支持 arm 和 x86 架构,支持 Linux 和 windows 系统。

中间 containerd 包含三层:Backend、core、API。其中 Backend  中 Runtime plugin 提供了容器运行时的具体操作,为了支持不同的容器运行时 containerd 也提供了一系列的 containerd-shim。Core 是核心部分,提供了各种功能的服务,其中比较常用的是 Content service ,提供对镜像中可寻址内容的访问,所有不可变的内容都被存储在这里;Images Service 提供镜像相关的操作;Snapshot Plugin : 用来管理容器镜像的文件系统快照。镜像中的每一个 layer 都会被解压成文件系统快照,类似于 Docker 中的 graphdriver。再往上则是 API 层,通过 GRPC 与客户端连接,这其中提供了给 Prometheus 使用 API 来进行监控,这里给个好评!

最上层则是各种客户端,包括 K8s 的 kubelet,containerd 自己的命令行 ctr 等。

简化一下上图:

图片

architecture.png

简化后,Containerd 分为三大块:Storage 管理镜像文件的存储;Metadata 管理镜像和容器的元数据;另外由 Task 触发运行时。对外提供 GRPC 方式的 API 以及 Metrics 接口。

Containerd-shim

Runtime v2 为运行时实现者引入了一套shim API,以便与 Containerd 集成。shim API 只针对容器的执行生命周期管理。其是用于剥离 Containerd 守护进程与容器进程。目前已有 shim v1 和 shim v2 两个版本;它是Containerd 中的一个插件,其通过 shim 调用低级运行时命令来启动容器。

注: 简单来说引入shim是允许低级运行时(如runc、youki等)在创建和运行容器之后退出, 并将shim作为容器的父进程, 而不是containerd作为父进程,是否还记得Containerd 抽象uds漏洞?

每一个 Containerd 容器都有一个相应的shim守护进程,这个守护进程会提供一个 API,Containerd 使用该 API 来管理容器基本的生命周期(启/停等),在容器中执行新的进程、调整 TTY 的大小以及与特定平台相关的其他操作。shim 还有一个作用是向 Containerd 报告容器的退出状态,在容器退出状态被 Containerd 收集之前,shim 会一直存在。这一点和僵尸进程很像,僵尸进程在被父进程回收之前会一直存在,只不过僵尸进程不会占用资源,而 shim 会占用资源。

k8s + CRI

由于Docker与CRI不兼容,所以在kubelet内部实现了dockershim服务,以便它能与Docker通信。dockershim在Kubernetes 1.20中声明废弃,并已经在Kubernetes 1.24版本着手删除工作,下图为满足CRI的运行时。

image.png

Containerd 是一个高级的容器运行时,它可以与下面列出的低级运行时进行交互。

图片

runc是Open Container Initiative(OCI)指定的容器运行时的实现参照。其是诸多Kubernetes发行版默认安装和使用的容器运行时,当然,安装和使用其他低级容器运行时也非常方便。为了提高工作负载的安全性,可能需要其他的安全容器。

docker 默认使用containerd容器运行时。

image.png

Containerd 使用

Docker CLI 工具提供了需要增强用户体验的功能,containerd 同样也提供一个对应的 CLI 工具:ctr,不过 ctr 的功能没有 docker 完善,但是关于镜像和容器的基本功能都是有的。接下来我们就先简单介绍下 ctr 的使用。

帮助

直接输入 ctr 命令即可获得所有相关的操作命令使用方式:

➜  ~ ctr
NAME:
   ctr -
        __
  _____/ /______
 / ___/ __/ ___/
/ /__/ /_/ /
\___/\__/_/

containerd CLI


USAGE:
   ctr [global options] command [command options] [arguments...]

VERSION:
   v1.5.5

DESCRIPTION:

ctr is an unsupported debug and administrative client for interacting
with the containerd daemon. Because it is unsupported, the commands,
options, and operations are not guaranteed to be backward compatible or
stable from release to release of the containerd project.

COMMANDS:
   plugins, plugin            provides information about containerd plugins
   version                    print the client and server versions
   containers, c, container   manage containers
   content                    manage content
   events, event              display containerd events
   images, image, i           manage images
   leases                     manage leases
   namespaces, namespace, ns  manage namespaces
   pprof                      provide golang pprof outputs for containerd
   run                        run a container
   snapshots, snapshot        manage snapshots
   tasks, t, task             manage tasks
   install                    install a new package
   oci                        OCI tools
   shim                       interact with a shim directly
   help, h                    Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --debug                      enable debug output in logs
   --address value, -a value    address for containerd's GRPC server (default: "/run/containerd/containerd.sock") [$CONTAINERD_ADDRESS]
   --timeout value              total timeout for ctr commands (default: 0s)
   --connect-timeout value      timeout for connecting to containerd (default: 0s)
   --namespace value, -n value  namespace to use with commands (default: "default") [$CONTAINERD_NAMESPACE]
   --help, -h                   show help
   --version, -v                print the version

镜像操作

拉取镜像

拉取镜像可以使用 ctr image pull 来完成,比如拉取 Docker Hub 官方镜像 nginx:alpine,需要注意的是镜像地址需要加上 docker.io Host 地址:

➜  ~ ctr image pull docker.io/library/nginx:alpine
docker.io/library/nginx:alpine:                                                   resolved       |++++++++++++++++++++++++++++++++++++++|
index-sha256:bead42240255ae1485653a956ef41c9e458eb077fcb6dc664cbc3aa9701a05ce:    exists         |++++++++++++++++++++++++++++++++++++++|
manifest-sha256:ce6ca11a3fa7e0e6b44813901e3289212fc2f327ee8b1366176666e8fb470f24: done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:9a6ac07b84eb50935293bb185d0a8696d03247f74fd7d43ea6161dc0f293f81f:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:e82f830de071ebcda58148003698f32205b7970b01c58a197ac60d6bb79241b0:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:d7c9fa7589ae28cd3306b204d5dd9a539612593e35df70f7a1d69ff7548e74cf:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:bf2b3ee132db5b4c65432e53aca69da4e609c6cb154e0d0e14b2b02259e9c1e3:    done           |++++++++++++++++++++++++++++++++++++++|
config-sha256:7ce0143dee376bfd2937b499a46fb110bda3c629c195b84b1cf6e19be1a9e23b:   done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:3c1eaf69ff492177c34bdbf1735b6f2e5400e417f8f11b98b0da878f4ecad5fb:    done           |++++++++++++++++++++++++++++++++++++++|
layer-sha256:29291e31a76a7e560b9b7ad3cada56e8c18d50a96cca8a2573e4f4689d7aca77:    done           |++++++++++++++++++++++++++++++++++++++|
elapsed: 11.9s                                                                    total:  8.7 Mi (748.1 KiB/s)
unpacking linux/amd64 sha256:bead42240255ae1485653a956ef41c9e458eb077fcb6dc664cbc3aa9701a05ce...
done: 410.86624ms

也可以使用 --platform 选项指定对应平台的镜像。当然对应的也有推送镜像的命令 ctr image push,如果是私有镜像则在推送的时候可以通过 --user 来自定义仓库的用户名和密码。

列出本地镜像

➜  ~ ctr image ls
REF                            TYPE                                                      DIGEST                                                                  SIZE    PLATFORMS                                                                                LABELS
docker.io/library/nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:bead42240255ae1485653a956ef41c9e458eb077fcb6dc664cbc3aa9701a05ce 9.5 MiB linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/s390x -
➜  ~ ctr image ls -q
docker.io/library/nginx:alpine

使用 -q(--quiet) 选项可以只打印镜像名称。

检测本地镜像

➜  ~ ctr image check
REF                            TYPE                                                      DIGEST                                                                  STATUS         SIZE            UNPACKED
docker.io/library/nginx:alpine application/vnd.docker.distribution.manifest.list.v2+json sha256:bead42240255ae1485653a956ef41c9e458eb077fcb6dc664cbc3aa9701a05ce complete (7/7) 9.5 MiB/9.5 MiB true

主要查看其中的 STATUScomplete 表示镜像是完整可用的状态。

重新打标签

同样的我们也可以重新给指定的镜像打一个 Tag:

➜  ~ ctr image tag docker.io/library/nginx:alpine harbor.k8s.local/course/nginx:alpine
harbor.k8s.local/course/nginx:alpine
➜  ~ ctr image ls -q
docker.io/library/nginx:alpine
harbor.k8s.local/course/nginx:alpine

删除镜像

不需要使用的镜像也可以使用 ctr image rm 进行删除:

➜  ~ ctr image rm harbor.k8s.local/course/nginx:alpine
harbor.k8s.local/course/nginx:alpine
➜  ~ ctr image ls -q
docker.io/library/nginx:alpine

加上 --sync 选项可以同步删除镜像和所有相关的资源。

将镜像挂载到主机目录

➜  ~ ctr image mount docker.io/library/nginx:alpine /mnt
sha256:c3554b2d61e3c1cffcaba4b4fa7651c644a3354efaafa2f22cb53542f6c600dc
/mnt
➜  ~ tree -L 1 /mnt
/mnt
├── bin
├── dev
├── docker-entrypoint.d
├── docker-entrypoint.sh
├── etc
├── home
├── lib
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var

18 directories, 1 file

将镜像从主机目录上卸载

➜  ~ ctr image unmount /mnt/mnt

将镜像导出为压缩包

➜  ~ ctr image export nginx.tar.gz docker.io/library/nginx:alpine

从压缩包导入镜像

➜  ~ ctr image import nginx.tar.gz

容器操作

容器相关操作可以通过 ctr container 获取。

创建容器

➜  ~ ctr container create docker.io/library/nginx:alpine nginx

列出容器

➜  ~ ctr container lsCONTAINER    IMAGE                             RUNTIMEnginx        docker.io/library/nginx:alpine    io.containerd.runc.v2

同样可以加上 -q 选项精简列表内容:

➜  ~ ctr container ls -qnginx

查看容器详细配置

类似于 docker inspect 功能。

➜  ~ ctr container info nginx
{
    "ID": "nginx",
    "Labels": {
        "io.containerd.image.config.stop-signal": "SIGQUIT"
    },
    "Image": "docker.io/library/nginx:alpine",
    "Runtime": {
        "Name": "io.containerd.runc.v2",
        "Options": {
            "type_url": "containerd.runc.v1.Options"
        }
    },
    "SnapshotKey": "nginx",
    "Snapshotter": "overlayfs",
    "CreatedAt": "2021-08-12T08:23:13.792871558Z",
    "UpdatedAt": "2021-08-12T08:23:13.792871558Z",
    "Extensions": null,
    "Spec": {
......

删除容器

➜  ~ ctr container rm nginx➜  ~ ctr container lsCONTAINER    IMAGE    RUNTIME

除了使用 rm 子命令之外也可以使用 delete 或者 del 删除容器。

任务

上面我们通过 container create 命令创建的容器,并没有处于运行状态,只是一个静态的容器。一个 container 对象只是包含了运行一个容器所需的资源及相关配置数据,表示 namespaces、rootfs 和容器的配置都已经初始化成功了,只是用户进程还没有启动。

一个容器真正运行起来是由 Task 任务实现的,Task 可以为容器设置网卡,还可以配置工具来对容器进行监控等。

Task 相关操作可以通过 ctr task 获取,如下我们通过 Task 来启动容器:

➜  ~ ctr task start -d nginx/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/

启动容器后可以通过 task ls 查看正在运行的容器:

➜  ~ ctr task lsTASK     PID     STATUSnginx    3630    RUNNING

同样也可以使用 exec 命令进入容器进行操作:

➜  ~ ctr task exec --exec-id 0 -t nginx sh/ #

不过这里需要注意必须要指定 --exec-id 参数,这个 id 可以随便写,只要唯一就行。

暂停容器,和 docker pause 类似的功能:

➜  ~ ctr task pause nginx

暂停后容器状态变成了 PAUSED

  ~ ctr task lsTASK     PID     STATUSnginx    3630    PAUSED

同样也可以使用 resume 命令来恢复容器:

➜  ~ ctr task resume nginx➜  ~ ctr task lsTASK     PID     STATUSnginx    3630    RUNNING

不过需要注意 ctr 没有 stop 容器的功能,只能暂停或者杀死容器。杀死容器可以使用 task kill 命令:

➜  ~ ctr task kill nginx➜  ~ ctr task lsTASK     PID     STATUSnginx    3630    STOPPED

杀掉容器后可以看到容器的状态变成了 STOPPED。同样也可以通过 task rm 命令删除 Task:

➜  ~ ctr task rm nginx➜  ~ ctr task lsTASK    PID    STATUS

除此之外我们还可以获取容器的 cgroup 相关信息,可以使用 task metrics 命令用来获取容器的内存、CPU 和 PID 的限额与使用量。

# 重新启动容器➜  ~ ctr task metrics nginxID       TIMESTAMPnginx    2021-08-12 08:50:46.952769941 +0000 UTCMETRIC                   VALUEmemory.usage_in_bytes    8855552memory.limit_in_bytes    9223372036854771712memory.stat.cache        0cpuacct.usage            22467106cpuacct.usage_percpu     [2962708 860891 1163413 1915748 1058868 2888139 6159277 5458062]pids.current             9pids.limit               0

还可以使用 task ps 命令查看容器中所有进程在宿主机中的 PID:

➜  ~ ctr task ps nginxPID     INFO3984    -4029    -4030    -4031    -4032    -4033    -4034    -4035    -4036    -➜  ~ ctr task lsTASK     PID     STATUSnginx    3984    RUNNING

其中第一个 PID 3984 就是我们容器中的1号进程。

命名空间

另外 Containerd 中也支持命名空间的概念,比如查看命名空间:

➜  ~ ctr ns lsNAME    LABELSdefault

如果不指定,ctr 默认使用的是 default 空间。同样也可以使用 ns create 命令创建一个命名空间:

➜  ~ ctr ns create test➜  ~ ctr ns lsNAME    LABELSdefaulttest

使用 remove 或者 rm 可以删除 namespace:

➜  ~ ctr ns rm testtest➜  ~ ctr ns lsNAME    LABELSdefault

有了命名空间后就可以在操作资源的时候指定 namespace,比如查看 test 命名空间的镜像,可以在操作命令后面加上 -n test 选项:

➜  ~ ctr -n test image lsREF TYPE DIGEST SIZE PLATFORMS LABELS

我们知道 Docker 其实也是默认调用的 containerd,事实上 Docker 使用的 containerd 下面的命名空间默认是 moby,而不是 default,所以假如我们有用 docker 启动容器,那么我们也可以通过 ctr -n moby 来定位下面的容器:

➜  ~ ctr -n moby container ls

同样 Kubernetes 下使用的 containerd 默认命名空间是 k8s.io,所以我们可以使用 ctr -n k8s.io 来查看 Kubernetes 下面创建的容器。后续我们再介绍如何将 Kubernetes 集群的容器运行时切换到 containerd