概述

  • 开源容器方案运行时规范(OCI Runtime Spec)主要目的是定义容器运行时相关规范,包括容器运行前的配置,容器生命周期和容器执行环境,当一种容器方案遵照OCI Runtime Spec实现了以上所有东西,我们认为它就是OCI兼容的。目前OCI兼容的容器方案有runc,kata。
  • 本文以分析OCI Runtime Spec为主,过程中使用runc容器方案加以举例,希望在理解OCI规范之后,能够更加轻松地理解runc的命令行接口与设计原则。

准备工作

go

  • runc,containerd,docker都是基于go语言开发,因此需要安装go语言用于编译runc源码,命令如下:
yum install -y golang
  • 检查是否安装成功,命令如下:
[root@hy runc]# go version
go version go1.15 linux/amd64
  • go工具有默认的工作目录GOPATH,当使用build和get命令时,GOPATH环境变量决定了go工具的查找路径,因此如果需要更改环境为个人的工作目录,首先通过如下命令查看go的环境变量:
go env
  • 然后修改GOPATH环境变量,将如下内容写入~/.bashrc中:
export GOPATH="/path/to/mygopath" 

runc

  • 有两种方法下载runc的代码,一种是通过go工具下载,如下:
go get github.com/opencontainers/runc
  • 以上命令会将runc的源码自动下载到$GOPATH/src/github.com/opencontainers/runc目录下,另一种方法是直接通过git工具克隆代码,如下:
cd $GOPATH/src/github.com/opencontainers
git clone https://github.com/opencontainers/runc
  • 以上两种方法最终下载的代码都在$GOPATH/src/github.com/opencontainers/runc目录下,编译并安装runc,命令如下:
cd $GOPATH/src/github.com/opencontainers/runc
make
make install
  • 检查runc工具是否安装成功:
[root@hy ~]# whereis runc
runc: /usr/local/sbin/runc
[root@hy ~]# runc -v
runc version 1.0.0-rc92+dev
commit: 33faa5d0e2404aaf4ceb1abbfe36c5135179d32f
spec: 1.0.2-dev
go: go1.15
libseccomp: 2.3.1

docker

  • 理论上,我们是不需要安装上层的docker工具用于学习runc的,但为了方便下载容器镜像,这里也把docker工具准备好,docker安装如下:
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum makecache -c /etc/yum.repos.d/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io
  • 启动docker服务,并确认docker 命令可用:
systemctl start docker
docker version
Client: Docker Engine - Community
 Version:           19.03.13
 API version:       1.40
 Go version:        go1.13.15
 Git commit:        4484c46d9d
 Built:             Wed Sep 16 17:03:45 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.13
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       4484c46d9d
  Built:            Wed Sep 16 17:02:21 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.3.7
  GitCommit:        8fba4e9a7d01810a393d5d25a3621dc101981175
 runc:
  Version:          1.0.0-rc92+dev
  GitCommit:        33faa5d0e2404aaf4ceb1abbfe36c5135179d32f
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
  • 下载一个常用的docker镜像为测试runc命令行接口做准备,比如busybox:
docker create busybox
docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
busybox             latest              6858809bf669        2 weeks ago         1.23MB
  • 将容器镜像导出,输出到文件:
[root@PC-Hyman containerd]# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
c7e726e36d1a        busybox             "sh"                26 hours ago        Created                                 stoic_noyce
[root@PC-Hyman containerd]# docker export c7e726e36d1a -o busybox.tar
  • 将busybox.tar解压,作为runc运行容器的根文件系统:
tar busybox.tar -C busyboxfs

状态查询

  • 运行时规范规定了容器必须包含的字段如下:
  1. ociVersion:描述容器遵循的OCI规范版本
  2. id:描述容器ID,用于区分同主机上的容器。对于跨主机的容器,id字段可以相同
  3. status:容器的生命周期状态,可以是creating,created,running,stopped,这些状态在生命周期中定义
  4. pid:容器进程ID,在linux平台上,进程ID是必选的。它是容器内部运行的应用程序对应进程的ID
  5. bundle:容器的bundle目录,bundle目录主要存放容器运行时的配置文件和容器的根文件系统
    annotations字段是可选的,存放容器的注释信息。容器的状态信息除以上字段以外,具体的OCI兼容容器方案还可以定义其它字段,视具体的实现而定。
  • 容器的状态可以通过state操作来查询,runc的查询命令如下:
[root@PC-Hyman ~]# runc state 1234
{
  "ociVersion": "1.0.2-dev",
  "id": "1234",
  "pid": 267441,
  "status": "created",
  "bundle": "/home/ubuntuVM/containerd/demo/runc",
  "rootfs": "/home/ubuntuVM/containerd/demo/runc/busyboxfs",
  "created": "2020-09-24T07:58:46.28138205Z",
  "owner": ""
}

生命周期

  • 生命周期描述了容器从创建到最终停止退出的整个时间线,OCI兼容运行时方案必须实现以下生命周期过程中定义的动作。

create

  • 运行时的创建命令(create)需要关联到bundle路径和唯一的容器ID。也就是说,创建命令的核心动作必须包含创建容器ID的动作和保存bundle路径的动作。

