容器化到容器编排之旅

点击查看目录

本文为翻译文章,点击查看原文

编者按

本文是一篇介绍容器运行时和管理工具的文章。文中对主要的容器管理项目和技术做了较为详细的介绍和横向对比,并给出了项目的代码库供读者参考。

前言

容器带来了更高级的服务端架构和更复杂的部署技术。目前已经有一堆类似标准的规范(1, 2, 3, 4, ……)描述了容器领域的方方面面。当然,它的底层是 Linux 的基本单元,如 namespace 和 cgroups。容器化软件已经变得非常的庞大,如果没有它自己关注的分离层,几乎是不可能实现的。在这个持续努力的过程中,我尝试引导自己从最底层到最高层尽可能多的实践(代码、安装、配置、集成等等),当然还有尽可能多的获得乐趣。本篇内容会随着时间的推移而改变,并反映出我对这一主题的理解。

容器运行时

我想从最底层的非内核原语说起——容器运行时。在容器服务里,运行时这个词是有歧义的。每个项目、公司或社区对术语容器运行时都有自己的、通常是基于上下文的特定理解。大多数情况下,运行时的特征是由一组职责定义的,从最基本的职责(创建 namespace、启动init进程)到复杂的容器管理,包括(但不限于)镜像操作。这篇文章对运行时有一个很好的概述。

img
img

本节专门讨论低阶容器运行时。在OCI 运行时规范中,组成Open Container Initiative的一些重要参与者对底层运行时进行了标准化。长话短说,低阶容器运行时是一个软件,作为一个包含 rootfs 和配置的目录输入,来描述容器参数(如资源限制、挂载点、流程开始等),并作为运行时启动一个独立进程,即容器。

到 2019 年,使用最广泛的容器运行时是runc。这个项目最初是 Docker 的一部分(因此它是用 Go 编写的),但最终被提取并转换为一个独立的 CLI 工具。很难高估这个组件的重要性——基本上 runc 是 OCI 运行时规范的一个参考实现。在我们的实践中将大量使用 runc,下面是一篇介绍性文章(编者注:页面暂无内容)。

img
img

一个更值得注意的 OCI 运行时实现是crun。它用 C 语言编写,既可以作为可执行文件,也可以作为库使用。

容器管理

在命令行中可以使用 runc 启动任意数量的容器。但是如果我们需要让这个过程自动化呢?假设我们需要启动数十个容器来跟踪它们的状态,其中一些在失败时需要重启,在终止时需要释放资源,必须从注册中心提取镜像,需要配置容器间网络等等。这是一个稍微高级的任务,并且是“容器管理器”的职责。老实说,我不知道这个词是否常用,但我发现用它来描述很合适。我将以下项目归类为“容器管理器”:containerdcri-odockerdpodman.

containerd

与 runc 一样,我们可以再次看到 Docker 的遗产——containerd曾经是 Docker 项目的一部分,现在它是一个独立的软件,自称为容器运行时。但显然,它与运行时 runc 不是同一种类型的运行时。不仅它们的职责不同,其组织形式也不同。runc 只是一个命令行工具,containerd 是一个长活的守护进程。一个 runc 实例不能比底层容器进程活得更久。通常,它在create调用时启动,然后在start时从容器的 rootfs 中exec特定文件。另一方面,containerd 可以运行的比成千上万个容器更长久。它更像是一个服务器,侦听传入的请求来启动、停止或报告容器的状态。在幕后,containerd 使用 runc。它不仅仅是一个容器生命周期管理器,还负责镜像管理(从注册表中拉取和提交镜像,本地存储镜像等等),跨容器联网管理和其他一些功能。

img
img

cri-o

另一个容器管理器是cri-o。containerd 是 Docker 重构后的结果,但 cri-o 却源于 Kubernetes 领域。在过去,Kubernetes 使用 Docker 管理容器。然而,随着rkt的崛起,一些人增加了在 Kubernetes 中可互换容器运行时的支持,允许 Docker 和/或 rkt 完成容器管理。这种变化导致 Kubernetes 中有大量的条件判断,没有人喜欢代码中有太多的“if”。因此,容器运行时接口(CRI)被引入到 Kubernetes 中,这使得任何兼容 CRI 的高阶运行时(例如容器管理器)都可以在 Kubernetes 中使用,而无需任何代码的更改。cri-o 是 RedHat 实现的兼容 CRI 的运行时,与 containerd 一样,它也是一个守护进程,通过开放一个 gRPC 服务接口来创建、启动、停止(以及许多其他操作)容器。在底层,cri-o 可以使用任何符合 OCI 标准的低阶运行时和容器工作,默认的运行时仍然是 runc。cri-o 的主要目标是作为 Kubernetes 的容器运行时,版本控制也与 K8S 一致,项目的范围界定的很好,其代码库也比期望的更小(截止 2019 年 7 月大约是 20 个 CLOC,近似于 containerd 的 5 倍)。

img
img

cri-o 架构 (图像来自 cri-o.io)

