背景

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标准的容器解决方案比较熟悉的有:

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规范规定容器的基本状态包含以下几种属性:

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

复制

示例如下:

{
"ociVersion": "0.2.0",
"id": "oci-container1",
"status": "running",
"pid": 4422,
"bundle": "/containers/redis",
"annotations": {
"myKey": "myValue"
}
}

复制

生命周期

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

示:

image.png

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

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

复制

运行时配置(Linux)

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

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

元数据主要包括:

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

复制

资源隔离(namespace):

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

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

复制

资源管理:

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

复制

用户进程 :

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

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

Linux下config.json配置示例

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


容器标准包(Bundle)

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

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

复制


OCI Image规范

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

- 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都提供那些功能:

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下我们要运行一个容器,需要做两个准备:

config.json
bundle(filesystem)

复制

1. config.json 准备

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

[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修改为:

/usr/bin/cadvisor -logtostderr

复制

修改后的config.json为:

{
"ociVersion": "1.0.0",
"process": {
..."terminal": false,
"args": [
"/usr/bin/cadvisor",
"-logtostderr"
],
...
}...
}

复制

2. bundle 准备

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

[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

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

[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

复制

在当前目录下执行

[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进程。具体我们会在后续的文章里解释。

[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操作。

[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进去简单看一下。

[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: ::1%32577/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状态。

[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仍在)。

[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/

 

浅析容器运行时奥秘——OCI标准的更多相关文章

  1. 第28 章 : 理解容器运行时接口 CRI

    理解容器运行时接口 CRI CRI 是 Kubernetes 体系中跟容器打交道的一个非常重要的部分.本文将主要分享以下三方面的内容: CRI 介绍 CRI 实现 相关工具 CRI 介绍 在 CRI ...

  2. Kubernetes容器运行时弃用Docker转型Containerd

    文章转载自:https://i4t.com/5435.html Kubernetes社区在2020年7月份发布的版本中已经开始了dockershim的移除计划,在1.20版本中将内置的dockersh ...

  3. CRI 与 ShimV2:一种 Kubernetes 集成容器运行时的新思路

    摘要: 关于 Kubernetes 接口化设计.CRI.容器运行时.shimv2.RuntimeClass 等关键技术特性的设计与实现.     Kubernetes 项目目前的重点发展方向,是为开发 ...

  4. kubernetes/k8s CRI分析-容器运行时接口分析

    关联博客:kubernetes/k8s CSI分析-容器存储接口分析 概述 kubernetes的设计初衷是支持可插拔架构,从而利于扩展kubernetes的功能.在此架构思想下,kubernetes ...

  5. 使用kubeoperator安装的k8s 版本1.20.14 将节点上的容器运行时从 Docker Engine 改为 containerd

    官方文档:https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/migrating-from-dockershim/change-runt ...

  6. 读书笔记-浅析Java运行时数据区

    作为一个 Java 为主语言的程序员,我偶尔也需要 用 C/C++ 写程序,在使用时让我很烦恼的一件事情就是需要对 new 出来的对象进行 delete/free 操作,我老是担心忘了这件事情,从而导 ...

  7. 程序员修神之路--打通Docker镜像发布容器运行流程

    菜菜哥,我看了一下docker相关的内容,但是还是有点迷糊 还有哪不明白呢? 如果我想用docker实现所谓的云原生,我的项目该怎么发布呢? 这还是要详细介绍一下docker了 Docker 是一个开 ...

  8. Xwork概况 XWork是一个标准的Command模式实现,并且完全从web层脱离出来。Xwork提供了很多核心功能:前端拦截机(interceptor),运行时表单属性验证,类型转换,强大的表达式语言(OGNL – the Object Graph NavigationLanguage),IoC(Inversion of Control反转控制)容器等。 ----------------

    Xwork概况 XWork是一个标准的Command模式实现,并且完全从web层脱离出来.Xwork提供了很多核心功能:前端拦截机(interceptor),运行时表单属性验证,类型转换,强大的表达式 ...

  9. VC++中的C运行时库浅析(控制台程序默认使用单线程的静态链接库,而MFC中的CFile类已暗藏了多线程)

    1.概论 运行时库是程序在运行时所需要的库文件,通常运行时库是以LIB或DLL形式提供的.C运行时库诞生于20世纪70年代,当时的程序世界还很单纯,应用程序都是单线程的,多任务或多线程机制在此时还属于 ...

  10. 运行docker容器镜像2(指定容器启动时启动的脚本)

    docker中启动容器有以下两种情况. 第一种是通过 # docker run containerid 启动一个容器. 第二种是重新启动已经关闭的容器. # docker start containe ...

随机推荐

  1. Qt栅格布局、ScrollArea和用户选择界面

    用户选择界面 就我们在实际开发的时候可能需要面对这样一个界面 做个demo试试看 其实我们可以分解一下这个界面 就是除了控制相关的内容,最主要的就是这个界面之上,有一个个动态的输入的控件,上面都是学生 ...

  2. sniff()函数的总结

    作用: sniff()函数主要是用来捕获经过本机网卡的数据包 格式: sniff(filter="",iface="any",prn=function,coun ...

  3. History模式的配置细节

    旧文从语雀迁移过来,原日期:2021-09-13 前言 我们知道,vue 单页面应用打包出来是静态资源,一般需要 nginx 或者其他服务器访问:当如果 Vue Router 是采用 History ...

  4. @LoadBalanced注解原理

    在使用springcloud ribbon客户端负载均衡的时候,可以给RestTemplate bean 加一个@LoadBalanced注解,就能让这个RestTemplate在请求时拥有客户端负载 ...

  5. [数学理论] NP问题解释

    转载自http://m.blog.csdn.net/csshuke/article/details/74909562 希望通过这篇文章可以不仅让计算机相关专业的人可以看懂和区分什么是P类问题什么是NP ...

  6. python之路49 模板层标签 自定义过滤器 模板继承、模型层准备、ORM部分操作

    模板层之标签 {% if 条件1(可以自己写也可以是用传递过来的数据) %} <p>周三了 周三了</p> {% elif 条件2(可以自己写也可以用传递过来的数据) %} & ...

  7. ResourceQuota与LimitRange区别

    ResourceQuota与LimitRange区别 ResourceQuota ResourceQuota 用来限制 namespace 中所有的 Pod 占用的总的资源 request 和 lim ...

  8. 03-Verilog语法

    Verilog语法 1 Register 组合逻辑-->寄存器-->组合逻辑-->寄存器 Register是一个变量,用于存储值,并不代表一个真正的硬件DFF. reg A,C; / ...

  9. string 类的用法

    访问字符串中的元素 string 字符串也可以像C风格的字符串一样按照下标来访问其中的每一个字符.string 字符串的起始下标仍是从 0 开始.请看下面的代码: int main(){ string ...

  10. three.js一步一步来--如何用线画出一个面--网格板子

    网格板子~~~ <template> <div style="width:1000px; height:800px"> <p>网格布局</ ...