容器技术火起来了以后,Docker的容器镜像和容器运行时已然成为行业的标准。此后,为了推进容器生态的健康发展。在Linux基金会的主导下,Docker和各大云厂商Google, Amazon, CloudFoundary, Microsoft积极响应于2015年成立了 “Open Container Initiative”,旨在主导容器的生态发展方向,促进容器生态的健康发展。本文主要介绍容器底层的运行标准OCI的背景和主要内容,最后通过用runc构建容器的示例带你了解容器背后不一样的故事。
2013年,Docker公司(前身为dotCloud)开源了Docker项目,为我们提供了一种更为轻量、灵活的“计算、网络、存储”资源虚拟化和管理的解决方案。最初的版本主要关注容器的打包、分发和运行,使用了libcontainer作为底层容器引擎。这一创新迅速引起了业界的关注和火热反响,推动了容器技术的迅速发展。
2014年,Docker开始引起广泛的关注和采用,许多开发者和企业开始意识到Docker的潜力,将其应用于应用程序的开发、测试和部署等方面;同时,Docker公司发布了Docker Hub,这是一个用于存储和分享Docker镜像的中央仓库,为用户提供了方便的镜像管理和共享平台。另外,2014年更是容器技术发展的一个爆发点,各种容器编排工具也逐步开始发力。值得一提的是Google发布了Kubernetes的第一个Release版本,现已成长为容器编排领域的领导者。
总之,在2013-2014年,Docker技术开始大火,Docker的容器镜像和容器运行时成为行业的标准。在这期间,Docker作为容器届的大明星架子很大,对各大佬(Linux基金会、谷歌、微软等)提出的合作邀请充耳不闻,态度强硬,力图独自主导容器生态的发展。加上Docker在Runtime的向下兼容性的问题,社区口碑较差。此时,各大佬就纷纷表示要另起炉灶、自已干,其中比较有代表性的就是Google声称要fork一个分支自己干。
不过,Linux基金会最后还是拉着前边提的这些大佬向Docker施压,最终Docker屈服,并于2015年6月在 Docker 大会DockerCon上推出容器标准,随后和亚马逊、谷歌和VMware等公司成立了OCI(Open Container Initiative)。OCI成立之初,Docker 公司为其捐赠了容器镜像格式和运行时的草案及相应的实现代码。原来属于Docker 的 libcontainer 项目被捐赠给OCI,成为独立的容器运行时项目 runc。
注意 1:为什么Docker屈服与Linux基金会?
我们知道Docker的容器运行时解决方案采用的两个核心技术:Namespace(资源隔离)和Cgroup(资源管理),并不是Docker实现的。这两项技术其实在Docker之前早已进入Linux内核。换种说法就是Docker的容器解决方案离不开Linux内核的支持。这就是说行业的各个大佬如果自己想搞,都可以利用这两项技术自己做一套类似于Docker的容器解决方案。说的难听点,Docker屈服于Linux基金会是因为Docker技术依赖于Linux内核;说的好听点,Docker选择屈服于Linux基金会并捐赠OCI的目的是为了推动容器技术的开放标准化、促进合作和共享,并构建一个更广泛参与的容器生态系统。这有助于加速容器技术的发展,推动行业的创新和进步。
注意 2:libcontainer是docker捐给oci之后立马改名为runc的吗?
libcontainer并没有立即在被Docker捐赠给OCI(Open Container Initiative)后改名为runc。libcontainer是Docker最初使用的容器引擎,用于实现容器的创建、管理和运行。它是Docker在容器运行时方面的核心组件之一。在2015年时,Docker将libcontainer捐赠给了OCI,成为OCI Runtime Specification的基础。然后,在OCI的基础上,OCI Runtime Specification进一步发展演进,并形成了一个名为"runtime-spec"的项目。后来,为了更好地推进容器运行时的标准化和互操作性,OCI runtime-spec项目与Open Container Initiative的其他相关项目合并,形成了OCI Runtime Bundle规范,并将容器运行时的核心组件命名为"runc"。
OCI(Open Container Initiative)是一个开放容器倡议组织,成立于 2015 年的 OCI 是Linux基金会旗下的合作项目,旨在推动容器运行时和容器镜像格式的开放标准化。该倡议组织的目标是创建一套通用的容器标准,以确保不同容器运行时和工具之间的互操作性和可移植性。
OCI的核心成果是一系列规范和标准,这些规范定义了容器运行时和镜像格式的规范化接口和结构。这些规范包括:
Runtime Specification(运行时规范):定义了容器的生命周期管理,包括创建、启动、停止和销毁容器等操作。最常见的容器运行时实现是OCI Runtime(也称为runc)。
Image Specification(镜像规范):定义了容器镜像的结构和格式,包括镜像的分层结构、元数据和配置等信息。常见的镜像格式是OCI Image(也称为OCI Image Format)。
这些规范和标准确保了不同容器运行时和工具的互操作性,使得用户可以在遵循OCI规范的前提下,使用不同的容器运行时和工具来创建、运行和管理容器。这也促进了容器生态系统的发展,提供了更大的选择和灵活性。目前在行业中遵循OCI标准的容器解决方案比较熟悉的有Docker、Rocket(CoreOS)、warden (Cloud Foundary)等。
除了规范和标准之外,OCI还有一个包含开放源代码项目的工作组,用于实现和推进OCI规范的实际落地。这些项目包括runc(容器运行时实现)、image-spec(镜像规范实现)等。
因此,OCI不仅是一些文档,而是一个综合性的容器标准化倡议组织,通过定义规范和提供开源项目来推动容器生态系统的互操作性和可移植性。
接下来,分别讲解下容器运行时规范和容器镜像规范。
OCI 运行时规范定义了容器配置、运行时和生命周期的标准,主流的容器运行时都遵循OCI运行时的规范,从而提高系统的可移植性和互操作性,用户可根据需要进行选择。
首先,容器启动前需要在文件系统中按一定格式存放所需的文件。OCI运行时规范定义了容器文件系统包(filesystem bundle)的标准,在OCI运行时的实现中通常由高层运行时下载 OCI 镜像,并将OCI镜像解压成OCI运行时文件系统包,然后 OCI 运行时读取配置信息和启动容器里的进程。OCI运行时文件系统包主要包括以下两部分。
然后,在定义文件系统包的基础上,OCI运行时规范制定了运行时和生命周期管理规范。生命周期定义了容器从创建到删除的全过程。总的来说OCI希望通过规范容器的配置、执行环境和生命周期管理,进而达到Docker所提出的“Build, Ship, and Run any app, anwhere”愿景,为了达到这个目的,OCI在制定之初提出了以下5个理念:
注意 1:OCI(Open Container Initiative)主要关注容器的低层运行时(runtime)规范,而不包含高层运行时的规范。而高层运行时(例如Docker、containerd、CRI-O等)则是在OCI规范的基础上构建的,它们提供了更高级别的容器管理和操作功能,例如镜像管理、容器编排、网络管理等。这些高层运行时工具根据OCI规范实现了自己的容器生态系统,为用户提供更便捷的容器使用体验。低层运行时和高层运行时将在其他博文进行详细介绍,本文不过多讲解。
注意 2:本文出现的OCI运行时都是指容器的底层运行时。
OCI规范规定容器的基本状态包含以下几种属性:
示例如下:
{
"ociVersion": "0.2.0",
"id": "oci-container1",
"status": "running",
"pid": 4422,
"bundle": "/containers/redis",
"annotations": {
"myKey": "myValue"
}
}
OCI定义了容器的生命周期中四个基本的状态: creating, created, running, stopped。各状态的转换如下图所示:
需要注意的是OCI在start操作中预置了3个勾子函数prestart, poststart, poststop。用于在容器进程,用户进程启动前后进行一些定制化的操作。
由于容器Runtime的配置文件config.json在各平台下的配置略有不同,本文主要介绍常见的Linux平台下的配置。容器Runtime配置主要围绕元数据、资源隔离、资源管理、用户进程几个维度展开:元数据主要包括:
对于Linux来讲,OCI支持Linux内核支持的7种类型,具体来讲如下:
用户进程即process配置项,主要包括环境变量、安全、权限控制、OOM管理等内容。当然还有最重要的用户进程的配置。对Linux来讲,还要求容器内的proc, sysfs, devpts, tmpfs这四个文件系统必须可用。
容器标准包包含了容器运行的所有环境依赖,它是保证容器运行一致性的基础。一个标准的容器标准包包含所需要加载和启动容器的所有信息。包含两部分内容:
注意 1: OCI Bundle用于描述容器运行时所需的文件和目录的集合。OCI Bundle 是一个文件夹(或目录),其中包含了容器的根文件系统、配置文件、执行上下文等相关内容。它提供了容器运行时所需的一切组件,使得容器的创建、启动和管理变得更加简单和可靠。
在 OCI 规范中,OCI Bundle 必须包含以下几个重要的元素:
根文件系统(Root Filesystem):包含了容器的根文件系统镜像,用于提供容器的文件系统环境。
配置文件(Configuration File):描述容器的配置和运行参数,例如容器的资源限制、环境变量、网络设置等。
执行上下文(Execution Context):包含了容器运行时所需的执行上下文信息,如容器的进程、文件描述符等。
通过定义和组织这些元素,OCI Bundle 提供了一种标准的容器描述方式,使得容器可以在不同的 OCI 兼容的运行时中进行无缝迁移和运行。
总结来说,OCI Bundle 是 Open Container Initiative 规范中定义的一个文件夹,用于描述容器运行时所需的文件和目录的集合。它包含了根文件系统、配置文件和执行上下文等组件,为容器的创建和管理提供了一致的标准化方式。
OCI的Image格式规范是容器ship anywhere的基础, 最终落地时体现为Runtime中的bundle,以此为基础为用户提供一致的运行时依赖环境。该规范由Docker贡献,并由社区维护。该规范包含manifest, image index 和 filesystem layers三部分内容。
OCI(Open Container Initiative)规范定义了容器的底层运行时接口和容器镜像格式,为容器生态系统的互操作性提供了标准。现在有了规范,还需要一个落地的实体。由此runc诞生了(runc是官方提供的容器运行时参考实现)。
runc最初属于Docker公司的libcontainer项目,用于提供容器的底层隔离和资源管理功能。随着Docker公司将libcontainer开源并捐赠给Linux基金会,runc成为OCI的一部分。作为基于libcontainer的重构版本,runc负责容器的生命周期管理,遵循OCI容器运行时规范。
runc在容器生态系统中得到了广泛的应用和采用。作为容器运行时的参考实现,它符合OCI规范,可以与其他兼容OCI规范的工具和生态系统无缝协同工作。runc为开发者和企业提供了一致的容器运行时接口,使得容器的创建、启动、停止和销毁等操作变得更加标准化和可移植。因此,runc在推动容器技术的发展和推广中发挥了重要的作用。
注意 1:Docker也在其v1.11版本以后开始将runc作为自身服务的一个组件。
我们先看下runC都提供那些功能:
[root@node1 ~]# runc -h
NAME:
runc - Open Container Initiative runtime
runc is a command line client for running applications packaged according to
the Open Container Initiative (OCI) format and is a compliant implementation of the
Open Container Initiative specification.
runc integrates well with existing process supervisors to provide a production
container runtime environment for applications. It can be used with your
existing process monitoring tools and the container will be spawned as a
direct child of the process supervisor.
Containers are configured using bundles. A bundle for a container is a directory
that includes a specification file named "config.json" and a root filesystem.
The root filesystem contains the contents of the container.
To start a new instance of a container:
# runc run \[ -b bundle \] <container-id>
Where "
are starting. The name you provide for the container instance must be unique on
your host. Providing the bundle directory using "-b" is optional. The default
value for "bundle" is the current directory.
USAGE:
runc [global options] command [command options] [arguments…]
VERSION:
1.0.3
commit: v1.0.3-0-gf46b6ba
spec: 1.0.2-dev
go: go1.16.15
libseccomp: 2.5.1
COMMANDS:
checkpoint checkpoint a running container
create create a container
delete delete any resources held by the container often used with detached container
events display container events such as OOM notifications, cpu, memory, and IO usage statistics
exec execute new process inside the container
init initialize the namespaces and launch the process (do not call it outside of runc)
kill kill sends the specified signal (default: SIGTERM) to the container's init process
list lists containers started by runc with the given root
pause pause suspends all processes inside the container
ps ps displays the processes running inside a container
restore restore a container from a previous checkpoint
resume resumes all processes that have been previously paused
run create and run a container
spec create a new specification file
start executes the user defined process in a created container
state output the state of a container
update update container resource constraints
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--debug enable debug output for logging
--log value set the log file path where internal debug information is written
--log-format value set the format used by logs ('text' (default), or 'json') (default: "text")
--root value root directory for storage of container state (this should be located in tmpfs) (default: "/run/runc")
--criu value path to the criu binary used for checkpoint and restore (default: "criu")
--systemd-cgroup enable systemd cgroup support, expects cgroupsPath to be of form "slice:prefix:name" for e.g. "system.slice:runc:434234"
--rootless value ignore cgroup permission errors ('true', 'false', or 'auto') (default: "auto")
--help, -h show help
--version, -v print the version
如前文的Runtime介绍中所述,runC提供了生命周期管理、暂停、恢复、热迁移、状态查询等操作。具体的细节在此不再赘述。下面我们通过运行一个容器来演示OCI是如何进行容器管理,提供基础的原子操作,与上层的管理系统进行解耦的。
我们通过运行一个容器监控工具cadvisor的容器来展示整个容器管理过程。
由上文可知,在OCI下我们要运行一个容器,需要做两个准备:config.json、bundle(filesystem) 。
(1)、config.json 准备
config.json定义了容器运行时的具体配置信息,首先我们利用runC生成一个模板,然后在模板上再进行相关的修改。
[root@master1 runc-demo]# pwd && ls
/opt/runc-demo
[root@master1 runc-demo]# runc spec
[root@master1 runc-demo]# ls
config.json
[root@master1 runc-demo]#
为了方便演示,我们简单修改cadvisor的启动参数,将args修改为:/usr/bin/cadvisor -logtostderr ,修改后的config.json为:
{
"ociVersion": "1.0.2-dev",
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"/usr/bin/cadvisor",
"-logtostderr"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/",
……
}
(2)、bundle 准备
runC可以使用符合OCI规范的bundle,前边提到这个规范是Docker贡献的,所以为了简化过程,我们可以直接利用Docker生成这样一个bundle。我们在另外一台部署有Docker的主机上执行以下命令创建cadvisor bundle。
[root@master1 runc-demo]# mdkir rootfs
[root@master1 runc-demo]# docker export $(docker create cadvisor:latest)|tar -C rootfs -xvf-
[root@master1 runc-demo]# ls rootfs/
bin dev etc glibc-2.28-r0.apk glibc-bin-2.28-r0.apk home lib lib64 media mnt proc root run sbin srv sys tmp usr var
(3)、create
完成了准备工作,我们就可以创建容器了。现在我们看下当前的目录结构:
[root@master1 runc-demo]# ls
config.json rootfs
[root@master1 runc-demo]# runc create oci-cadvisor
ERRO[0000] runc create failed: cannot allocate tty if runc will detach without setting console socket
执行过程中会出现如下错误,将config.json里面的terminal字段修改微false,即可。
[root@master1 runc-demo]# runc create oci-cadvisor
[root@master1 runc-demo]# runc list
ID PID STATUS BUNDLE CREATED OWNER
oci-cadvisor 75690 created /opt/runc-demo 2023-06-19T03:28:34.390683035Z root
[root@master1 runc-demo]# ps -ef|grep cadvisor
root 78338 29627 0 11:29 pts/0 00:00:00 grep --color=auto cadvisor
从执行输出可以看到oci-cadvisor容器已经create成功,但cadvisor进程还未被拉起。等等,那这个pid(75690)是谁的进程ID?我们来看一下,其实这是runc的init进程。具体我们会在后续的文章里解释。
[root@master1 runc-demo]# ps -ef|grep 75690
root 75690 1 0 11:28 ? 00:00:00 runc init
root 85074 29627 0 11:33 pts/0 00:00:00 grep --color=auto 75690
[root@master1 runc-demo]# ps -ef|grep 75690|grep -v grep
root 75690 1 0 11:28 ? 00:00:00 runc init
(4) start
oci-cadvisor容器的容器进程已经被拉起,接下来需要做的就是把真正的业务进程拉起来。结合前边的生命周期管理图,所以可看我们现在需要执行start操作。
[root@master1 runc-demo]# runc start oci-cadvisor
[root@master1 ~]# runc list
ID PID STATUS BUNDLE CREATED OWNER
oci-cadvisor 75690 running /opt/runc-demo 2023-06-19T03:39:43.998415069Z root
[root@master1 ~]# ps -ef|grep cadvisor|grep -v grep
root 75690 1 0 11:39 ? 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@master1 ~]# runc exec -t oci-cadvisor /bin/sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /usr/bin/cadvisor -logtostderr
24 root 0:00 /bin/sh
30 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/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:24 errors:0 dropped:0 overruns:0 frame:0
TX packets:24 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1744 (1.7 KiB) TX bytes:1744 (1.7 KiB)
/ # netstat -ntlp|grep 8080
tcp 0 0 :::8080 :::* LISTEN 1/cadvisor
从容器内我们可以看到oci-cadvisor拥有的cadvisor进程,我们刚才exec的sh进程。同样该容器也拥有独立的网络栈。当然,这些只是容器的一部分特性。
(6)、kill
接下来,我们kill掉oci-cadvisor容器,使其进行stopped状态。
[root@master1 ~]# runc kill oci-cadvisor
[root@master1 ~]# runc list
ID PID STATUS BUNDLE CREATED OWNER
oci-cadvisor 0 stopped /opt/runc-demo 2023-06-19T03:39:43.998415069Z root
(7)、delete
最后,我们将oci-cadvisor从我们的环境里清理掉,删除运行时的数据(注意bundle仍在)。
[root@master1 ~]# runc delete oci-cadvisor
[root@master1 ~]# runc list
ID PID STATUS BUNDLE CREATED OWNER
[root@master1 runc-demo]# cd /opt/runc-demo/
[root@master1 runc-demo]# ls rootfs/
bin dev etc glibc-2.28-r0.apk glibc-bin-2.28-r0.apk home lib lib64 media mnt proc root run sbin srv sys tmp usr var
至此完成了runc对容器的整个生命周期管理过程展示。
现在看起来利用runc创建容器,并对其进行管理还是比较简单,解决了容器最核心、最底层、最基础的问题。而这离我们的实际需求还差的很远,想要成为云计算的基础设施还有很长的路要走。具体来说主要面临以下几个问题。
对大规模管理的支持较弱。runc只是个命令行工具,不是常驻进程,对于大规模的编排需求,无法通过网络调用实现。同样,也无法实现整个容器生命周期的自动化管理。
bundle的管理。OCI包含了OCF规范,但是像我们这样直接利用原生的bundle来构建容器运行时的环境依赖直观上来看有以下几个缺陷:
虽然总有不足的地方,但庆幸的是已经迈出了第一步。OCI(Open Container Initiative)组织一成立便得到了包括谷歌、微软、亚马逊、华为等一系列云计算厂商的支持。制定的容器运行时和镜像规范现已经成为一个可靠的基础标准。OCI通过开源的方式以runc落地,逐步脱离Docker的控制范围。在runc的基础上,允许和鼓励多样化的容器解决方案,这为广大的云厂商和我们这些开发者提供了更广阔的发挥空间,不断促进容器生态的持续创新,服务各行各业。
OCI(Open Container Initiative)是一个开放容器倡议组织,旨在推动容器运行时和镜像格式的开放标准化。OCI由多家技术公司和个人共同组成,包括Docker、CoreOS、Google、Red Hat等。该倡议组织的目标是创建一套通用的容器标准,以确保不同容器运行时和工具之间的互操作性和可移植性。
OCI的核心成果是一系列规范和标准,这些规范定义了容器运行时和镜像格式的规范化接口和结构。这些规范包括:
Runtime Specification(运行时规范):定义了容器的生命周期管理,包括创建、启动、停止和销毁容器等操作。最常见的容器运行时实现是OCI Runtime(也称为runc)。
Image Specification(镜像规范):定义了容器镜像的结构和格式,包括镜像的分层结构、元数据和配置等信息。常见的镜像格式是OCI Image(也称为OCI Image Format)。
这些规范和标准确保了不同容器运行时和工具的互操作性,使得用户可以在遵循OCI规范的前提下,使用不同的容器运行时和工具来创建、运行和管理容器。这也促进了容器生态系统的发展,提供了更大的选择和灵活性。
除了规范和标准之外,OCI还有一个包含开放源代码项目的工作组,用于实现和推进OCI规范的实际落地。这些项目包括runc(底层容器运行时实现)、image-spec(镜像规范实现)等。
因此,OCI不仅是一些文档,而是一个综合性的容器标准化倡议组织,通过定义规范和提供开源项目来推动容器生态系统的互操作性和可移植性。
主要参考:浅析容器运行时奥秘——OCI标准
手机扫一扫
移动阅读更方便
你可能感兴趣的文章