Containerd深度剖析-runtime篇

k8s技术圈

共 5871字,需浏览 12分钟

 ·

2022-05-28 16:01

虽然容器领域的创业随着CoreOS、Docker的卖身,而逐渐归于平寂,但随着Rust语言的兴起,Firecracker、youki项目在容器领域泛起涟漪,对于云原生从业者来说,面试等场景中或多或少都会谈论到容器一些的历史与技术背景。


文|ianlewis
编辑|zouyee

技术深度|简单

需求简介


注: Container runtime统称为容器运行时

Docker时代,关于容器运行时术语的定义是非常明确的,其为运行和管理容器的软件。但随着Docker涵盖的内容日益增多,以及多种容器编排工具的引入,该定义变得日益模糊了。

当你运行一个Docker容器时,一般的步骤是:

  • 下载镜像

  • 将镜像解压成一个bundle,即将各层文件平铺到一个单一的文件系统中。

  • 运行容器

最初的规范规定,只有运行容器的部分定义为容器运行时,但一般用户,将上述三个步骤都默认为容器运行时所必须的能力,从而让容器运行时的定义成为一个令人困惑的话题。

当人们想到容器运行时,可能会想到一连串的相关概念;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)。需要注意的是,低级运行时和高级运行时有本质区别,各自解决的问题也不同。


低级容器运行时

低级运行时的功能有限,通常执行运行容器的低级任务。大多数开发者日常工作中不会使用到。其一般指按照 OCI 规范、能够接收可运行roofs文件系统和配置文件并运行隔离进程的实现。这种运行时只负责将进程运行在相对隔离的资源空间里,不提供存储实现和网络实现。但是其他实现可以在系统中预设好相关资源,低级容器运行时可通过 config.json 声明加载对应资源。低级运行时的特点是底层、轻量,限制也很一目了然:

  • 只认识 rootfs 和 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文件

$ runc spec

这个命令为容器创建一个模板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。


高级容器运行时

高级运行时负责容器镜像的传输和管理,解压镜像,并传递给低级运行时来运行容器。通常情况下,高级运行时提供一个守护程序和一个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。

可以通过ctr拉取一个容器镜像。

$ sudo ctr images pull docker.io/library/redis:latest

列出所有的镜像:

$ sudo ctr images list
运行容器:
$ 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.sockEOF

或者通过命令行指定:

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进行运行时配置。

下图是Containerd的发展史。


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
浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报