规范的好处是所有符合规范的技术都可以互换使用。一旦 CRI 被引入,一个 containerd 的插件就可以在它的功能之上实现 CRI 的 gRPC 服务。这个想法是可行的,所以后来 containerd 获得了原生的 CRI 支持。因此,Kubernetes 可以同时使用 cri-o 和 containerd 作为运行时。

dockerd

还有一个守护进程是dockerd,它是个多面手。一方面,它为Docker 命令行 开放了一个 API,为我们提供了所有这些常用的 Docker 命令(Docker pullDocker pushDocker runDocker stats等)。但是我们已经知道这部分功能被提取到了 containerd 中,所以在底层 dockerd 依赖于 containerd 就不足为奇了。但这基本上意味着 dockerd 只是一个前端适配器,它将 containerd 的 API 转换为广泛使用的 docker 引擎的 API。

dockerd(编者注:原文链接是 moby 项目)也提供了composeswarm功能,试图解决容器编配问题,包括容器的多机器集群。正如我们在 Kubernetes 上看到的,这个问题相当难解决。对于一个单 dockerd 守护进程来说,同时承担两大职责并不好。

img
img

dockerd 是 containerd 前端的一部分(图片来源于 Docker Blog)

podman

守护进程中一个有趣的例外是podman。这是另一个 Red Hat 项目,目的是提供一个名为libpod的库(而不是守护进程)来管理镜像、容器生命周期和 pod(容器组)。podman是一个构建在这个库之上的命令行管理工具。作为一个低阶的容器运行时,这个项目也使用 runc。从代码角度来看,podman 和 cri-o(都是 Red Hat 项目)有很多共同点。例如,它们都在内部使用storageimage库。另一项正在进行的工作是在 cri-o 中直接使用 libpod 而不是 runc。podman 的另一个有趣的特性是用 drop-in 替换一些(最流行的?)日常工作流程中的docker命令。该项目声称兼容(在一定程度上)docker CLI API。

既然我们已经有了 dockerd、containerd 和 cri-o,为什么还要开发这样的项目呢?守护进程作为容器管理器的问题是,它们大多数时候必须使用 root 权限运行。尽管由于守护进程的整体性,系统中没有 root 权限也可以完成其 90% 的功能,但是剩下的 10% 需要以 root 启动守护进程。使用 podman,最终有可能使 Linux 用户的 namespace 拥有无根(rootless)容器。这可能是一个大问题,特别是在广泛的 CI 或多租户环境中,因为即使是没有权限的 Docker 容器实际上也只是系统上的一个没有 root 访问权限的内核错误

关于这个有趣项目的更多信息可以在 这里这里找到。

img
img

conman

这是我正在做的项目,目的是实现一个微型容器管理器。它主要用于教学目的,但是最终的目标是使它兼容 CRI,并作为容器运行时运行在 Kubernetes 集群上。

运行时垫片(runtime shims)

如果你自己尝试一下就会很快发现,以编程方式从容器管理器使用 runc 是一项相当棘手的任务。以下是需要解决的困难清单。

在容器管理器重启时保证容器存活

容器可以长时间运行,而容器管理器可能由于崩溃或更新(或无法预见的原因)而需要重新启动。这意味着我们需要使每个容器实例独立于启动它的容器管理器进程。幸运的是,runc 提供了一种方式通过命令runc run --detach从正在运行的容器中分离。我们也可能需要能够附加到一个正在运行的容器上。为此,runc 可以运行一个由 Linux 伪终端控制的容器。通过 Unix 套接字传递 PTY 主文件描述符,可以将 PTY 的 master 端回传到启动进程(请参阅runc create --console-socket选项)。这意味着,只要底层容器实例存在,我们就可以保持启动进程的活动状态,以保存 PTY 文件描述符。如果我们决定在容器管理器进程中存储主 PTY 文件描述符,则重新启动该管理器将导致文件描述符的丢失,从而失去重新附着到正在运行的容器的能力。这意味着我们需要一个专用的(轻量级的)包装进程来负责转化和保持运行容器的附属状态。

同步容器管理器和包装的 runc 实例

由于我们通过添加包装器进程对 runc 进行了转化,所以需要一个 side-channel(也可能是 Unix 套接字)来将容器的启动传回容器管理器。

持续追踪容器退出码(exit code)

分离容器会导致缺少容器状态更新。我们需要有一种方式将状态反馈给管理器。出于这个目的,文件系统听起来也是一个不错的选择。我们可以让包装器进程等待子 runc 进程终止,然后将它的退出码写到磁盘上预定义的位置。

为了解决所有这些问题(可能还有其他一些问题),通常使用所谓的 runtime shims。shim 是一个轻量级守护进程,控制一个正在运行的容器。shims 的实现有conmon和 containerd 的 runtime shim。我花了一些时间实现了自己的 shim 作为conman项目的一部分,可以在文章“实现容器运行时 shim”中找到(编者注:此文章还未撰写)。

容器网络接口 (CNI)

