1

引言

这是有关Container Runtime(容器运行时)系列文章的第二篇,在上篇文章中,我概述了Container Runtime,并讨论了低级和高级运行时之间的区别,在这篇文章中,我将详细介绍低级(low-level)Container Runtime。

低级运行时具有有限的功能特性集合,通常会执行用于运行容器的低级别任务,绝大多数开发人员不应将它们用于日常工作。低级运行时通常被实现为简单的工具或库,高级运行时和工具的开发人员可以将其用于低级功能。尽管大多数开发人员不会直接使用低级运行时,但最好了解它们的工作原理便于故障排除和调试。

正如在上篇文章中所说的,容器是基于Linux namespaces(命名空间)和cgroups实现的,命名空间可以使你虚拟化系统资源,例如每个容器的文件系统和网络资源;另一方面,cgroup可以提供了一种方法限制每个容器的资源使用量,例如CPU或者内存。低级容器运行时的核心功能是负责为容器创建这些namespaces和cgroups,然后在这些命名空间和cgroups中运行系统命令,大多数容器运行时实现了更多的特性,但是这些功能是必不可少的。一定要去看下Liz Rice的演讲《Building a container from scratch in Go》,她的演讲中很好地展示了低级容器运行时是如何实现的,Liz完成了许多像这样的步骤,但是你可以想象即使是那些勉强可以称得上"container runtime"的容器运行时也可以做以下的事:

创建cgroup
在cgroup里面运行命令
利用Unshare将其移动到自己的命名空间
命令完成后清理cgroup(正在运行的进程未引用命名空间时,它们会自动删除)

但是,一个健壮的低级容器运行时会做更多的事情,例如允许在cgroup上设置资源限制,设置根文件系统以及将容器的进程chroot到根文件系统。

创建一个Runtime的范例

让我们逐步运行一个简单的临时容器运行时用于创建一个容器。我们可以执行以下标准的Linux命令执行这些步骤:

- cgcreate
- cgset
- cgexec
- chroot
- unshare

上面大多数命令需要你以root身份执行。首先为容器创建根文件系统,以busybox Docker容器为基础,这里创建一个临时目录并将busybox提取到这个目录,执行这些命令需要以root用户运行。

$ CID=$(docker create busybox)
$ ROOTFS=$(mktemp -d)
$ docker export $CID | tar -xf - -C $ROOTFS

现在,创建cgroup并设置对内存和CPU的限制,内存限制以字节为单位设置,设置为100MB。

$ UUID=$(uuidgen)
$ cgcreate -g cpu,memory:$UUID
$ cgset -r memory.limit_in_bytes=100000000 $UUID
$ cgset -r cpu.shares=512 $UUID
可以通过两种方法的一种限制CPU使用率,这里使用CPU共享(shares)来限制CPU使用率:在同一时间与其他进程按比例划分CPU时间,独立运行的容器可以享有所有的CPU时间,但是如果有其他的容器也在运行,就能使用CPU shares按比例划分CPU时间。基于CPU核数的CPU限制要复杂一些,它们能让你对容器的CPU核数设置硬性的限制。想要限制CPU核数需要两个步骤:cfs_period_us和cfs_quota_us。cfs_period_us用来指定cpu分配的周期,默认为100000;cfs_quota_us指定单个任务在一个CPU周期内占用的时间,默认为-1,表示不限制。如果设为50000,表示占用50000/10000=50%的CPU;两者都以微秒为单位指定。

在本例中,如果想将容器的CPU内核限制为2个,则可以指定设置cfs_period_us为100000(100000ms),cfs_quota_us设置为200000,这样设置的话进程就能在一秒内有效使用2个cpu时间,本文将深入剖析这个概念。

$ cgset -r cpu.cfs_period_us = 1000000  $ UUID 
$ cgset -r cpu.cfs_quota_us = 2000000  $ UUID

接着,在容器中执行以下命令,这条命令会在前面创建的cgroup里面执行,unshare指定的命名空间,设置hostname,同时将chroot设置为容器的根文件系统。

