我们都知道runc是容器runtime的一个实现,那到底什么是runtime?包含了哪些内容?
容器的runtime和image一样,也有标准,也由OCI (Open Containers Initiative)负责维护,地址为Runtime Specification,了解runtime标准有利于我们更好的理解docker和runc的关系,本文将对该标准做一个简单的解释。
规范内容
下面分别介绍这些规范。
在上一篇中,已经用过bundle了,hello-world的bundle看起来是这个样子的:
dev@debian:~/images$ tree hello-world-bundle
hello-world-bundle
├── config.json
└── rootfs
└── hello
1 directory, 2 files
bundle中包含了运行容器所需要的所有信息,有了这个bundle后,符合runtime标准的程序(比如runc)就可以根据bundle启动容器了。
bundle包含一个config.json文件和容器的根文件系统目录,config.json就是后面要介绍的Container Configuration file,标准要求该配置文件必须叫这个名字,不过对容器的根文件系统目录没有要求,只要在config.json里面将路径配置正确就可以了,不过一般约定俗成都叫rootfs。
实际使用过程中,根文件系统目录可能在其它的地方,只要config.json里面配置正确的路径就可以了,但如果bundle需要打包和其它人分享的话,必须将根文件系统和config.json打包在一起,并且不包含外层的文件夹。
该规范定义了上面介绍的config.json里面应该包含哪些内容,字段很多,这里不一一详细介绍,只简单说明一下,完整的示例请参考这里:
ociVersion(必须):对应的OCI标准版本
root(必须):根文件系统的位置
mounts:需要挂载哪些目录到容器里面。如果是Linux平台,这里面必须要包含/proc、/sys,/dev/pts,/dev/shm这四个目录
process:容器启动后执行什么命令
platform(必须):平台信息,如 amd64 + Linux
linux:Linux平台的特殊配置,这里包含下面要介绍的Linux Container Configuration里面的内容
hooks:配置容器运行生命周期中会调用的hooks,包括prestart、poststart和poststop,容器的生命周期见后面Runtime and Lifecycle介绍。
annotations:容器的注释,相当于容器标签,key:value格式
该规范是Linux平台上对Container Configuration file的扩展,这部分的内容也包含在上面的config.json文件中。
namespaces: namespace相关的配置,相关原理可参考Namespace概述及这些namespace(UTS、IPC、mount、pid、network、user 1、user 2)。
uidMappings,gidMappings:配置主机和容器用户/组之间的对应关系,原理可参考user namespace
devices:设置哪些设备可以在容器内被访问到。除了这里指定的设备外,/dev/null、/dev/zero、/dev/full、/dev/random、/dev/urandom、/dev/tty、/dev/console(如果在process的配置里面启动terminal的话)和/dev/ptmx这些设备默认就能在容器内访问到,即runtime的实现需要默认将这些设备bind到容器内,dev/tty和/dev/ptmx的原理可以参考TTY/PTS概述
cgroupsPath:cgroup的路径,可参考Cgroup概述
sysctl:调整容器运行时的kernel参数,主要是一些网络参数,因为每个network namespace都有自己的协议栈,所以可以修改自己协议栈的参数而不影响别人
seccomp:和安全相关的配置,见Seccomp
rootfsPropagation:设置Propagation类型。可以参考Shared subtrees
maskedPaths:设置容器内的哪些目录对用户不可见
readonlyPaths:设置容器内的哪些目录是只读的
mountLabel:和Selinux有关。
该规范主要定义了跟容器运行时相关的三部分内容,容器的状态、容器相关的操作以及容器的生命周期。
容器的状态
当查询容器的状态时,返回的状态里面至少包含如下信息:
{
“ociVersion”: “0.2.0”,
“id”: “oci-container1”,
“status”: “running”,
“pid”: 4422,
“bundle”: “/containers/redis”,
“annotations”: {
“myKey”: “myValue”
}
}
ociVersion (必须): 创建该容器时使用的OCI runtime的版本
id (必须): 容器ID,本机全局唯一
status (必须): 容器的运行时状态,包含如下状态:
creating: 创建中
created: 创建完成
running: 运行中
stopped: 运行结束
实现runtime时可以包含更多的状态,但不能改变这几个状态的含义
pid (容器是running状态时必须): 容器内第一个进程在系统初始pid namespace中的pid,即在容器外面看到的pid
bundle (REQUIRED): bundle所在位置的绝对路径。bundle里面包含了容器的配置文件和根文件系统。
annotations: 容器的注释,相当于容器标签,来自于容器的配置文件,key:value格式。
容器相关的操作
该部分定义了一个符合runtime标准的实现(如runc)至少需要实现下面这些命令:
state: 返回容器的状态,包含上面介绍的那些内容.
create: 创建容器,这一步执行完成后,容器创建完成,修改bundle中的config.json将不再对已创建的容器产生影响
start: 启动容器,执行config.json中process部分指定的进程
kill: 通过给容器发送信号来停止容器,信号的内容由kill命令的参数指定
delete: 删除容器,如果容器正在运行中,则删除失败。删除操作会删除掉create操作时创建的所有内容。
容器的生命周期
这里以runc为例,说明容器的生命周期
执行命令runc create创建容器,参数中指定bundle的位置以及容器的ID,容器的状态变为creating
runc根据bundle中的config.json,准备好容器运行时需要的环境和资源,但不运行process中指定的进程,这步执行完成之后,表示容器创建成功,修改config.json将不再对创建的容器产生影响,这时容器的状态变成created。
执行命令runc start启动容器
runc执行config.json中配置的prestart钩子
runc执行config.json中process指定的程序,这时容器状态变成了running
runc执行poststart钩子。
容器由于某些原因退出,比如容器中的第一个进程主动退出,挂掉或者被kill掉等。这时容器状态变成了stoped
执行命令runc delete删除容器,这时runc就会删除掉上面第2步所做的所有工作。
runc执行poststop钩子
该规范是Linux平台上对Runtime and Lifecycle的补充,目前该规范很简单,只要求容器运行起来后,里面必须建立下面这些软连接:
# ls -l /dev/fd /dev/std*
lrwxrwxrwx 1 root root 13 May 4 12:32 /dev/fd -> /proc/self/fd
lrwxrwxrwx 1 root root 15 May 4 12:32 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 May 4 12:32 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 May 4 12:32 /dev/stdout -> /proc/self/fd/1
结束语
简单点说,docker负责准备runtime的bundle,而runc负责运行该bundle,并管理容器的整个生命周期。
但对于docker来说,并不是只要准备好根文件系统和配置文件就可以了,比如对于网络,runtime没有做任何要求,只要在config.json中指定network namespace就行了(不指定就新建一个),而至于这个network namespace里面有哪些东西则完全由docker负责,docker需要保证新network namespace里面有合适的设备来和外界通信。
参考