浅析容器运行时奥秘——OCI标准

2022-04-25 10:33:59 浏览数 (3)

image.pngimage.png

腾讯蓝鲸智云,简称蓝鲸,是腾讯互动娱乐事业群(Interactive Entertainment Group,简称 IEG)自研自用的一套用于构建企业研发运营一体化体系的 PaaS 开发框架,该套体系不仅提供了基础运维(发布变更、监控处理、数值调整、数据提取等)的无人值守服务,而且还给运维人员提供了解决方案(工具),并随时调整,避免重复性的操作服务。

导语

容器技术火起来了以后,Docker的容器镜像和容器运行时已然成为行业的标准。此后,为了推进容器生态的健康发展。在Linux基金会的主导下,Docker和各大云厂商Google, Amazon, CloudFoundary, Microsoft积极响应于2016年成立了 “Open Container Initiative”,旨在主导容器的生态发展方向,促进容器生态的健康发展。本文主要介绍容器底层的运行标准OCI的背景和主要内容,最后通过用runC构建容器的示例带你了解容器背后不一样的故事。


背景

2013年Docker开源了容器镜像格式和运行时以后,为我们提供了一种更为轻量、灵活的“计算、网络、存储”资源虚拟化和管理的解决方案,在业界迅速火了起来。

2014年更是容器技术发展的一个爆发点,各种容器编排工具也逐步开始发力。值得一提的是Google发布了Kubernetes的第一个Release版本,现已成长为容器编排领域的领导者。

我们知道Docker的容器运行时解决方案采用的两个核心技术:Namespace(资源隔离)和Cgroup(资源管理),并不是Docker实现的。这两项技术其实在Docker之前早已进入Linux内核。换种说法就是Docker的容器解决方案离不开Linux内核的支持。这就是说行业的各个大佬如果自己想搞,都可以利用这两项技术自己做一套类似于Docker的容器解决方案。

到这里就引出了故事的另一个主角: Linux基金会。

容器技术火起来了以后,Docker的容器镜像和容器运行时已然成为行业的标准。彼时,Docker正当红,对各大佬(Linux基金会、谷歌、微软等)提出的合作邀请充耳不闻,态度强硬,力图独自主导容器生态的发展。加上Docker在Runtime的向下兼容性的问题,社区口碑较差。此时,各大佬就纷纷表示要另起炉灶、自已干,其中比较有代表性的就是Google声称要fork一个分支自己干。

不过,Linux基金会最后还是拉着前边提的这些大佬向Docker施压,最终Docker屈服,并于 2015 年 6 月在 Docker 大会DockerCon上推出容器标准,随后便成立了OCI(Open Container Initiative),并发展成为Linux基金会下的一个项目。在OCI的官网可以看到如下描述:

The Open Container Initiative (OCI) is a lightweight, open governance structure (project), formed under the auspices of the Linux Foundation, for the express purpose of creating open industry standards around container formats and runtime. The OCI was launched on June 22nd 2015 by Docker, CoreOS and other leaders in the container industry.

The OCI currently contains two specifications: the Runtime Specification (runtime-spec) and the Image Specification (image-spec). The Runtime Specification outlines how to run a “filesystem bundle” that is unpacked on disk. At a high-level an OCI implementation would download an OCI Image then unpack that image into an OCI Runtime filesystem bundle. At this point the OCI Runtime Bundle would be run by an OCI Runtime.

在这两段描述中透露出2点关键信息:

  • OCI是在Linux基金会主导下的轻量级的开源管理项目。旨在为容器格式和运行时构建开放的行业标准。
  • OCI标准目前包含两部分内容:
    • 容器运行时规范: 该规范定义了如何根据相应的配置构建容器运行时。
    • 容器镜像规范: 该规范定义了容器运行时使用的镜像的打包规范。

总的来说OCI的成立促进了社区的持续创新,同时可以防止行业由于竞争导致的碎片化,容器生态中的各方都能从中获益。目前在行业中遵循OCI标准的容器解决方案比较熟悉的有:

代码语言:txt复制
Docker
Rocket(CoreOS)
warden (Cloud Foundary)

OCI Runtime 规范

基本理念