$ 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

为了进一步论证其工作原理,我在bash里面写了一个简单的runtime叫做execc,它支持mount、user、pid、ipc、uts和network命名空间,同时设置了内存限制和CPU核数限制,挂载了proc文件系统同时让容器运行在自己的根文件系统之上。

一些低级容器运行时的例子

为了更好地理解低级容器运行时,最好来看一些示例,这些不同的运行时实现了不同的特性并强调了容器化的不同方面。

imctfy

尽管没有得到广泛使用,Imctfy也是记录在册一个容器运行时。它是google一个内部项目,brog系统内部使用的容器运行时正是Imctfy,它最有趣的一个特性是支持通过容器名称使用cgroup层级的容器层次结构。例如,一个叫busybox的父容器可以在"busybox/sub1"或者"busybox/sub2"下面创建子容器,这些名称之间构成一种路径结构。这样做的结果就是每个子容器都有自己的cgroups但同时也受到父容器cgroup的限制。这是受Borg启发的,它使lmctfy中的容器能够在服务器上预先分配的一组资源下运行子任务容器,从而实现了比运行时本身更严格的SLOs。

尽管lmctfy提供了一些有趣的特性,但其他运行时的特性更优异,因此Google决定让社区将精力集中在Docker的libcontainer而不是lmctfy上。

runc

runc是目前使用最广泛的容器运行时,最初是作为Docker的一部分,后来被剥离为单独的工具和库。在内部,runc运行容器的方式与我上面描述的方式类似,但是runc实现了OCI运行时规范。 这意味着它以特定的“ OCI bundle”规范运行容器,bundle软件有用于配置的config.json文件和用于容器的根文件系统,可以通过阅读GitHub上的OCI运行时规范了解更多信息,也可以从runc GitHub项目中学习如何安装runc。
首先创建根文件系统,这里再次使用busybox。

$ mkdir rootfs
$ docker export $(docker create busybox) | tar -xf - -C rootfs

接着创建一个config.json文件。

$ runc spec

这个命令会为容器创建一个config.json文件的模版,看起来是这个样子:

{
        "ociVersion": "1.0.0",
        "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": {
...

默认情况下,它会在有着根文件系统的容器中运行sh命令,默认位置是./rootfs。 因为这正是我们想要的设置,所以继续运行容器。

默认情况下,它会在有着根文件系统的容器中运行sh命令,默认位置是./rootfs。 因为这正是我们想要的设置,所以继续运行容器。

rkt

rkt是CoreOS主持开发,用于替代Docker/runc的流行方案。 rkt有点难以分类,因为它提供了其他低级运行时(如runc)提供的所有功能,而且还提供了高级运行时的典型功能。 在这里,我将描述rkt的低级功能,将高级功能留给下一篇文章。

rkt最初使用的是应用程序容器(appc)标准,该标准是作为Docker容器格式的开放替代标准而开发的。 Appc从未以容器格式获得广泛采用,并且不再积极开发appc来实现其目标,以确保社区可以使用开放标准。在未来, rkt使用OCI容器格式来替代appc。

应用程序容器镜像(ACI)是Appc的镜像格式, 格式是tar.gz,其中包含一个manifest文件目录和根文件系统的rootfs目录,可以在此处阅读有关ACI的更多信息。
你也可以使用acbuild工具构建容器镜像,在可运行的Docker脚本中使用acbuild,就像运行Dockerfiles一样。

acbuild begin
acbuild set-name example.com/hello
acbuild dep add quay.io/coreos/alpine-sh
acbuild copy hello /bin/hello
acbuild set-exec /bin/hello
acbuild port add www tcp 5000
acbuild label add version 0.0.1
acbuild label add arch amd64
acbuild label add os linux
acbuild annotation add authors "Carly Container <carly@example.com>"
acbuild write hello-0.0.1-linux-amd64.aci
acbuild end

EngineerLeo
598 声望38 粉丝

专注于云原生、AI等相关技术