随着容器技术发展的愈发火热,Linux基金会于2015年6月成立 OCI(Open Container Initiative) 组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。该组织一成立便得到了包括谷歌、微软、亚马逊、华为等一系列云计算厂商的支持。而runC就是Docker贡献出来的,按照该开放容器格式标准(OCF, Open Container Format)制定的一种具体实现。本文作者及浙大团队将在接下来的容器系列文章中,从架构和源码层面详细解读这个开源项目的设计思想和实现原理,敬请关注。
制定容器格式标准的 宗旨 概括来说就是不受上层结构的绑定,如特定的客户端、编排栈等,同时也不受特定的供应商或项目的绑定,即不限于某种特定操作系统、硬件、CPU架构、公有云等。
该标准目前由 libcontainer 和 appc 的项目负责人(maintainer)进行维护和制定,其规范文档就作为一个项目在GitHub上维护,地址为 https://github.com/opencontainers/specs 。
标准化容器的宗旨具体分为如下五条。
操作标准化:容器的标准化操作包括使用标准容器感觉创建、启动、停止容器,使用标准文件系统工具复制和创建容器快照,使用标准化网络工具进行下载和上传。
内容无关:内容无关指不管针对的具体容器内容是什么,容器标准操作执行后都能产生同样的效果。如容器可以用同样的方式上传、启动,不管是php应用还是mysql数据库服务。
基础设施无关:无论是个人的笔记本电脑还是AWS S3,亦或是Openstack,或者其他基础设施,都应该对支持容器的各项操作。
为自动化量身定制:制定容器统一标准,是的操作内容无关化、平台无关化的根本目的之一,就是为了可以使容器操作全平台自动化。
工业级交付:制定容器标准一大目标,就是使软件分发可以达到工业级交付成为现实。
一个标准的容器包具体应该至少包含三块部分:
config.json
: 基本配置文件,包括与宿主机独立的和应用相关的特定信息,如安全权限、环境变量和参数等。具体如下: runtime.json
配置中保持一致) runtime.json
: 运行时配置文件,包含运行时与主机相关的信息,如内存限制、本地设备访问权限、挂载点等。除了上述配置信息以外,运行时配置文件还提供了“钩子(hooks)”的特性,这样可以在容器运行前和停止后各执行一些自定义脚本。hooks的配置包含执行脚本路径、参数、环境变量等。 rootfs/
:根文件系统目录,包含了容器执行所需的必要环境依赖,如 /bin
、 /var
、 /lib
、 /dev
、 /usr
等目录及相应文件。rootfs目录必须与包含配置信息的 config.json
文件同时存在容器目录最顶层。 容器标准格式也要求容器把自身运行时的状态持久化到磁盘中,这样便于外部的其他工具对此信息使用和演绎。该运行时状态以JSON格式编码存储。推荐把运行时状态的json文件存储在临时文件系统中以便系统重启后会自动移除。
基于Linux内核的操作系统,该信息应该统一地存储在 /run/opencontainer/containers
目录,该目录结构下以容器ID命名的文件夹( /run/opencontainer/containers/<containerID>/state.json
)中存放容器的状态信息并实时更新。有了这样默认的容器状态信息存储位置以后,外部的应用程序就可以在系统上简便地找到所有运行着的容器了。
state.json
文件中包含的具体信息需要有:
state.json
文件中加入容器ID是为了便于之前提到的运行时hooks只需载入 state.json
就可以定位到容器,然后检测 state.json
,发现文件不见了就认为容器关停,再执行相应预定义的脚本操作。 state.json
就可以定位到宿主机上的容器文件目录。 标准的容器生命周期应该包含三个基本过程。
config.json
中, args
项。 从上述几点中总结来看,开放容器规范的格式要求非常宽松,它并不限定具体的实现技术也不限定相应框架,目前已经有基于OCF的具体实现,相信不久后会有越来越多的项目出现。
runC的前身实际上是Docker的libcontainer项目,笔者曾经写过一篇文章 《Docker背后的容器管理——Libcontainer深度解析》 专门对libcontainer进行源码分析和解读,感兴趣的读者可以先阅读一下,目前runC也是对libcontainer包的调用,libcontainer包变化并不大。所以此文将不再花费太多笔墨分析其源码,我们将着重讲解其中的变化。
从本质上来说,容器是提供一个与宿主机系统共享内核但与系统中的其他进程资源相隔离的执行环境。Docker通过调用libcontainer包对namespaces、cgroups、capabilities以及文件系统的管理和分配来“隔离”出一个上述执行环境。同样的,runC也是对libcontainer包进行调用,去除了Docker包含的诸如镜像、Volume等高级特性,以最朴素简洁的方式达到符合OCF标准的容器管理实现。
总体而言,从libcontainer项目转变为runC项目至今,其功能和特性并没有太多变化,具体有如下几点。
nsinit
移除,放到外面,命令名称改为 runc
,同样使用 cli.go
实现,一目了然。 config.json
和 runtime.json
两个。 hook
脚本功能。 nsinit
时期的指令,增加了 runc kill
命令,用于发送一个 SIG_KILL
信号给指定容器ID的 init
进程。 总体而言,runC希望包含的特征有:
从开放容器标准中我们已经定义了关于容器的两份配置文件和一个依赖包,runc就是通过这些来启动一个容器的。首先我们按照官方的步骤来操作一下。
runc运行时需要有rootfs,最简单的就是你本地已经安装好了Docker,通过
docker pull busybox
下载一个基本的镜像,然后通过
docker export $(docker create busybox) > busybox.tar
导出容器镜像的rootfs文件压缩包,命名为 busybox.tar
。然后解压缩为 rootfs
目录。
mkdir rootfs tar -C rootfs -xf busybox.tar
这时我们就有了OCF标准的rootfs目录,需要说明的是,我们使用Docker只是为了获取rootfs目录的方便,runc的运行本身不依赖Docker。
接下来你还需要 config.json
和 runtime.json
,使用
runc spec
可以生成一份标准的 config.json
和 runtime.json
配置文件,当然你也可以按照格式自己编写。
如果你还没有安装 runc
,那就需要按照如下步骤安装一下,目前 runc
暂时只支持Linux平台。
# create a 'github.com/opencontainers' in your GOPATH/src cd github.com/opencontainers git clone https://github.com/opencontainers/runc cd runc make sudo make install
最后执行
runc start
你就启动了一个容器了。
可以看到,我们对容器的所有定义,均包含在两份配置文件中,一份简略的 config.json
配置文件类似如下,已用省略号省去部分信息,完整的可以查看 官方github 。
{ "version": "0.1.0", "platform": { "os": "linux", "arch": "amd64" }, "process": { "terminal": true, "user": { "uid": 0, "gid": 0, "additionalGids": null }, "args": [ "sh" ], "env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM=xterm" ], "cwd": "" }, "root": { "path": "rootfs", "readonly": true }, "hostname": "shell", "mounts": [ { "name": "proc", "path": "/proc" }, …… { "name": "cgroup", "path": "/sys/fs/cgroup" } ], "linux": { "capabilities": [ "CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE" ] } }
各部分表示的意思在1.2节中已经讲解,针对具体的内容我们可以看到,版本是0.10,该配置文件针对的是AMD64架构下的Linux系统,启动容器后执行的命令就是 sh
,配置的环境变量有两个,是 PATH
和 TERM
,启动后user的uid和gid都为0,表示进入后是root用户。 cwd
项为空表示工作目录为当前目录。capabilities能力方面则使用白名单的形式,从配置上可以看到只允许三个能力,功能分别为允许写入审计日志、允许发送信号、允许绑定socket到网络端口。
一份简略的 runtime.json
配置则如下,同样用省略号省去了部分内容:
{ "mounts": { "proc": { "type": "proc", "source": "proc", "options": null }, …… "cgroup": { "type": "cgroup", "source": "cgroup", "options": [ "nosuid", "noexec", "nodev", "relatime", "ro" ] } }, "hooks": { "prestart": null, "poststop": null }, "linux": { "uidMappings": null, "gidMappings": null, "rlimits": [ { "type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024 } ], "sysctl": null, "resources": { "disableOOMKiller": false, "memory": { "limit": 0, "reservation": 0, "swap": 0, "kernel": 0, "swappiness": -1 }, "cpu": { "shares": 0, "quota": 0, "period": 0, "realtimeRuntime": 0, "realtimePeriod": 0, "cpus": "", "mems": "" }, "pids": { "limit": 0 }, "blockIO": { "blkioWeight": 0, "blkioWeightDevice": "", "blkioThrottleReadBpsDevice": "", "blkioThrottleWriteBpsDevice": "", "blkioThrottleReadIopsDevice": "", "blkioThrottleWriteIopsDevice": "" }, "hugepageLimits": null, "network": { "classId": "", "priorities": null } }, "cgroupsPath": "", "namespaces": [ { "type": "pid", "path": "" }, { "type": "network", "path": "" }, { "type": "ipc", "path": "" }, { "type": "uts", "path": "" }, { "type": "mount", "path": "" } ], "devices": [ { "path": "/dev/null", "type": 99, "major": 1, "minor": 3, "permissions": "rwm", "fileMode": 438, "uid": 0, "gid": 0 }, …… { "path": "/dev/urandom", "type": 99, "major": 1, "minor": 9, "permissions": "rwm", "fileMode": 438, "uid": 0, "gid": 0 } ], "apparmorProfile": "", "selinuxProcessLabel": "", "seccomp": { "defaultAction": "SCMP_ACT_ALLOW", "syscalls": [] }, "rootfsPropagation": "" } }
可以看到基本的几项配置分别为挂载点信息、启动前与停止后hooks脚本、然后就是针对Linux的特性支持的诸如用户uid/gid绑定,rlimit配置、namespace设置、cgroups资源限额、设备权限配置、apparmor配置项目录、selinux标记以及seccomp配置。其中,namespaces和cgroups笔者均有写文章详细介绍过。
再下面的工作便都由libcontainer完成了,大家可以阅读这个系列前一篇文章 《Docker背后的容器管理——Libcontainer深度解析》 或者购买书籍 《Docker容器与容器云》 ,里面均有详细介绍。
简单来讲,有了配置文件以后,runC就开始借助libcontainer处理以下事情:
可以看到,具体的执行者是libcontainer,它是对容器的一层抽象,它定义了Process和Container来对应Linux中“进程”与“容器”的关系。一旦上述物理的容器创建成功,其他调用者就可以通过ID获取这个容器,接着使用Container.Stats得到容器的资源使用信息,或者执行Container.Destory来销毁这个容器。
综上,runC实际上是把配置交给libcontainer,然后由libcontainer完成容器的启动,而libcontainer中最主要的内容是Process、Container以及Factory这3个逻辑实体的实现原理。runC或者其他调用者只要依次执行“使用Factory创建逻辑容器Container”、“用Process启动逻辑容器Container”即可。
本文从OCI组织的成立开始讲起,描述了开放容器格式的标准及其宗旨,这其实就是runC的由来。继而针对具体的runC特性及其启动进行了详细介绍。笔者在后续的文章中还将针对runC中诸如CRIU热迁移、selinux、apparmor及seccomp配置等特性进行具体的介绍。可以看到OCI的成立表明了社区及各大厂商对容器技术的肯定以及加快容器技术发展进步的强烈决心,相信在不久的将来,符合OCI标准的开放容器项目会越来越多,容器技术将更加欣欣向荣地不断前进。
孙健波, 浙江大学SEL实验室 硕士研究生, 《Docker容器与容器云》 主要作者之一,目前在云平台团队从事科研和开发工作。浙大团队对PaaS、Docker、大数据和主流开源云计算技术有深入的研究和二次开发经验,团队现将部分技术文章贡献出来,希望能对读者有所帮助。
感谢郭蕾对本文的策划和审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ,@丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群 )。