OCI规范了容器的配置、执行环境和生命周期管理。容器的配置信息由config.json配置文件来管理。规范容器的执行环境可以保证容器内运行的应用在生命周期内拥有一致的运行环境。总的来说OCI希望通过规范容器的配置、执行环境和生命周期管理,进而达到Docker所提出的“Build, Ship, and Run any app, anwhere”愿景,为了达到这个目的,OCI在制定之初提出了以下5个理念:

1. 操作标准化:

对容器整个生命周期内相关的标准化进行标准化,包括:创建、启动、停止、创建快照、暂停、恢复等操作。规范每个操作的具体含义,将容器的具体操作进行原子化规范。

2. 内容无关:

内容无关指不管针对的具体容器内容是什么,容器标准操作执行后都能产生同样的效果。如容器可以用同样的方式上传、启动,不管是PostgreSQL还是MySQL数据库服务。

3. 基础设施无关:

容器可以运行在任何支持OCI的基础设施上。

4. 为自动化而生:

由于容器的标准操作与基础设施无关,这样就为我们更好的进行自动化管理提供了良好的基础。以前那些耗时、耗力需要投入大量人力的工作,现在就可以利用容器进行自支管理。

制定容器统一标准,是的操作内容无关化、平台无关化的根本目的之一,就是为了可以使容器操作全平台自动化。

5. 工业级交付:

容器标准化能够使软件应用的分发可以达到工业级的交付。标准容器使得我们可以构建自动化的软件交付流水线。不管是内部的DevOps流,还是外部的软件交付机制,容器正在一点点的改变我们对软件打包和交付的认识。

基本属性

OCI规范规定容器的基本状态包含以下几种属性:

代码语言:txt复制
- ociVersion:oci规范的版本信息
- id: 容器的ID, 在同一主机上必须唯一,对于不同主机的容器ID,不做强制性要求。
- status: 容器的运行状态,包含以下几种:
  creating: 正在被创建
  created: 容器进程未退出,而用户的应用进程还未执行的状态。
  running: 容器进程已经退出,而且用户的应用进程已经开始正常运行。
  stopped: 容器进程已经退出。
- pid: 容器进程ID。
- bundle: 容器标准包的绝对路径。包含了容器的具体运行时配置信息和root文件系统。
- annotations: 容器的自定义属性信息。

示例如下:

代码语言:txt复制
{
    "ociVersion": "0.2.0",
    "id": "oci-container1",
    "status": "running",
    "pid": 4422,
    "bundle": "/containers/redis",
    "annotations": {
        "myKey": "myValue"
    }
}
生命周期

OCI定义了容器的生命周期中四个基本的状态: creating, created, running, stopped。各状态的转换如下图所

示:

image.pngimage.png

需要注意的是OCI在start操作中预置了3个勾子函数prestart, poststart, poststop。用于在容器进程,用户进程启动前后进行一些定制化的操作。

代码语言:txt复制
prestart: 只能在运行时进行调用,如果调用失败需要清除容器进程。prestart会在start命令执行后,但还未启动用户进程之前进行调用。对Linux来讲,prestart会在容器命名空间创建完成后调用。
poststart:该hook会在启动完用户进程,但start操作还未返回前进行调用。比如,我们可以通过poststart hook通知用户容器的进程已经启动。
poststop: 会在容器被删除但是删除命令还未返回之前被调用。
运行时配置(Linux)

由于容器Runtime的配置文件config.json在各平台下的配置略有不同,本文主要介绍常见的Linux平台下的配置。

容器Runtime配置主要围绕元数据、资源隔离、资源管理、用户进程几个维度展开:

元数据主要包括:

代码语言:txt复制
- oci的版本信息: ociVersion
- 容器运行的根文件系统(root filesystem)路径和读写权限。
"root": {
    "path": "rootfs",
    "readonly": true
 }
- hostname配置。
- 用户配置

资源隔离(namespace):

对于Linux来讲,OCI支持Linux内核支持的7种类型,具体来讲如下:

代码语言:txt复制
- pid: 保证用户进程只能看到所在容器内的其它进程。
- network:使容器拥有自已的网络栈。
- mount: 使容器拥有隔离的mount表。
- ipc: 使容器内的进程拥有系统级的IPC资源隔离。
- uts: 容器可以使用自已的hostname和domainname。
- user: 使得容器可以对主机和容器内的用户和用户组进行映射。
- cgroup:使得容器拥有独立的cgroup视图。

