姚洪 译 分布式实验室
容器运行时(container runtime) 是一个既熟悉又陌生的话题。在过去的一年里,随着Kubernetes的进一步发展,以及CNCF和OCI 在标准化方向的努力,市面上可供选择的容器运行时也不再只是Docker一家了。容器运行时是什么? 市面上有哪些可选的运行时?各有什么优缺点? 在这篇文章里,你都会得到答案。
我们在之前关于KubeCon + CloudNativeCon概述的文章中曾经简要提到 现在市面上已经有多个容器“运行时”(container runtime)。容器运行时的定义是:能够基于在线获取的镜像来创建和运行容器的程序。 容器运行时领域的标准和实现正在慢慢地走向成熟: Docker在KubeCon上发布了containerd 1.0, 几个月前CRI-O 1.0也发布了, 同时还有rkt也是一个运行时。 如果你正打算从0开始设计部署一个自己的容器系统或者Kubernetes集群的话, 面对这么多的容器运行时,你或许会感到有点迷茫。这篇文章我就会尝试解释什么是容器运行时,它们是做什么的,比较它们的特点,然后告诉你该如何选择适合自己的。 同时本文也会介绍容器规范和标准化方面的基础知识。
容器是什么?
在我们探索容器运行时之前,让我们先来看看容器到底是什么。当一个容器被启动时,主要会发生如下活动:
基于镜像(image),一个容器会被创建出来。镜像image就是附加一个JSON配置文件的tar包。镜像常常是嵌套的:例如Libresonic的镜像是基于Tomcat的镜像, 而Tomcat的镜像又最终基于一个Debian的镜像。这样可以防止重复的内容占用空间。此例子中Debian镜像(或者其他的中间镜像)是上层其他镜像的基础。一个容器的镜像常常可以通过使用类似docker build的命令来生成。
如果必要, 容器运行时会从某处下载镜像,这个地方称为registry。Registry通常是一个通过HTTP协议暴露镜像的元数据和文件以供下载的容器仓库。 之前就只有Docker Hub独一个registry, 但是现在基本上每个玩家都有自己的仓库了: 比如, Red Hat 有一个仓库服务于OpenShift项目, 微软的Azure也有一个仓库, Gitlab也有一个仓库服务于它的持续集成平台。 Registry仓库就是docker pull/push所交互的服务器。
运行时将层次化的镜像解压到支持Copy on Write(CoW)的文件系统里。通常这通过覆盖(overlay)文件系统来实现,所有的层次层层覆盖来构成一个合并的文件系统。 这个步骤通常不能通过命令行直接访问,而是当运行时创建容器时会自动在后台发生。
-
最后,运行时来实际地执行容器。 它告诉内核给容器分配合适的资源限制,创建隔离的层(为进程、网络、文件系统等),使用各种机制的混合(包括cgroups、namespaces、capabilities、seccomp、AppArmor、SELinux等等)。 比如Docker, 当执行docker run时,它会创建并运行容器,但是底层它实际调用的是runC命令。
镜像规范(常被称为“OCI 1.0 images")定义了容器镜像的内容
运行时规范( 常被称为“CRI 1.0”或者容器运行时接口)表述了容器的”配置,运行环境和生命周期“。
-
容器网络接口(CNI)描述了如何在容器内部配置网络接口,不过它是在CNCF下标准化的, 而不是OCI。
这些标准在不同项目里面的实现是不同的。比如Docker除了镜像格式之外的东西基本都是兼容的。 Docker早在标准化之前就有了自己的镜像格式,而且它也承诺未来很快会转换到新标准上。容器的运行时接口的实现也是不同的,因为Docker里面并不是所有的东西都是标准化的,我们接下来会说明。
Docker和rkt的故事
因为Docker是第一个流行的容器,我们就从它开始说起。最先,Docker使用的是LXC但是层次隔离不太完整,所以后来Docker开发了libcontainer,最后演变为了runC。接下来容器迎来了爆发,而Docker也成为容器部署的事实标准。2014年Kubernetes诞生了,它很自然地使用了Docker,因为Docker是当时唯一的容器运行时。但是,Docker是一家很有野心的公司,一直在独立开发新的功能和工具。比如Docker Compose,与Kubernetes 同时发布了1.0,而这两个项目有很多的重复的东西。尽管我们使用Kcompose这样的工具可以让它们两个互相交互,但是Docker给大家的感觉是:这个项目太大,做了太多的事情。 这导致CoreOS发布了一个简单,独立的运行时rkt。 当时rkt这样描述的:Docker现在正在构建工具做了很多事情,比如启动云服务器,建集群系统,还有一堆的功能:构建镜像,运行镜像,上传下载,甚至做覆盖网络,最后都编译进一个大的运行时里面,当作你服务器里面的root程序。标准化容器宣言早就被它删除了。我们应该停止谈论Docker容器,开始谈论Docker平台,因为它已经不再是那个我们曾经希望的,用起来简单的构建容器的基础组件了。rkt的一个创新是通过appc规范来标准化容器格式,这我们在2015年曾经提到过[2]。CoreOS一直都没有一个容器运行时接口的完整的标准的实现。到现在为止,rkt的Kubernetes兼容层(rktlet)并没通过Kubernetes的所有集成测试,仍然还在开发中。CoreOS CTO Brandon Philips在一封邮件里面这么说:rkt初始是支持OCI image规范的, 但是有些地方不全。在当前支持OCI还不是那么重要,因为在容器仓库方面对OCI的支持才刚开始,而且Kubernetes里面这部分也是没有的。在rkt里面,OCI标准的运行时规范没有使用,消费,也没有被处理。这是因为rkt是基于pod语义的,而容器运行时规范针对的是单个容器的执行。尽管如此,在Red Hat容器团队的主管Dan Walsh在一封邮件访谈中说道:在Kubernetes生态系统中的容器标准化部分,CoreOS功不可没: “没有CoreOS我们可能就没有CNI, CRI可能还在OCI里面艰难抗争, CoreOS的成就在市场上没有得到应有的认可”。 确实如此, Philips也说 “CNI项目和很多规范最开始都来自于rkt,最后我们将它捐给了CNCF。 CNI现在仍然在rkt中广泛使用, 无论是内部还是用户配置里面”。 当前,CoreOS已经把重心转向了构建自己的Kubernetes平台(Tectonic)和镜像发布服务(Quay),而不是在运行时这个层面竞争。
CRI-O 最小的运行时
见到这些新标准以后,Red Hat的一些人开始想他们可以构建一个更简单的运行时,而且这个运行时仅仅为Kubernetes所用。这样就有了“skunkworks”项目,最后定名为CRI-O, 它实现了一个最小的CRI接口。在2017 Kubecon Austin的一个演讲[3]中, Walsh解释说,“CRI-O被设计为比其他的方案都要小,遵从Unix只做一件事并把它做好的设计哲学,实现组件重用。”
根据Red Hat的CRI-O开发者Mrunal Patel在研究里面说的, 最开始Red Hat在2016年底为它的OpenShift平台启动了这个项目,同时项目也得到了Intel和SUSE的支持。CRI-O与CRI规范兼容,并且与OCI和Docker镜像的格式也兼容。它也支持校验镜像的GPG签名。 它使用CNI的包来做网络部分,支持CNI插件,OpenShift也用它来做软件定义存储层。 它支持多个CoW文件系统,比如常见的overlay,aufs,也支持不太常见的Btrfs。
但是CRI-O最出名的特点是它支持“受信容器”和“非受信容器”的混合工作负载。比如,CRI-O可以使用Clear Containers做强隔离,这样在多租户配置或者运行非信任代码时很有用。这个功能如何集成进Kubernetes现在还不太清楚,Kubernetes现在认为所有的后端都是一样的。
CRI-O有一个有趣的架构(见下图,摘录于slides[4]),它重用了很多基础组件,比如runC来启动容器,使用containers/image和containers/storage 软件库来拉取容器镜像,创建容器的文件系统(这2个库是为skopeo项目创建的)。还有一个名为oci-runtime-tool的库来准备容器配置。CRI-O引入了一个新的deamon来处理容器,名字为conmon。 根据Patel所说,conmon程序是“纯C编写的,用来提高稳定性和性能”,conmon负责监控,日志,TTY分配,以及类似out-of-memory情况的杂事。
conmon需要去做所有systemd不做或者不想做的事情。但是即使CRI-O不直接使用systemd来管理容器,它也将容器分配到sytemd兼容的cgroup中,这样常规的systemd工具比如systemctl就可以看见容器资源使用情况了。因为conmon(不是CRI daemon)是容器的父进程,它允许CRI-O的部分组件重启而不会影响容器,这样可以保证更加平滑的升级。现在Docker部署的问题就是Docker升级需要重起所有的容器。 通常这对于Kubernetes集群来说不是问题,但因为它可以将容器迁移来滚动升级。
CRI-O是OCI标准的实现里面第一个通过所有Kubernetes集成测试的(抛开Docker本身)。Patel通过一个CRI-O支撑的Kubernetes集群展现了这些功能。Dan Walsh在一篇博客[5]里面解释了CRI-O与Kubernetes交互的方式:与其他容器运行时不同,我们的第一目标是永远不弄坏Kubernetes。为Kubernetes提供稳定可靠的容器运行时是CRI-O的唯一任务。根据Patel所说,CRI-O现在性能与一个常规的Docker部署对比已经不相上下,但是开发团队正在进一步优化性能实现超越。CRI-O 提供Debian和RPM的包,并且类似minikube、kubeadm这样的部署工具也都支持切换到CRI-O。在现有的集群上,切换运行时相当的直接明了:只需要一个环境变量来改运行时的socket(Kubernetes用它来与运行时沟通)。
CRI-O 1.0在2017年10月发布,支持Kubernetes 1.7. 后来CRI-O 1.8、1.9相继发布,支持Kubernetes的1.8, 1.9(此时版本命名规则改为与Kubernetes一致)。Patel考虑在2017年11月CRI-O生产可用,在Openshift 3.7中作为beta版提供。在Openshift 3.9中让它进步一步稳定,在3.10中成为缺省的运行时,同时让Docker作为候选的。下一步的工作包括集成新的Kata Containers的这个基于VM的运行时,增加kube-spawn的支持,支持更多类似NFS, GlusterFS的存储后端等。 团队也在讨论如何通过支持casync或者libtorrent来优化多节点间的镜像同步。
containerd:Docker带API的运行时
当Redhat在做OCI的实现时,Docker也在朝标准努力,他们创建了另一个运行时,containerd。 这个新的Daemon是对Docker内部组件的一个重构以便支持OCI规范,比如执行,存储,网络接口管理部分等。它在Docker1.12中就是一个特性了,但是直到containerd 1.0发布时才完成, 并会成为Docker 17.12版本的一部分(Docker已经采用年+月的方式做版本号)。虽然我们称containerd为运行时,但是它不是直接实现CRI接口,而且是由一个叫cri-containerd的独立daemon来实现。所以containerd需要的daemon比CRI-O要多(5个, CRI-O 3个)。在写此文的时候,cri-containerd还是beta版本,但是containerd已经在众多的生产环境中以Docker的形式被使用了。
在KubeCon的Node SIG会议上, Stephen Day 如此表述containerd: 它被设计为一组松耦合组件的紧核心。 与CRI-O不同,containerd可以通过一个Go API支持Kubernetes生态系统以外的工作负载。不过这个API现在还没稳定,但是containerd已经定义了一个清晰的发布流程[6]来更新API和命令行工具。与CRI-O类似的是,containerd本身功能齐全,并通过了Kubernetes的所有测试,但是它无法与systemd的cgroup互操作。
项目的下一步是开发更多测试,在内存使用以及延迟上提高性能,他们也在努力提高稳定性。他们准备提供Debian和RPM的包以方便安装, 并与minikub和kops集成等。 他们也计划与Kata Containers更平滑的集成;runC已经能在基础集成方面被Kata替换,但是cri-containerd的集成还没完全实现。
互操作性与默认项
Kubernetes会切换到哪个运行时(如果它确定要改),这个问题也是开放的。这也直接导致了运行时之间的竞争。在KubeCon上这个问题甚至带来了一些争议,因为在CNCF的keynote里面CRI-O都没被提到。Vicent Batts, Red Hat的一名高级工程师因此在Twitter上发表不满:简直有毛病,KubeCon的keynote上提到了containerd和rktlet这些CRI实现,但是根本只字未提CRI-O,而CRI-O是Kubernetes项目中的,已经1.0并已经在生产上使用了。当我咨询他相关细节时,他解释到:健康的竞争是好的,问题在于不健康的竞争。CNCF应该更好管理好自家的项目而不是迫于压力吹捧某些项目比其他的好。Batts补充道, Red Hat可能正处于一个临界点,一些应用可能开始以容器部署而不是RPM,安全担忧(也就是安全补丁的跟踪,在容器格式的包中是缺乏的)是这种转换的一个阻碍。 通过Atomic项目,Red Hat看来正转向容器为核心,但是对于Linux发行版却有大的风险。
当我在KubeCon上问 CNCF的COO Chris Aniszczyk时,他解释说CNCF现在的政策时优先营销*项目:类似于CRI-O,Helm的项目一定程度上是Kubernetes的一部分,因此我们可以说它们也是CNCF的一部分。我们只是没有像我们的*项目那样重点营销而已,因为*项目都已经通过了CNCF TOC的标准。他补充到, “我们希望提供帮助,我们听到了反馈,也计划在2018着手解决”, 同时他建议的一个解决方法是CRI-O申请毕业[7]成为CNCF的一个*项目。
在一个container运行时的发布会上,Philips解释到,社区会在取得共识后作出决定哪个运行时会成为Kubernetes的缺省项。他把运行时比作浏览器,说可以把容器的OCI标准比做HTML5或者javascript标准:这些标准通过不同的实现得到进化发展。 他重申这种竞争是健康的,表明有更多的创新。
现在写这些新的运行时的很多人当初都是Docker的贡献者:Patel是OCI实现的最初维护者,后来这个实现成为了runC;Philips在启动rkt项目前也是Docker的核心开发者。这些人与Docker开发者积极合作,标准化接口,都希望看到Kubernetes稳定和提高。如Patrick Chazenon, Docker Inc所说,“目标是为了让容器运行时成熟稳定,让人们厌烦在它上面做创新。“ 发布会上开发者都为他们的成就感到高兴和自豪:他们成功的创建了一个容器的互操作性规范,而且规范还在成长。
2018:融合与标准化仍将持续
现在容器标准化领域的火热话题已经从运行时转向了镜像发布(例如,容器仓库),很可能在围绕Docker的发布系统产生出一个标准。也有一些工作正跟踪Linux内核的更新,比如cgroups v2。
实际情况是,每个运行时有它自己的优点:containerd有一个API,所以它可以被用来构建自己的自定义平台;CRI-O只是一个仅针对Kubernetes的简单运行时。Docker和rkt在另一个层面上,提供的东西比运行时要多: 比如他们也提供构建容器的方式,发布推送到仓库的方式等。
现在大多数的共有云基础架构仍然使用Docker做运行时。实际上甚至CoreOS也用Docker而不是rkt在自家的Tectonic平台上。根据Philips所说,这是因为“我们的客户依赖docker engine为核心的持续集成系统。相比其他的Kubernetes产品,它是被测试的最充分的。 如果其他的容器运行时提供给Kubernetes更大的提高的话,Tectnoic可能会考虑用它。在此刻containerd和CRI-O都还是非常年轻的项目,尤其要考虑到每个项目在今年都引入了大量的新代码。接下来,它们需要通过与生态系统中的第三方集成测试达到成熟:比如日志、监控、安全等等。Philips在博客中深入解释了CoreOS的位置:目前,CRI对于Kubernetes的主要优点是更好的代码组织和kubelet里面更多的代码覆盖率,这样给代码更高质量,也比以前更加深入彻底的测试。尽管如此,对于几乎所有的部署来说,我们期望Kubernetes社区在短期内仍然使用Docker Engine。在发布会的讨论上,Patel也说了类似的话,"我们不需要Kubernetes用户知道什么是运行时"。说来也是,只要它工作正常,用户根本不用关心。 而且OpenShift,Tectonic 等平台都把运行时的决策抽象化了,它们都会自动选他们自己的最佳缺省项来满足用户需求。所以Kubernetes到底选用哪个运行时作为缺省的这个问题对于开发者来说根本不用操心,只要他们在一个共识的标准里工作就行。在充满冲突的世界里面,看到这些开发者一起开诚布公的工作本身就是难得的了。
相关链接:
https://github.com/moby/moby/commit/0db56e6c519b19ec16c6fbd12e3cee7dfa6018c5
https://lwn.net/Articles/631630/
https://kccncna17.sched.com/event/CU6T/cri-o-all-the-runtime-kubernetes-needs-and-nothing-more-mrunal-patel-red-hat
https://schd.ws/hosted_files/kccncna17/e8/CRI-O-Kubecon-2017.pdf
https://medium.com/cri-o/cri-o-support-for-kubernetes-4934830eb98e
https://github.com/containerd/containerd/blob/master/RELEASES.md
https://www.cncf.io/projects/graduation-criteria/
原文链接:https://lwn.net/Articles/741897/