01 需求简介
注: Container runtime 统称为容器运行时
在 Docker 时代,关于容器运行时术语的定义是非常明确的,其为运行和管理容器的软件。但随着 Docker 涵盖的内容日益增多,以及多种容器编排工具的引入,该定义变得日益模糊了。
当你运行一个 Docker 容器时,一般的步骤是:
最初的规范规定,只有运行容器的部分定义为容器运行时,但一般用户,将上述三个步骤都默认为容器运行时所必须的能力,从而让容器运行时的定义成为一个令人困惑的话题。
当人们想到容器运行时,可能会想到一连串的相关概念;runc、runv、lxc、lmctfy、Docker(containerd)、rkt、cri-o。每一个都是基于不同的场景而实现的,均实现了不同的功能。如containerd和cri-o,实际均可使用runc来运行容器,但其实现了如镜像管理、容器API等功能,可以将这些看作是比runc具备的更高级的功能。
可以发现,容器运行时是相当复杂的。每个运行时都涵盖了从低级到高级的不同部分,如下图所示。
根据功能范围划分,将其分为低级容器运行时 (Low level Container Runtime)和高级容器运行时 (High level Container Runtime),其中只关注容器的本身运行通常称为低级容器运行时(Low level Container Runtime)。支持更多高级功能的运行时,如镜像管理及一些gRPC/Web APIs,通常被称为 高级容器运行时 (High level Container Runtime)。需要注意的是,低级运行时和高级运行时有本质区别,各自解决的问题也不同。02 低级容器运行时
低级运行时的功能有限,通常执行运行容器的低级任务。大多数开发者日常工作中不会使用到。其一般指按照 OCI 规范、能够接收可运行 roofs 文件系统和配置文件并运行隔离进程的实现。这种运行时只负责将进程运行在相对隔离的资源空间里,不提供存储实现和网络实现。但是其他实现可以在系统中预设好相关资源,低级容器运行时可通过 config.json 声明加载对应资源。低级运行时的特点是底层、轻量,限制也很一目了然:低级运行时 demo
通过以 root 方式使用 Linux cgcreate、cgset、cgexec、chroot 和 unshare 命令来实现简单容器。首先,以 busybox 容器镜像作为基础,设置一个根文件系统。然后,创建一个临时目录,并将 busybox 解压到该目录中。$ CID=$(docker create busybox)
$ ROOTFS=$(mktemp -d)
$ docker export $CID | tar -xf - -C $ROOTFS
紧接着创建 uuid,并对内存和 CPU 设置限制。内存限制是以字节为单位设置的。在这里,将内存限制设置为 100MB。$ UUID=$(uuidgen)
$ cgcreate -g cpu,memory:$UUID
$ cgset -r memory.limit_in_bytes=100000000 $UUID
$ cgset -r cpu.shares=512 $UUID
例如,如果我们想把我们的容器限制在两个 cpu core 上,可以设定一秒钟的周期和两秒钟的配额(1s=1,000,000us),这将允许进程在一秒钟的时间内使用两个cpu core。$ cgset -r cpu.cfs_period_us=1000000 $UUID
$ cgset -r cpu.cfs_quota_us=2000000 $UUID
接下来在容器中执行命令。
$ cgexec -g cpu,memory:$UUID \
> unshare -uinpUrf --mount-proc \
> sh -c "/bin/hostname $UUID && chroot $ROOTFS /bin/sh"
/ # echo "Hello from in a container"
Hello from in a container
/ # exit
最后,删除前面创建的 cgroup 和临时目录。
$ cgdelete -r -g cpu,memory:$UUID
$ rm -r $ROOTFS
低级运行时demo
为了更好地理解低级容器运行时,以下列举了几个低级运行时代表,各自实现了不同的功能。
runC
runC是目前使用最广泛的容器运行时。它最初是集成在Docker的内部,后来作为一个单独的工具,并以公共库的方式提取出来。
在2015 年,在 Linux 基金会的支持下有了 Open Container Initiative (OCI)(就是负责制定容器标准的组织),Docker 将自己容器格式和运行时 runC 捐给了 OCI。OCI 在此基础上制定了 2 个标准:运行时标准 Runtime Specification (runtime-spec) 和 镜像标准 Image Specification (image-spec) ,下面通过示例,简要介绍一下 runC。首先创建根文件系统。这里我们将再次使用 busybox。
$ mkdir rootfs
$ docker export $(docker create busybox) | tar -xf - -C rootfs
接下来创建一个 config.json 文件
这个命令为容器创建一个模板config.json。
$ cat config.json
{
"ociVersion": "1.0.2",
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/",
"capabilities": {
...
默认情况下,它在根文件系统位于 ./rootfs
的目录下运行命令。
$ sudo runc run mycontainerid
/ # echo "Hello from in a container"
Hello from in a container
rkt(已废弃)
rkt是一个同时具有低级和高级功能的运行时。例如,很像Docker,rkt允许你构建容器镜像,获取和管理本地存储库中的容器镜像,并通过一个命令运行它们。runV
runv 是 OCF 基于管理程序的(Hypervisor-based )运行时 Runtime.runV 兼容 OCF。作为虚拟容器运行时引擎的runV已被淘汰。runV团队与英特尔一起在OpenInfra Foundation中创建了Kata Containers项目youki
Rust是时下最流行的编程语言,而容器开发也是一个时兴的应用领域。将两者结合使用Rust来做容器开发是一个值得尝鲜的体验。youki是使用Rust的实现OCI运行时规范,类似于runc。03高级容器运行时
高级运行时负责容器镜像的传输和管理,解压镜像,并传递给低级运行时来运行容器。通常情况下,高级运行时提供一个守护程序和一个API,远程应用程序可以使用它来运行容器并监控它们,它们位于低层运行时或其他高级运行时之上。高层运行时也会提供一些看似很低级的功能。例如,管理网络命名空间,并允许容器加入另一个容器的网络命名空间。这里有一个类似逻辑分层图,可以帮助理解这些组件是如何结合在一起工作的。高级运行时代表
Docker
Docker 是最早的开源容器运行时之一。它是由平台即服务的公司 dotCloud 开发的,用于在容器中运行用户的应用。Docker 是一个容器运行时,包含了构建、打包、共享和运行容器。Docker 基于 C/S 架构实现,最初是由一个守护程序 dockerd 和 docker 客户端应用程序组成。守护程序提供了构建容器、管理镜像和运行容器的大部分逻辑,以及一些API。命令行客户端可以用来发送命令和从守护进程中获取信息。它是第一个流行开来的运行时间,毫不过分的说,Docker 对容器的推广做出了巨大的贡献。Docker 最初实现了高级和低级的运行时功能,但这些功能后来被分解成单独的项目,如 runc 和 containerd,以前 Docker 的架构如下图所示,现有架构中,docker-containerd 变成了 containerd,docker-runc 变成了 runc。dockerd 提供了诸如构建镜像的功能,而 dockerd 使用 containerd 来提供诸如镜像管理和运行容器的功能。例如,Docker 的构建步骤实际上只是一些逻辑,它解释 Docker文件,使用 containerd 在容器中运行必要的命令,并将产生的容器文件系统保存为一个镜像。Containerd
containerd 是从 Docker 中分离出来的高级运行时。containerd 实现了下载镜像、管理镜像和运行镜像中的容器。当需要运行一个容器时,它会将镜像解压到一个 OCI 运行时 bundle 中,并向 runc 发送 init 以运行它。Containerd 还提供了 API,可以用来与它交互。containerd 的命令行客户端是 ctr 和 nerdctl。$ sudo ctr images pull docker.io/library/redis:latest
列出所有的镜像:
运行容器:
$ sudo ctr container create docker.io/library/redis:latest redis
列出运行容器:
$ sudo ctr container list
停止容器:
$ sudo ctr container delete redis
这些命令类似于用户与 Docker 的互动方式。
rkt(已废弃)
rkt是一个同时具有低级和高级功能的运行时。例如,很像Docker,rkt允许你构建容器镜像,获取和管理本地存储库中的容器镜像,并通过一个命令运行它们。Kubernetes CRI
CRI 在 Kubernetes 1.5 中引入,作为 kubelet 和容器运行时之间的桥梁。社区希望Kubernetes 集成的高级容器运行时实现 CRI。该运行时处理镜像的管理,支持Kubernetes pods,并管理容器,因此根据高级运行时的定义,支持 CRI 的运行时必须是一个高级运行时。低级别的运行时并不具备上述功能。为了进一步了解 CRI,可以看看整个 Kubernetes 架构。kubelet 代表工作节点,位于Kubernetes集群的每个节点上,kubelet负责管理其节点的工作负载。当需要运行工作负载时,kubelet通过CRI与运行时进行通信。由此可以看出,CRI只是一个抽象层,允许切换不同的容器运行时。CRI规范
CRI定义了gRPC API,该规范定义在 Kubernetes 仓库中 cri-api 目录中。CRI定义了几个远程程序调用(RPC)和消息类型。这些RPC用于管理工作负载等内容,如 “拉取镜像”(ImageService.PullImage)、”创建pod”(RuntimeService.RunPodSandbox)、”创建容器”(RuntimeService.CreateContainer)、”启动容器”(RuntimeService.StartContainer)、”停止容器”(RuntimeService.StopContainer)等操作。
例如,通过CRI启动一个新的Pod(篇幅有限,进行了一些简化工作)。RunPodSandbox和CreateContainer RPCs在其响应中返回ID,在后续请求中使用。
ImageService.PullImage({image: "image1"})
ImageService.PullImage({image: "image2"})
podID = RuntimeService.RunPodSandbox({name: "mypod"})
id1 = RuntimeService.CreateContainer({
pod: podID,
name: "container1",
image: "image1",
})
id2 = RuntimeService.CreateContainer({
pod: podID,
name: "container2",
image: "image2",
})
RuntimeService.StartContainer({id: id1})
RuntimeService.StartContainer({id: id2})
可以直接使用 crictl 工具与 CRI 运行时交互,可以用它来调试和测试CRI的相关实现。
cat <
runtime-endpoint: unix:///run/containerd/containerd.sock
EOF
或者通过命令行指定:
crictl --runtime-endpoint unix:///run/containerd/containerd.sock …
关于 crictl 的使用参见官网。
支持 CRI 的运行时
Containerd
containerd应该是目前最流行的CRI运行时。它以插件的方式实现CRI,默认是启用的。它默认在unix套接字上监听消息。从1.2版本开始,它通过 runtime handler 来支持多种低级运行时。运行时处理程序是通过 CRI 中的字段传递,根据该运行时处理程序,containerd 运行 shim 的应用程序来启动容器。这可以用来运行 runc及其他的低级运行时的容器,如 gVisor、Kata Containers等。在 Kubernetes API 中通过 RuntimeClass 进行运行时配置。Docker
docker-shim 是 K8s 社区第一个被开发的,作为kubelet 和 Docker 之间的 shim。随着Docker 将其许多功能分解到 containerd 中,现在通过 containerd 支持 CRI。
当现代版本的 Docker 被安装时,containerd 也一起被安装,CRI 直接与 containerd 对话,随着docker-shim正式废弃,是时候考虑相关迁移的工作了,K8s在这方面做了大量的工作,具体可参看官方文档。
CRI-O
cri-o 是一个轻量级的 CRI 运行时,它支持 OCI,并提供镜像的管理、容器进程管理、监控日志及资源隔离等工作。
cri-o 的通信地址默认是在 /var/run/crio/crio.sock。
下图为CRI插件的演变史。
由于笔者时间、视野、认知有限,本文难免出现错误、疏漏等问题,期待各位读者朋友、业界专家指正交流。
参考文献
1.https://blog.mobyproject.org/where-are-containerds-graph-drivers-145fc9b7255
2.https://insujang.github.io/2019-10-31/container-runtime/
3.https://github.com/cri-o/cri-o
来源:本文转自公众号 DCOS,点击查看原文。
如果你运维转型处于迷茫时,8月19-20日,GOPS 2022 · 深圳站,值得你参加~通信行业数字化转型,AIOps、MLOps、云原生、SRE等相关精彩内容,2022年最值得参加的运维大会,就在这里啦!
近期好文:
“高效运维”公众号诚邀广大技术人员投稿,
投稿邮箱:jiachen@greatops.net,或添加联系人微信:greatops1118.