资源管理:

代码语言:txt复制
- mount:根据用户的需求,顺序对用户的挂载配置项进行挂载操作。每个挂载项包含基本的source, destination配置项。
- rlimit: cpu,mem等的限制。

用户进程 :

用户进程即process配置项,主要包括环境变量、安全、权限控制、OOM管理等内容。当然还有最重要的用户进程的配置。

对Linux来讲,还要求容器内的proc, sysfs, devpts, tmpfs这四个文件系统必须可用。

Linux下config.json配置示例

配置项较多,这里就不列出,感兴趣的可以在这里查看。


容器标准包(Bundle)

容器标准包包含了容器运行的所有环境依赖,它是保证容器运行一致性的基础。一个标准的容器标准包包含所需要加载和启动容器的所有信息。包含两部分内容:

代码语言:txt复制
- config.json: 即前文所述的容器运行时配置内容。
- root filesystem: 即前文所述的root.path所代表的位置。

OCI Image规范

OCI的Image格式规范是容器ship anywhere的基础, 最终落地时体现为Runtime中的bundle,以此为基础为用户提供一致的运行时依赖环境。该规范由Docker贡献,并由社区维护。该规范包含manifest, image index 和 filesystem layers三部分内容。

代码语言:txt复制
- anifest: 
对于指定架构和OS的容器镜像, manifest定义了它所依赖的相关配置信息和对应的layer镜像层信息。
- image index: 
比manifest更高层的抽象,包含了额外的配置信息。
- filesystem layer: 
给出了如何将容器的文件系统进行序列化,如何创建和使用这些layer。我们知道容器的启动速度可达秒级。主要的原因是我们常见的aufs, devicemapper等均采用了COW(copy on write)的技术,使得相同镜像的不同容器实例可以共享bundle,write(修改)的数据也是在layter中。

runC

OCI定义了容器的Runtime和镜像格式两个核心的规范,现在有了规范,还需要一个落地的实体。由此runC诞生了。runC是一个符合OCI规范的轻量级容器运行时生命周期管理工具,最初由Docker贡献给社区,来源于Docker原有的运行时管理部分。Docker也在其v1.11版本以后开始将runC作为自身服务的一个组件。关于这一点我们在后续的文章会里详细介绍。

功能简介

我们先看下runC都提供那些功能:

代码语言:txt复制
1.[root@breeze ~]# runc -h
2.USAGE:
3.runc [global options] command [command options] [arguments...]
4.
5.VERSION:
6.   spec: 1.0.0
7.
8.COMMANDS:
9.     checkpoint  checkpoint a running container
10.     create      create a container
11.     delete      delete any resources held by the container often used with detached container
12.     events      display container events such as OOM notifications, cpu, memory, and IO usage statistics
13.     exec        execute new process inside the container
14.     init        initialize the namespaces and launch the process (do not call it outside of runc)
15.     kill        kill sends the specified signal (default: SIGTERM) to the container's init process
16.     list        lists containers started by runc with the given root
17.     pause       pause suspends all processes inside the container
18.     ps          ps displays the processes running inside a container
19.     restore     restore a container from a previous checkpoint
20.     resume      resumes all processes that have been previously paused
21.     run         create and run a container
22.     spec        create a new specification file
23.     start       executes the user defined process in a created container
24.     state       output the state of a container
25.     update      update container resource constraints
26.     help, h     Shows a list of commands or help for one command
27.
28.GLOBAL OPTIONS:
29.   --debug             enable debug output for logging
30.   --log value         set the log file path where internal debug information is written (default: "/dev/null")
31.   --log-format value  set the format used by logs ('text' (default), or 'json') (default: "text")
32.   --root value        root directory for storage of container state (this should be located in tmpfs) (default: "/run/runc")
33.   --criu value        path to the criu binary used for checkpoint and restore (default: "criu")
34.   --systemd-cgroup    enable systemd cgroup support, expects cgroupsPath to be of form "slice:prefix:name" for e.g. "system.slice:runc:434234"
35.   --help, -h          show help
36.   --version, -v       print the version
37.

