解密容器运行时
技术
作者:Docker
译者:姚洪
2018-01-25 06:17

容器运行时( container runtime ) 是一个既熟悉又陌生的话题。在过去的一年里,随着 Kubernetes 的进一步发展,以及 CNCF 和 OCI  在标准化方向的努力,市面上可供选择的容器运行时也不再只是Docker 一家了。容器运行时是什么? 市面上有哪些可选的运行时?各有什么优缺点? 在这篇文章里,你都会得到答案。

我们在之前关于 KubeCon + CloudNativeCon 概述的文章中曾经简要提到 现在市面上已经有多个容器“运行时”( container runtime )。容器运行时的定义是:能够基于在线获取的镜像来创建和运行容器的程序。 容器运行时领域的标准和实现正在慢慢地走向成熟: Docker 在 KubeCon 上发布了containerd 1.0 , 几个月前 CRI-O 1.0 也发布了, 同时还有rkt也是一个运行时。 如果你正打算从 0 开始设计部署一个自己的容器系统或者 Kubernetes 集群的话, 面对这么多的容器运行时,你或许会感到有点迷茫。这篇文章我就会尝试解释什么是容器运行时,它们是做什么的,比较它们的特点,然后告诉你该如何选择适合自己的。 同时本文也会介绍容器规范和标准化方面的基础知识。


容器是什么?

在我们探索容器运行时之前,让我们先来看看容器到底是什么。当一个容器被启动时,主要会发生如下活动:

  1. 基于镜像( image ),一个容器会被创建出来。镜像image就是附加一个 JSON 配置文件的 tar 包。镜像常常是嵌套的:例如 Libresonic 的镜像是基于 Tomcat 的镜像, 而 Tomcat 的镜像又最终基于一个 Debian 的镜像。这样可以防止重复的内容占用空间。此例子中 Debian 镜像(或者其他的中间镜像)是上层其他镜像的基础。一个容器的镜像常常可以通过使用类似 docker build 的命令来生成。
  2. 如果必要, 容器运行时会从某处下载镜像,这个地方称为 registry 。Registry 通常是一个通过 HTTP协议暴露镜像的元数据和文件以供下载的容器仓库。 之前就只有 Docker Hub 独一个 registry , 但是现在基本上每个玩家都有自己的仓库了: 比如, Red Hat 有一个仓库服务于 OpenShift 项目, 微软的 Azure 也有一个仓库, Gitlab 也有一个仓库服务于它的持续集成平台。 Registry 仓库就是docker pull/push 所交互的服务器。
  3. 运行时将层次化的镜像解压到支持 Copy on Write( CoW )的文件系统里。通常这通过覆盖( overlay )文件系统来实现,所有的层次层层覆盖来构成一个合并的文件系统。 这个步骤通常不能通过命令行直接访问,而是当运行时创建容器时会自动在后台发生。
  4. 最后,运行时来实际地执行容器。 它告诉内核给容器分配合适的资源限制,创建隔离的层(为进程、网络、文件系统等),使用各种机制的混合(包括 cgroups、namespaces、capabilities、seccomp、AppArmor、SELinux 等等)。 比如 Docker , 当执行 docker run 时,它会创建并运行容器,但是底层它实际调用的是 runC 命令。

这些概念最先是在 Docker 的标准容器宣言 [1] 中描述的, 但是后来被从 Docker 里面删除了。尽管如此,标准化的努力一直在持续。Open Container Initiative( OCI )现在通过如下的一些规范涵盖了上面所述的大部分内容:

  • 镜像规范(常被称为“ 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 这样描述的:

rkt 的一个创新是通过 appc 规范来标准化容器格式,这我们在 2015 年曾经提到过 [2] 。CoreOS 一直都没有一个容器运行时接口的完整的标准的实现。到现在为止,rkt 的 Kubernetes 兼容层( rktlet )并没通过 Kubernetes 的所有集成测试,仍然还在开发中。CoreOS CTO Brandon Philips 在一封邮件里面这么说:


尽管如此,在 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 交互的方式:

 根据 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 的集成还没完全实现。


互操作性与默认项


所有以上这些运行时选项给社区里面带来了相当大的困惑。在 KubeCon 上,应该使用哪个 runtime 被反复问到。 Kubernetes 很可能从 Docker 转到别的运行时,因为它不需要 Docker 提供的所有功能,人们担心这种转换会带来兼容性的问题,因为新的运行时并不是实现了 Docker 一模一样的接口。比如日志文件,在 CRI 标准里面就是不同的。有些程序直接监控 Docker socket ,相关操作也不是符合标准的, 在新的运行时里面这些实现方法会不同,甚至完全没有实现。这些都可能导致在切换到一个不同的运行时的情况下会有风险。

 Kubernetes 会切换到哪个运行时(如果它确定要改),这个问题也是开放的。这也直接导致了运行时之间的竞争。在 KubeCon 上这个问题甚至带来了一些争议,因为在 CNCF 的 keynote 里面 CRI-O 都没被提到。Vicent Batts, Red Hat 的一名高级工程师因此在 Twitter 上发表不满: 

当我咨询他相关细节时,他解释到:

Batts 补充道, Red Hat 可能正处于一个临界点,一些应用可能开始以容器部署而不是 RPM,安全担忧(也就是安全补丁的跟踪,在容器格式的包中是缺乏的)是这种转换的一个阻碍。 通过Atomic 项目,Red Hat 看来正转向容器为核心,但是对于 Linux 发行版却有大的风险。


当我在 KubeCon 上问 CNCF 的 COO Chris Aniszczyk 时,他解释说 CNCF 现在的政策时优先营销顶级项目:

他补充到, “我们希望提供帮助,我们听到了反馈,也计划在 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 可能会考虑用它。

Philips 在博客中深入解释了 CoreOS 的位置:

在发布会的讨论上,Patel 也说了类似的话,"我们不需要 Kubernetes 用户知道什么是运行时"。说来也是,只要它工作正常,用户根本不用关心。 而且 OpenShift,Tectonic 等平台都把运行时的决策抽象化了,它们都会自动选他们自己的最佳缺省项来满足用户需求。所以 Kubernetes 到底选用哪个运行时作为缺省的这个问题对于开发者来说根本不用操心,只要他们在一个共识的标准里工作就行。在充满冲突的世界里面,看到这些开发者一起开诚布公的工作本身就是难得的了。

相关链接:

  1. https://github.com/moby/moby/commit/0db56e6c519b19ec16c6fbd12e3cee7dfa6018c5
  2. https://lwn.net/Articles/631630/
  3. https://kccncna17.sched.com/event/CU6T/cri-o-all-the-runtime-kubernetes-needs-and-nothing-more-mrunal-patel-red-hat
  4. https://schd.ws/hosted_files/kccncna17/e8/CRI-O-Kubecon-2017.pdf
  5. https://medium.com/cri-o/cri-o-support-for-kubernetes-4934830eb98e
  6. https://github.com/containerd/containerd/blob/master/RELEASES.md
  7. https://www.cncf.io/projects/graduation-criteria/

原文链接:https://lwn.net/Articles/741897/

END

331 comCount 0