容器运行时的定义、分类、历史与简介
本文70%左右内容来自对英文文章 “https://www.capitalone.com/tech/cloud/container-runtime/” 的翻译。笔者首先看到文章 “https://www.zhangjiee.com/blog/2021/container-runtime.html” ,发现此文是对上述文章的完全翻译,因为笔者个人觉得此中文文章一些地方翻译得不够准确与不够可读,所以笔者个人尝试对上述英文文章进行了再次翻译,同时在其中某些段落插入了一些自己编辑的图文,以更好地理解相关概念,在此做下记录以备后续回忆与学习。
1 容器运行时的定义
容器本质上是由几个底层的内核原语相关组件捆绑在一起组成,每次创建隔离进程时,都不需要手动隔离、自定义命名空间等,把这些组件捆绑在一起,我们称之为容器。容器运行时就是一个工具,这个工具将这些组件以一起有序高效地方式组合起来,从而构建、管理与销毁一个隔离的、安全的执行环境(即容器),让用户不再需要手动隔离、自定义命名空间等,同时能够以一种简洁方便的方式去对容器的整个周期进行管理。
随着技术的发展和迭代,它们的运行时也在随着变化。
2 容器运行时的早期历史
随着技术的发展和迭代,它们的运行时也在随着变化。
自从2007年cgroup被加入Linux内核后,讯速出现了几个利用包括cgroup命名空间在内的7个Linux命名空间的项目,这些项目都是的目的都是为了创建容器化进程。这些项目主要如下几个:
- LXC(https://linuxcontainers.org/lxc/)
- LMCTFY(https://github.com/google/lmctfy)
- systemd-nspawn(https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html)
- rkt(https://github.com/rkt/rkt)
LXC即Linux Containers
LXC是在crgoup被引入Linux内核后立即被创建的项目,它被设计用于"full-system" 容器,是用来创建Linux容器的,此时的Linux容器跟我们现在常用的Linux上Docker容器或containerd容器是有区别的,最显著的区别在于它们使用不是容器运行时不同。同时Systemd也获得了类似的容器支持,systemd-nspawn可以运行命名空间进程,systemd本身可以控制cgroup。尽管lxc与systemd-nspawn也在其他项目中被利用到了,但它们还是没有能够吸引足够多的终端用户。直到现在也是一个不流行的开源容器化项目。
dotCloud即后来的Docker)
dotCloud(它就是现在的Docker公司的前身)开始围绕LXC开发各种工具以使容器对开发人员与用户体验更友好。但不久后,dotCloud抛弃了LXC,创建了 开放容器倡议(OCI, https://github.com/opencontainers/runtime-spec )来建立业内容器标准,并将一些容器组件开源为 libcontainer(地址: https://github.com/docker-archive/libcontainer ) 项目。
LMCTFY
Google开源也其内部容器技术栈的开源版本:LMCTFY( https://github.com/google/lmctfy )。但随着dotCloud(后来改名为Docker)变得流行起来,LMCTFY 项目最终被抛弃,LMCTFY 项目中的绝大部分功能也被其开发人员复制融入到了Docker的开源项目libcontainer 中。
rkt与appc
CoreOS起初在其Containerd Linux产品中只使用Docker技术栈,一段时间后也发行了一个名为rkt 项目( https://github.com/rkt/rkt )作为Docker的替代品。在当时,rkt提前具备了区别于Docker与其他 早期容器运行时的特性。它不需要以root用户运行、没有守护进程、是CLI驱动的,同时它也具有一些独到特性如加密验证和完全的 Docker 镜像兼容。 令人惋惜地是,在Docker 创建 开放容器倡议(OCI, https://github.com/opencontainers/runtime-spec )之前,CoreOS发布了一个叫做 appc 的容器标准。但抗不住后起这秀 OCI 在全球范围内的流行,CoreOS最终放弃了appc 转而支持 OCI,并将appc的部分特性功能也提交给 OCI 。至此,rkt与appc最终都被抛弃。
直至Kubernetes1.24之前,OCI 成为并一直是业界最流行的容器标准。
3 容器运行时的汇总
当前容器运行时有多种类型,但它们可以被分两大类:OCI 运行时和 CRI(容器运行接口)。

4 开放容器规范(OCI)
有时称为低级别运行时,实现 OCI 运行时规范专注于管理容器的生命周期,并且无需其它操作。 低级别运行时创建和运行容器。
随着技术的发展和迭代,它们的运行时也在随着变化。
本地运行时
本地容器运行时在同一个主机内核上运行容器化进程。早期出现的有较大影响力的本地容器运行时有如下,它们当中的其中某些仍然在被广泛使用。
runC是Docker在开源项目libcontainer与开放容器规范OCI所有工作的结晶。它是事实上的标准低级运行时,由Go语言实现并由Docker下的开源项目moby维护。
Railcar是一个由Oracle创建的OCI运行时实现。它是用Rust语言实现的,对于与内核执行低级的交互操作的容器运行时组件来说,与runC的Go语言代码相比之下,Rust是一门更加优秀的语言。但不幸地是Railcar 已经被抛弃了。
crun是一个由Redhat引领的OCI运行时实现。它是containers project( https://github.com/containers ) 的一部分,是libpod的姊妹项目。是由C语言实现的,具有性能好、轻量级等特点,是第一批支持 cgroups v2( https://medium.com/nttlabs/cgroup-v2-596d035be4d7 ) 的容器运行时之一。
rkt不是一个OCI运行时实现,但是它是一个类低级容器运行时。除了支持appc软件集外,它还支持运行Docker与OCI镜像,但它不能与使用OCI运行时的更高级组件进行交互。
从现有容器运行时来看,低级容器运行时只有在容器创建与删除阶段才显得尤为重要,一旦相关进程处于运行状态,低级容器运行时就不再被重要关注了即将容器处于运行状态中时低级容器运行时显得不那么重要了。
沙箱和虚拟化运行时
对于开放容器规范OCI,它不仅有本地运行时实现方案,还有虚拟化方向实现方案,早期出现的虚拟化实现方案有如下,其中某些当前仍被使用中。
gVisor( https://gvisor.dev/ ) 与Nabla( https://nabla-containers.github.io/ )都是沙箱运行时实现方案,它们在宿主机与容器进程间提供了更进一步的隔离。不像本地容器运行时那样与宿主机共享内核,此时容器化进程运行在“unikernel or kernel proxy”层,容器化进程与这个层进行交互通信,然后再由这个层代表容器与宿主机内核进行交互。因为增加了隔离,这两个沙箱运行时降低了攻击面,使得容器化进程对宿主机实施破坏或不得影响的功能性更低了。
runV( https://github.com/hyperhq/runv ),Clear ( https://github.com/clearcontainers )与Kata( https://github.com/kata-containers )都是虚拟化运行时。它们都是OCI运行时实现方案,底层由虚拟机接口而不是宿主机内核提供支持。前两者当前已经被废弃且它们特性已经被Kata所吸收。它(们)能够使用标准的OCI容器镜像,但在使用过程中在容器与宿主机之间提供了强有力的隔离,它(们)使用一个标准的Linux内核镜像来启动一个轻量级虚拟机,然后在这个虚拟机中运行容器化进程。
与本地化运行时相比,沙箱运行时与虚拟化运行时在容器的整个生命周期中增加了更多性能上的负面影响。在沙箱运行时容器中,存在一个额外的抽象层即容器进程运行在沙箱unikernel/proxy 层,由这个层来传递容器指令给宿主机内核。 而在虚拟化运行时容器中,存在一个额外的虚拟化层即整个容器进程都是运行在虚拟机上的,由虚拟机再与宿主机内核进行交互,这比本地运行时直接与内核进行交互更慢;幸运地是,专注于虚拟化运行时下容器性能的 AWS Firecracker ( https://github.com/kata-containers )在这方面做出了努力,提供的这种底层虚拟机能够尽可能地将性能影响降到最小。
以下是对本地运行时沙箱、虚拟化运行时代表性实现方案的对比。

5 容器运行时接口(CRI)
当Kubernetes(2014年9月第一个正式版本v0.2,2015年7月v1.0版本正式发布并可在生产环境中使用)这一容器编排工具出现时,Docker已经在业界大为流行,是业界标准的高级容器运行时,所以当时Kubernetes当时只能迁就Docker,将Docker这一高级运行时硬编码到Kubernetes的守护进程kubelet中。之后,随着Kubernetes在业界的大火,社区与企业办都产生了创建一个替代Docker这一高级运行时的要求。
关于Kubernetes弃用docker的原因,内容较多,笔者打算另做学习总结然后做记录。
rkt是一个尝试这一要求的支持者,通过定制化kubelet的代码产生了rktlet。但是,这种按运行时定制的构建过程无法扩展,此时也正好暴露出了Kubernetes中对抽象运行时模型的需求。为了解决这个痛点,Hyper、CoreOS、Google和Kubernetes的其他赞助商一起协商合作,站在容器编排角度推出了一个容器运行时的一个高级规范即高级容器运行规范,这个规范就是容器运行时接口(英文原名是Container Runtime Interface,简称CRI, https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes/ )。通过与CRI集成而不是与特定的低级运行时耦合,kubelet不需要针对每种低级运行时进行硬编码与重编译,就可以灵活地支持多个容器运行时。这不仅符合软件设计的解耦设计规范,也为满足了业界中想要Kubernetes 灵活地支持多个容器运行时的要求。
CRI同时也对OCI运行时的镜像管理与分发,也对容器存储、快照、网络等其他容器与镜像生命周期中用到的事物也给予了应有关注。不像OCI运行时主要紧密关注于在宿主上创建与运行容器,CRI具备在动态云环境中充分利用容器所需的功能。更有甚者,CRI通常也是委托一个低级的OCI运行时作为其实际上的底层运行时。通过引入CRI,Kubernetes以一种高度可扩展的方式高效地达到将kubelet与底层容器运行时进行解耦这一目的。
CRI的第一个实现是dockershim,它在Docker容器引擎的上面提供了一个抽象层(这也是Kubernetes在Docker在业界仍然占据主导地位时的一个折中选择)。随着containerd与runc从Docker技术栈的核心中被分离出来,dockershim实际的地位终于被撼动,containerd当前提供了CRI的完整实现并逐渐成为(或说正在成为)主流。

也有一个虚拟化方向的CRI实现,它就是frakti(v1),它也是第一个非docker式的CRI实现方案(参考:Unified Kubernetes CRI runtimes based on Kata Containers, https://object-storage-ca-ymq-1.vexxhost.net/swift/v1/6e4619c416ff4bd19e1c087f27a43eea/www-assets-prod/presentation-media/hyper-kata-frakti-cri2.pdf )。它本来是为runV而开发的,同时也被设计与runV协同工作的。虽然是虚拟化方向的,但它提供的功能跟基于本地OCI底层支持的CRI实现是一样的。因为runV被废弃了同时Clear Containers 与runV的特性也被 kata 所吸收,现在frakti已经不再流行。“frakti加runV组合”也被当前的“containerd加kata组合”被取代,换句话说“containerd加kata组合”就是当前“frakti加runV组合”方案实现。
当前,CRI领域内有两个主要活跃项目,它们分别是containerd与cri-o。以下是对它们的简单介绍。
containerd
containerd是从Docker中孕育出来的高级运行时,是开源项目Moby在管理与研发出来的。默认情况下,containerd也是使用runc作为其低级运行时。跟其他源自Docker的容器工具一样,containerd当前是事实上的标准高级运行时。它实现与提供了CRI的全部核心功能,且增加了一些额外功能。containerd中有一个插件设计即 cri-containerd ( https://github.com/containerd/cri ),它实现了CRI,并且存在多种垫片设计与实现以将containerd与各种低级运行时如kata、runc等进行集成。
上面提到的这些CRI实现能够与所有的OCI运行时(不仅包括本地运行时,还包含沙箱运行时与虚拟化运行时)进行交互,交互的方式可能是本地交互也可能是通过在中间插入一个插件或垫片。
cri-o
cri-o是一个由Redhat领导的精简CRI实现方案,专为Kubernetes而设计。它意在充当为CRI与各种底层低级OCI运行时之间轻量级桥梁。与containd相比,它的外围功能更少,并委托来自libpod和“Container Tools”项目的组件进行镜像管理和存储。默认情况下,cri-o也是使用runc作为其底层低级运行时,但在RedHat Fedora installations (with cgroups v2) 项目中,它使用了crun这一低级运行时。因为cri-o能够对OCI实现完全兼容, cri-o 与低级别的运行时比如 kata开箱即用,不需要其它的组件,只需要很少的配置即可。
6 容器引擎
docker容器引擎
Docker并不是一个纯粹的CRI实现或OCI实现,但是它包含了CRI的一个实现即containerd,也包含一个OCI的实现。事实上,它具有 CRI 或者 OCI 范围之外的其他功能,比如镜像构建与签名。那么到底该如何定义Docker?根据我对此文与其他技术文档的学习,最后可以得出如下大致总结:
第一,Docker现在全称应该叫Docker 容器引擎,并且通常将这些完整的容器工具套件称之为容器引擎。而且一般来说,我们在Linux服务器(比如Ubuntu20.04 LTS)上通过执行“apt-get install docker-ce docker-ce-cli containerd.io”命令安装所谓Docker其实就是安装整个Docker容器引擎套件。除了Docker,没有其他单一可执行文件能提供这样一个完整功能,如果一定需要找到一个替换解决方案的话,我们可以从container tools项目中拼凑出一个类似的工具套件。Container Tools 项目遵循了 UNIX 小型工具哲学,每个工具只做好一件事情:
- podman - 运行镜像
- buildah - 构建镜像
- skopeo - 分发镜像
- 其他的
第二,Docker是一个大而完备的高级运行时,其用户端核心叫做Docker Engine( https://docs.docker.com/engine/ ),由 3 部分构成:Docker Server (docker daemon, 简称 dockerd)、REST API 和Docker cli。借助 Docker Engine 既能便捷地运行容器进程进行集成开发、也能快速构建分发镜像。下图来自文章 “https://www.zeng.dev/post/2020-container-runtimes/” 。

containerd容器引擎
containerd其实就是Docker从自身中剥离出来并捐赠给CNCF。从Docker中剥离出来后,containerd现在也是一个独立的容器引擎,下图(来自containerd官网)展示了整个容器技术架构及containerd在其中的位置。

关于containerd还不够了解,更多内容有待进一步探索与补充。
8 备注与参考文章
订阅号文章不方便更新,如后续有修改或完善,将更新到个人博客,博客地址参考文首。技术操作类文章推荐直接访问个人博客查阅,阅读效果更佳。
某些观点或阐述,笔者水平有限无法给出正式严谨的答案,唯有引用或参考企业单位的官网与其他前辈的描述。本文某些内容来自以下文章:
- https://www.capitalone.com/tech/cloud/container-runtime/
- https://www.zhangjiee.com/blog/2021/container-runtime.html#org4b14f92
- https://www.zeng.dev/post/2020-container-runtimes/
- https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/comparison-of-docker-containerd-and-sandboxed-container
- https://mp.weixin.qq.com/s/qEKyEseD370xWI-2yIyUzg
- https://containerd.io/
- https://www.cnblogs.com/ricklz/p/17032914.html
- https://blog.csdn.net/projim_tao/article/details/129534626