如前文的Runtime介绍中所述,runC提供了生命周期管理、暂停、恢复、热迁移、状态查询等操作。具体的细节在此不再赘述。下面我们通过运行一个容器来演示OCI是如何进行容器管理,提供基础的原子操作,与上层的管理系统进行解耦的。

示例

我们通过运行一个容器监控工具cadvisor的容器来展示整个容器管理过程。

由上文可知,在OCI下我们要运行一个容器,需要做两个准备:

代码语言:txt复制
config.json
bundle(filesystem)
1. config.json 准备

config.json定义了容器运行时的具体配置信息,首先我们利用runC生成一个模板,然后在模板上再进行相关的修改。

代码语言:txt复制
[root@breeze runc]$runc spec
[root@breeze runc]$ll
total 4
-rw-r--r-- 1 root root 2597 Nov 14 18:31 config.json

为了方便演示,我们简单修改cadvisor的启动参数,将args修改为:

代码语言:txt复制
/usr/bin/cadvisor -logtostderr

修改后的config.json为:

代码语言:txt复制
{
    "ociVersion": "1.0.0",
    "process": {
        ..."terminal": false,
        "args": [
            "/usr/bin/cadvisor",
            "-logtostderr"
        ],
        ...
    }...
}
2. bundle 准备

runC可以使用符合OCI规范的bundle,前边提到这个规范是Docker贡献的,所以为了简化过程,我们可以直接利用Docker生成这样一个bundle。我们在另外一台部署有Docker的主机上执行以下命令创建cadvisor bundle。

代码语言:txt复制
[root@breeze runc]$ mkdir rootfs
[root@breeze runc]$ docker export $(docker create cadvisor:latest) | tar -C rootfs -xvf -
[root@breeze runc]$ ls rootfs/
bin  glibc-2.23-r3.apk      lib      media  root  srv  usr
dev  glibc-bin-2.23-r3.apk  lib64    mnt    run   sys  var
etc  home                   linuxrc  proc   sbin  tmp
3. create

完成了准备工作,我们就可以创建容器了。现在我们看下当前的目录结构:

代码语言:txt复制
[root@breeze runc]$ll
total 8
-rw-r--r--  1 root root 2597 Nov 14 18:31 config.json
drwxr-xr-x 19 root root 4096 Nov 14 18:27 rootfs

在当前目录下执行

代码语言:txt复制
[root@breeze runc]$ runc create oci-cadvisor
[root@breeze runc]$ runc list
ID             PID         STATUS      BUNDLE       CREATED
oci-cadvisor   17921       created     /home/runc   2017-11-14T12:38:53.64550602Z
[root@breeze runc]$ ps -ef|grep cadvisor
root     18015 22812  0 20:39 pts/0    00:00:00 grep --color=auto cadvisor

从执行输出可以看到oci-cadvisor容器已经create成功,但cadvisor进程还未被拉起。等等,那这个pid(17921)是谁的进程ID?我们来看一下,其实这是runC的init进程。具体我们会在后续的文章里解释。

代码语言:txt复制
[root@breeze runc]$ ps -ef|grep 17921 | grep -v grep
root     17921     1  0 20:38 ?        00:00:00 /proc/self/exeinit
4. start

oci-cadvisor容器的容器进程已经被拉起,接下来需要做的就是把真正的业务进程拉起来。结合前边的生命周期管理图,所以可看我们现在需要执行start操作。

代码语言:txt复制
[root@breeze runc]$ runc start oci-cadvisor
[root@breeze runc]$ runc list
ID             PID         STATUS      BUNDLE       CREATED                         OWNER
oci-cadvisor   17921       running     /home/runc   2017-11-14T12:38:53.64550602Z   root
[root@breeze runc]$ ps -ef|grep cadvisor | grep -v grep
root     17921     1  0 20:38 ?        00:00:00 /usr/bin/cadvisor -logtostderr

现在可以看到oci-cadvisor容器已经run起来了。仔细再观察一下,what? cadvisor进程的pid和前边runc init的进程pid居然是一样的? 这是因为runC通过执行syscall.Exec(Linux 中的exec)让用户进程接管了init进程。

5. exec

