CRI与OCI

OCI

Open Container Initiative 开放容器协议

成立于 2015 年的 OCI 是Linux基金会旗下的合作项目,以开放治理的方式制定操作系统虚拟化(特别是Linux容器)的开放工业标准,主要包括容器镜像格式和容器运行时(runtime)。初始成员包括 Docker、亚马逊、谷歌和VMware等公司。OCI成立之初,Docker 公司为其捐赠了容器镜像格式和运行时的草案及相应的实现代码。原来属于Docker 的 libcontainer 项目被捐赠给OCI,成为独立的容器运行时项目 runC。

OCI 运行时规范定义了容器配置、运行时和生命周期的标准,主流的容器运行时都遵循OCI运行时的规范,从而提高系统的可移植性和互操作性,用户可根据需要进行选择。

首先,容器启动前需要在文件系统中按一定格式存放所需的文件。OCI运行时规范定义了容器文件系统包(filesystem bundle)的标准,在OCI运行时的实现中通常由高层运行时下载 OCI 镜像,并将OCI镜像解压成OCI运行时文件系统包,然后 OCI 运行时读取配置信息和启动容器里的进程。OCI运行时文件系统包主要包括以下两部分。

  • config.json:这是必需的配置文件,存放于文件系统包的根目录下。OCI运行时规范对Linux、Windows、Solaris和虚拟机4种平台的运行时做了相应的配置规范。
  • 容器的根文件系统:容器启动后进程所使用的根文件系统,由 config.json 中的root.path属性确定该文件系统的路径,通常是“rootfs/”。

然后,在定义文件系统包的基础上,OCI运行时规范制定了运行时和生命周期管理规范。生命周期定义了容器从创建到删除的全过程。


image-20201216150547929

Docker(图中的 dockerd)不符合 CRI 规范,因此当年 kubelet 内置了一个模块 dockershim,用来对 Docker 进行 CRI 接口的适配。经过几年的发展,CRI 的运行时已经很成熟了,用户在 Kubernetes 中可以直接使用 containerd或者 CRI-O ,无需再通过 dockershim – dockerd – containerd 绕一圈(图中红色箭头),既费时又费力的。

符合 CRI 标准的 containerd,以及底层的 runC,都是从Docker 项目中分拆出来的

Docker 最精华的部分并不是容器运行时。因为容器的运行时归根到底仅仅是 Linux 内核功能的调用而已,Docker 的容器运行时是可以被替代的。

Docker 镜像格式已是实际上的标准, OCI 的镜像规范是以 Docker 镜像格式为蓝本制定的,在大多数情况下两者是兼容的。开发者平时用到的“Docker”,除了可以运行容器之外,还有一个重要的功能就是构建容器镜像(例如 docker build),是上图中 dockerd 提供的主要功能之一。这部分面向开发者的功能在运行环境中确实用处不大,是 dockershim 被移除的原因之一。

因为镜像的格式已经标准化了,除了 Docker 以外,其他工具也可以构建镜像,如红帽的 Podman 等,但这些工具万变不离其宗,依然(必须)使用 Docker 开创的镜像格式标准。

Docker 公司有个著名的口号:“Build, Ship and Run”,翻译过来就是三个动词:“构建、传送和运行”,简练地描绘出了应用开发的精髓,其中隐含的意思是:构建镜像、传送镜像和运行镜像,一切皆以镜像为中心。OCI 组织对应有三个规范,分别与上述三个动词对应,即镜像规范(构建)、运行时规范(运行)和正在制定的分发规范(传送)。镜像是容器应用的关键技术,围绕镜像的一系列管理工作将是实际运维中的重要组成部分,这也是我们当初创建 Harbor 开源项目所希望解决的问题。

CRI

Container Runtime Interface 容器运行时接口

Linux提供了命名空间和控制组两大系统功能,它们是容器的基础。但是,要把进程运行在容器中,还需要有便捷的 SDK 或命令来调用 Linux 的系统功能,从而创建出容器。容器的运行时(runtime)就是容器进程运行和管理的工具。

容器运行时分为低层运行时和高层运行时,功能各有侧重。低层运行时主要负责运行容器,可在给定的容器文件系统上运行容器的进程;高层运行时则主要为容器准备必要的运行环境,如容器镜像下载和解压并转化为容器所需的文件系统、创建容器的网络等,然后调用低层运行时启动容器。主要的容器运行时的关系如下图所示。

image-20201216151502779

runC

runC 是 OCI 运行时规范的参考实现,也是最常用的容器运行时,被其他多个项目使用,如 containerd 和CRI-O等。runC也是低层容器运行时,开发人员可通过runC实现容器的生命周期管理,避免烦琐的操作系统调用。根据OCI运行时规范,runC不包括容器镜像的管理功能,它假定容器的文件包已经从镜像里解压出来并存放于文件系统中。runC创建的容器需要手动配置网络才能与其他容器或者网络节点连通,为此可在容器启动之前通过OCI定义的事件钩子来设置网络。