environment

  • 运行时环境在启动容器时必须按照配置文件config.json中配置好环境,包括环境变量,挂载点等等。在创建资源的时不允许用户定义的程序运行。当环境好之后,所有对config.json文件的更新都不能影响容器。举例如下:
  • config.json中配置环境变量和挂载点:
"env": [
	"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
	"TERM=xterm"
],

"mounts": [
	{
		"destination": "/dev/pts",
		"type": "devpts",
		"source": "devpts",
		"options": [
			"nosuid",
			"noexec",
			"newinstance",
			"ptmxmode=0666",
			"mode=0620",
			"gid=5"
		]
	}
]
  • 在运行时启动容器之前,必须按照此配置在容器内部准备好这些资源,容器内部看到的信息如下:
/ # env
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

/ # mount |grep pts
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)

start

  • 容器启动操作,核心实现视具体容器方案而定,但必须关联一个容器ID。

delete

  • 容器启动操作,核心实现视具体容器方案而定,但必须关联一个容器ID。

hooks

  • OCI兼容的运行时还必须设计容器生命周期中各个阶段的回调函数,供上层使用者注册自己的程序,包括如下hook:
  1. Prestart:这个hook在start命令之后,用户定义的程序执行之前调用,比如在linux平台上,对于runc运行时方案,prestart hook在容器命令空间之后被执行,这样hook可以有机会定制即将创建的容器。
  2. CreateRuntime:这个hook需要作为create操作的一部分在执行create操作时被调用。它的执行时间介于环境变量配置之后,改变当前所有进程/线程工作目录之前(pivot_root)
  3. CreateContainer:同CreateRuntime hook执行阶段相同,但必须在它之后。
  4. StartContainer:在用户定义的程序执行之前执行。
  5. Poststart:在用户定义的程序执行之后之前。
  6. Poststop:在容器删除的核心操作之后,删除动作返回之前执行。

容器操作

create

create <container-id> <path-to-bundle>

  • 容器创建操作需要指定容器ID和bundle目录,对于runc命令来说,bundle目录是可选的,如果不指定,默认去当前目录下查找。/home/ubuntuVM/containerd/demo/runc目录下放置了一个busybox容器的配置文件config.json以及这个容器的根文件系统
[root@PC-Hyman runc]# ls /home/ubuntuVM/containerd/demo/runc
busyboxfs config.json
  • 指定容器的id和bundle目录,创建该容器:
[root@PC-Hyman demo]# pwd
/home/ubuntuVM/containerd/demo
[root@PC-Hyman demo]# ls
containerd-1.4.1 main main.go runc tools v1.4.1.zip
  • 传入容器ID创建容器,由于当前目录没有配置文件,报错
[root@PC-Hyman demo]# runc create 12345
ERRO[0000] JSON specification file config.json not found
  • 指定bundle目录创建容器
[root@PC-Hyman demo]# runc create 12345 -b /home/ubuntuVM/containerd/demo/runc
[root@PC-Hyman demo]# runc list
ID PID STATUS BUNDLE CREATED OWNER
12345 262814 created /home/ubuntuVM/containerd/demo/runc 2020-09-24T02:23:23.388415943Z root

start

start <container-id>

  • 容器创建之后,其状态时created,通过start操作可以启动容器,启动后容器状态变为running,如果容器内部启动的应用程序是需要长久运行的,比如交互式程序sh,那么容器会一直运行直到被kill掉。如果内部应用程序是运行一段时间就结束的普通程序,那容器状态也会随之变为stopped。

kill

kill <container-id> <signal>

  • 对于一个一直运行的容器,可以通过kill命令向它内部的应用发信号,通知其结束

delete

delete <container-id>

  • 删除一个容器是创建容器的逆操作,它将容器的ID和配置信息容runc得管理中删除

容器配置

  • OCI的配置规范定义了实现容器操作所需要的所有元数据,即容器操作的配置。它可以是json格式,可以是go格式,规范不限制。以下以几个关键的配置字段举例。

root

  • root字段指定了容器的根文件系统,它有两个属性,path和readonly,path定义了根文件系统在主机上的路径,可以是绝对路径,也可以是相对路径(相对bundle目录的路径),readonly声明该文件系统是否只读,如下:
"root": {
	"path": "busyboxfs",          /* 相对bundle的路径 */
	"readonly": true                 /* 根文件系统只读 */
},

mount

  • mount字段指定进程的挂载点信息,如下:
"mounts": [
    {
        "destination": "/proc",    /* 对应挂载点:proc on /proc type proc (rw,relatime) */
        "type": "proc",
        "source": "proc"
    },
    {
        "destination": "/dev",     /* 对应挂载点:tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755) */
        "type": "tmpfs",
        "source": "tmpfs",
        "options": [
            "nosuid",
            "strictatime",
            "mode=755",
            "size=65536k"
         ]
    },
    ....
}

总结

  • 总体来看,OCI的运行时规范比较简单,它将容器运行过程中一些状态的定义,动作的执行规范化,以便于兼容不同的运行时实现方案。
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