现在容器进程也跑起来了,让我们进到oci-cadvisor去看一看。说进容器里看一看,其实是我们再新起了一个进程,而这个进程的命名空间和容器拥有的命名空间,这样就可以通过这个进程去查看容器内的信息。让我们sh进去简单看一下。

代码语言:txt复制
[root@breeze runc]$ runc exec -t  oci-cadvisor /bin/sh
/ # ps aux
PID   USER     TIME   COMMAND
    1 root       0:00 /usr/bin/cadvisor -logtostderr
   30 root       0:00 /bin/sh
   36 root       0:00 ps aux
/ # ifconfig
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::12577/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:16 errors:0 dropped:0 overruns:0 frame:0
          TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1344 (1.3 KiB)  TX bytes:1344 (1.3 KiB)
/ # netstat -nltp|grep 8080
tcp        0      0 :::8080                 :::*                    LISTEN      1/cadvisor

从容器内我们可以看到oci-cadvisor拥有的cadvisor进程,我们刚才exec的sh进程。同样该容器也拥有独立的网络栈。当然,这些只是容器的一部分特性。

kill

接下来,我们kill掉oci-cadvisor容器,使其进行stopped状态。

代码语言:txt复制
[root@breeze oci-cadvisor]$ runc kill oci-cadvisor
[root@breeze oci-cadvisor]$ runc list
ID             PID         STATUS      BUNDLE       CREATED                         OWNER
oci-cadvisor   0           stopped     /home/runc   2017-11-14T12:38:53.64550
delete

最后,我们将oci-cadvisor从我们的环境里清理掉,删除运行时的数据(注意bundle仍在)。

代码语言:txt复制
[root@breeze runc]$ runc delete oci-cadvisor
[root@breeze runc]$ runc list
ID          PID         STATUS      BUNDLE      CREATED     OWNER
[root@breeze runc]$

至此完成了runC对容器的整个生命周期管理过程展示。


写在最后

现在看起来利用runC创建容器,并对其进行管理还是比较简单,解决了容器最核心、最底层、最基础的问题。而这离我们的实际需求还差的很远,想要成为云计算的基础设施还有很长的路要走。具体来说主要面临以下几个问题。

  1. 对大规模管理的支持较弱。runC只是个命令行工具,不是常驻进程,对于大规模的编排需求,无法通过网络调用实现。同样,也无法实现整个容器生命周期的自动化管理。
  2. bundle的管理。OCI包含了OCF规范,但是像我们这样直接利用原生的bundle来构建容器运行时的环境依赖直观上来看有以下几个缺陷:
    • 每个容器都要有自己的bundle,无法复用(应用都有写数据需求),同时带来的是存储资源的浪费和启动速度的下降。
    • 容器的bundle没有统一的管理,“ship anywhere”的愿望看起来可望不可及。
    • bundle的生命周期管理现在还没解决。runC的delete操作并不是清理bundle。
  3. 网络能力弱。容器拥有独立的网络栈,但是还没有解决容器内的业务进程的通信需求,“世界那么大,还是要出去看看的”。

虽然总有不足的地方,但庆幸的是已经迈出了第一步。OCI(Open Container Initiative)组织一成立便得到了包括谷歌、微软、亚马逊、华为等一系列云计算厂商的支持。制定的容器运行时和镜像规范现已经成为一个可靠的基础标准。OCI通过开源的方式以runC落地,逐步脱离Docker的控制范围。在runC的基础上,允许和鼓励多样化的容器解决方案,这为广大的云厂商和我们这些开发者提供了更广阔的发挥空间,不断促进容器生态的持续创新,服务各行各业。


参考文献

https://blog.docker.com/2017/07/oci-release-of-v1-0-runtime-and-image-format-specifications/

https://github.com/opencontainers/runc

https://opensource.com/life/16/8/runc-little-container-engine-could

https://www.opencontainers.org/


蓝鲸智云简介

腾讯蓝鲸智云(简称蓝鲸)软件体系是一套基于PaaS的技术解决方案,致力于打造行业领先的一站式自动化运维平台。目前已经推出社区版、企业版,欢迎体验。

如有需要请联系蓝鲸客服QQ:800802001,有关蓝鲸搭建布署以及使用方面的疑问,可加入QQ群讨论交流:QQ群(495299374)


0 人点赞