我们有多个责任重叠的容器运行时(或管理器),很明显需要提取网络相关的代码到一个专门的项目来复用它,或者每个运行时都应该有自己的方式来配置 NIC 设备,IP 路由,防火墙和网络的其他方面。例如,cri-o 和 containerd 都必须创建 Linux 网络名称空间,并设置 Linux bridgeveth设备来为 Kubernetes pods 创建沙箱。为了解决这个问题,引入了容器网络接口项目。

CNI 项目提供了一个定义 CNI 插件的容器网络接口规范。插件是一个可执行的 sic,容器运行时(或管理器)会调用它来安装(或释放)网络资源。插件可以用来创建网络接口,管理 IP 地址分配,或者对系统进行一些自定义配置。CNI 项目与语言无关,由于插件被定义为可执行的,它可以用于任何编程语言实现的运行时管理系统。CNI 项目还为作为一个名为plugins的用于存放最流行的用例的独立的代码库提供了一组参考插件实现。例如bridgeloopbackflannel等。

一些第三方项目将其网络相关的功能实现为 CNI 插件。一些最著名的项目如Project CalicoWeave

编排

容器的编排是一个非常大的主题。实际上,Kubernetes 代码中最大的部分就是解决编排问题,而不是容器化问题。因此,编排应该有自己单独的文章(或几篇)而不在本文描述。希望他们能很快跟进。

img
img

值得注意的项目

buildah

Buildah 是一个和OCI 容器镜像一起使用的命令行工具。它是 RedHat 发起的一组项目(podman、skopeo、buildah)的一部分,目的是重新设计 Docker 处理容器的方法(主要是将单体和基于守护进程的方法转换为更细粒度的方法)。

cni

CNI 项目定义了一个容器网络接口插件规范以及一些 Go 工具。有关更深入的解释,请参见这篇文章相应的部分。

cni-plugins

一个最流行的 CNI 插件(如网桥、主机设备、环回、dhcp、防火墙等)的主库。有关更深入的解释,请参见文章的相应部分。

containerd

高级容器运行时(或容器管理器)作为 Docker 的一部分启动,并提取到了独立的项目中。有关更深入的解释,请参见相应的部分

conmon

一个用 C 语言编写的小型 OCI 运行时 shim,主要由crio使用。它提供了父进程(crio)与启动容器之间的同步、容器启动、退出码追踪、PTY 转发和其他一些功能。有关更深入的解释,请参见相应的部分

cri-o

专注于 Kubernetes 容器管理器,遵循 Kubernetes 容器运行时接口(CRI)规范。版本控制与 k8s 版本控制相同。有关更深入的解释,请参见相应的部分

crun

另一个 OCI 运行时规范实现。它声称是”快速和低内存占用的 OCI 容器运行时,完全用 C 编写“。但最重要的是,它可以用作任何 C/C++代码(或提供绑定其他语言)的库。它允许避免一些由它的守护进程特性引起的特定的“runc”缺陷。有关更多信息,请参见Runtime Shims一节。

image

一个被低估的(主观评价)Go 工具库,为criopodmanskopeo等知名项目提供了支持。通过它的名字就很容易猜到——其目的是用各种方式来处理容器镜像和镜像注册表。

lxc

一个由 C 编写的可替换的低级容器运行时。

lxd

一个由 Go 编写的高级容器运行时(或容器管理器)。底层使用 lxc 作为低级运行时。

moby

高级容器运行时(或容器管理器),以前称为docker/docker。提供一个著名的基于containerd功能的 Docker 引擎 API。有关更深入的解释,请参见相应的部分

OCI distribution spec

一个容器镜像发布规范(开发中)。

OCI image spec

一个容器镜像规范。

OCI runtime spec

一个低阶容器运行时规范。

podman

一个无守护进程的 Docker 替代品。Docker 重新设计的 frontman 项目。更多信息参见 RedHat developers blog

rkt

另一个容器管理系统。它提供了一个低阶运行时和一个高阶管理接口。它宣称是 Pod 原生的。向 Kubernetes 添加rkt支持的想法催生了 CRI 规范。该项目由 CoreOS 团队于 5 年前启动,在被 RedHat 收购后却停滞不前。截止到 2019 年 8 月,该项目的最后一次提交已经是大约两个月前了。更新:8 月 16 日,CNCF宣布技术监督委员会(TOC)投票决定将 rkt 项目存档。

runc

一个低阶容器运行时和 OCI 运行时规范的参考实现。一开始作为 Docker 的一部分现在提取到了一个独立的项目中,普及度很高。有关更深入的解释,请参见相应的部分

skopeo

Skopeo 是一个命令行工具集,对容器镜像和镜像库执行各种操作。这是 RedHat 重新设计 Docker(参见 podman 和 buildah)工作的一部分,它将自己的职责抽取为专用的和独立的工具。

storage

一个被低估的 Go 类库,为 crio、podman 和 skopeo 等知名项目提供了支持。其目的是为存储文件系统层、容器镜像和容器(磁盘上的)提供方法。它还管理 bundle 的加载。

编辑本页