Container Runtime (二) :低级容器运行时

引言

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

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

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

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

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

建立一个Runtime的范例

让咱们逐步运行一个简单的临时容器运行时用于建立一个容器。咱们能够执行如下标准的Linux命令执行这些步骤:docker

- cgcreate
- cgset
- cgexec
- chroot
- unshare

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

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

如今,建立cgroup并设置对内存和CPU的限制,内存限制以字节为单位设置,设置为100MB。bash

$ 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和临时目录完成清理。app

$ cgdelete -r -g cpu,memory:$UUID
$ rm -r $ROOTFS

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

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

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

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