由于runC提供的功能比较单一,复杂的环境需要更高层的容器运行时来生成,所以runC常常成为其他高层容器运行时的底层实现基础。

containerd

在OCI成立时,Docker公司把其Docker项目拆分为runC的低层运行时及高层运行时功能。2017年,Docker公司把这部分高层容器运行时的功能集中到containerd项目里,捐赠给云原生计算基金会。

containerd 已经成为多个项目共同使用的高层容器运行时,提供了容器镜像的下载和解压等镜像管理功能,在运行容器时,containerd先把镜像解压成OCI的文件系统包,然后调用runC运行容器。containerd提供了API,其他应用程序可以通过API与containerd交互。“ctr”是containerd的命令行工具,和“docker”命令很相像。但作为容器运行时,containerd只注重在容器运行等方面,因而不包含开发者使用的镜像构建和镜像上传镜像仓库等功能。

Docker

Docker引擎是最早流行也是最广泛使用的容器运行时之一,是一个容器管理工具,架构如下图所示。Docker的客户端(命令行CLI工具)通过API调用容器引擎Docker Daemon(dockerd)的功能,完成各种容器管理任务。

image-20201216152714460

Docker 引擎在发布时是一个单体应用,所有功能都集中在一个可执行文件里,后来按功能分拆成 runC 和 containerd 两个不同层次的运行时,分别捐献给了OCI和CNCF。上面两节已经分别介绍了runC和containerd的主要特点,剩下的dockerd就是Docker公司维护的容器运行时。

dockerd同时提供了面向开发者和面向运维人员的功能。其中,面向开发者的命令主要提供镜像管理功能。容器镜像一般可由Dockerfile构建(build)而来。Dockerfile是一个文本文件,通过一组命令关键字定义了容器镜像所包含的基础镜像(base image)、所需的软件包及有关应用程序。在Dockerfile编写完成以后,就可以用“docker build”命令构建镜像了。下面是一个Dockerfile的简单例子:

1
2
3
FROM ubuntu:18.04
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

容器的镜像在构建之后被存放在本地镜像库里,当需要与其他节点共享镜像时,可上传镜像到镜像仓库(Registry)以供其他节点下载。

Docker还提供了容器存储和网络映射到宿主机的功能,大部分由containerd实现。应用的数据可以被保存在容器的私有文件系统里面,这部分数据会随着容器一起被删除。对需要数据持久化的有状态应用来说,可用数据卷Volume的方式导入宿主机上的文件目录到容器中,对该目录的所有写操作都将被保存到宿主机的文件系统中。Docker可以把容器内的网络映射到宿主机的网络上,并且可以连接外部网络。

CRI和CRI-O

Kubernetes是当今主流的容器编排平台,为了适应不同场景的需求,Kubernetes需要有使用不同容器运行时的能力。为此,Kubernetes从1.5版本开始,在kubelet中增加了一个容器运行时接口CRI(Container Runtime Interface),需要接入Kubernetes的容器运行时必须实现CRI接口。由于kubelet的任务是管理本节点的工作负载,需要有镜像管理和运行容器的能力,因此只有高层容器运行时才适合接入CRI。CRI和容器运行时的关系如下图所示。

image-20201216152817791

CRI和容器运行时之间需要有个接口层,通常称之为shim(垫片),用以匹配相应的容器运行时。

由于 Docker运行时被普遍使用,它的CRI shim被称为dockershim,内置在Kubernetes 的 kubelet 中,由 Kubernetes 项目组开发和维护。其他运行时则需要提供外置的shim。containerd 从1.1版本开始内置了CRI plugin,不再需要外置shim来转发请求,因此效率更高。在安装 Docker 的最新版本时,会自动安装 containerd,所以在一些系统中,Docker 和 Kubernetes 可以同时使用 containerd 来运行容器,但是二者的镜像用了命名空间隔离,彼此是独立的,即镜像不可以共用。因为Docker 和 containerd常常同时存在,因此在不需要使用Docker的系统中只安装 containerd 即可。

containerd最早是为 Docker 设计的代码,包含一些用户相关的功能。相比之下,CRI-O是替代Docker或者containerd的高效且轻量级的容器运行时方案,是CRI的一个实现,能够运行符合OCI规范的容器,所以被称为CRI-O。CRI-O是原生为生产系统运行容器设计的,有个简单的命令行工具供测试用,但并不能进行容器管理。CRI-O支持OCI的容器镜像格式,可以从容器镜像仓库中下载镜像。CRI-O支持runC和Kata Containers这两种低层容器运行时。