LWN: Docker 以及 OCI 容器生态!

关注了就能看到更多这么棒的文章哦~

Docker and the OCI container ecosystem

July 26, 2022
This article was contributed by Jordan Webb
DeepL assisted translation
https://lwn.net/Articles/902049/

Docker 已经改变了许多人开发以及部署软件的方式。它不是第一个在 Linux 上实现的容器方案,但 Docker 对于容器应该如何结构化以及管理的想法跟其他先行者很不一样。这些想法现在已经成为行业标准,围绕它们还发展出了一个软件生态系统。Docker 仍然是这个生态系统的主要参与者,但它不再是这个大海中唯一的鲸鱼了,因为 Red Hat 也在容器工具方面做了大量工作,而且现在 Docker 的许多产品都有了其他人实现的替代方案。

Anatomy of a container

容器有点像是一个轻量级的虚拟机;它与跟 host 共享同一个内核,但从在容器里面运行的软件看来,其他大多数内容都是跟 host 的软件无关联的。Linux 内核本身没有容器的概念;实际上是通过使用几个内核功能的组合来创建出来容器的。

  • bind mount 和 overlayfs,用来构建了容器的根文件系统。

  • cgroup 可以用来划分 host kernel 的 CPU、内存和 I/O 资源来独立使用。

  • namespace 用来给容器内运行的进程创建一个独立的系统视图。

Linux 的命名空间(namespace)是一个用来创建容器的关键因素。Linux 支持系统在好几个不同方面的命名空间,包括用于 user ID 和 group ID 的独立的 user namespace、用于不同进程 ID 的 PID namespace、用于不同网络接口组合的 network namespace,以及其他一些命名空间。在容器启动时,有一个运行时(runtime)会为容器创建适当的 cgroup、namespace 和文件系统 mount;然后在它所创建的环境中启动一个进程。

关于这个进程应该是什么,存在着一定程度的分歧。有些人喜欢启动一个像 systemd 这样的初始进程,在容器内运行一个完整的 Linux 系统。这种被称为 "系统容器(system container)";在 Docker 之前,这是最常见的一类容器。系统容器目前仍然有 LXC 和 OpenVZ 等软件可以支持。

Docker 的开发者有一个不同的想法。Docker 认为,每个容器不应该在容器内运行整个系统,而应该只运行一个应用程序。这种风格的容器被称为 "应用容器(application container)"。应用容器是通过一个容器镜像文件(container image)来启动的,这个 image 里把应用程序本身以及它所依赖的内容捆绑在一起,并构造了最基本的 Linux 根文件系统来运行这个应用。

容器镜像通常不包括一个初始系统,甚至可能不包括一个软件包管理器,容器镜像通常会被替换成更新版本,而不是在容器里面进行软件更新。一个静态编译的应用程序的镜像可能是最小的,只包含一个二进制文件以及 /etc 中的几个支持文件。应用程序容器通常没有一个持久的根文件系统(persistent root filesystem);相反,overlayfs 被用来在容器镜像的最上层创建了一个临时的 layer。当容器停止时,这个 layer 就被扔掉了。容器镜像之外的任何持久性数据(persistent data)都是通过将 host 上的另一个位置的目录 bind mount 来作为容器的文件系统的。

The OCI ecosystem

如今当人们谈论容器时,他们很可能是在谈论由 Docker 所推广的应用容器的这种方式。事实上,除非另有说明,他们说的很可能就是 Docker 软件所实现的那些具体的容器镜像格式、运行时环境(run-time environment)和 registry API。这些都已经被开放容器倡议(OCI, Open Container Initiative)标准化了,这是一个由 Docker 和 Linux 基金会在 2015 年所建立的行业机构。Docker 将其软件重构为一些较小的组件;其中一些组件及其规范都被 OCI 所涵盖。OCI 发布的软件和规范就形成了现在这样一个强大的容器相关软件生态系统的种子。

OCI image specification 就定义了容器图像格式,其是由 JSON 配置(包含环境变量、执行路径等)和一系列称为 "layer" 的 tarballs 组成。每层的内容都是相互堆叠起来,按顺序叠放在一起,从而构建出容器镜像的根文件系统。这些 layer 可以在镜像之间互相共享;如果一台服务器正在运行引用了相同 layer 的几个不同容器,它们就可以共享该 layer 的同一个副本。Docker 为几个流行的 Linux 发行版提供了最小的镜像,可以作为应用容器的基础 layer。

OCI 还发布了一个 distribution specification。在这里,"distribution" 指的并不是 Linux 发行版,而是更广泛的意义。该规范定义了一个 HTTP API,用于向服务器 push 和 pull 容器镜像文件;实现该 API 的服务器就被称为容器注册中心(container registry)。Docker 维护着一个名为 Docker Hub 的大型的公共注册服务,以及一个可以自我托管(self-hosted)的参考实现(称为 "Distribution",这个名字有点让人困惑)。该规范还有其他实现版本,包括红帽的 Quay 和 VMware 的 Harbor,以及亚马逊、GitHub、GitLab 和谷歌所提供托管的产品。

实现 OCI runtime specification 的程序则负责与实际运行容器有关的一切内容。它设置了所有必需的 mount、cgroup 和 kernel namespace,并运行容器内的进程,也负责在容器内的所有进程退出后关闭任何与容器有关的资源。这个 runtime specification 的参考实现就是 runc,这是由 Docker 为 OCI 创建的。

还有一些其他的 OCI runtime 可供选择。例如,crun 提供了一个用 C 语言编写的 OCI 运行时,其目标是比 runc 更快、更轻量,而 runc 和 OCI 生态系统的其他大部分一样,是用 Go 编写的。谷歌的 gVisor 包括了一个 runsc,这通过在用户模式内核(user-mode kernel)上运行应用程序,从而提供了与 host 更好的隔离性。亚马逊的 Firecracker 是一个用 Rust 编写的最小的 hypervisor 程序,可以使用 KVM 来给每个容器提供自己的虚拟机;英特尔的 Kata Containers 工作方式类似,但支持多个 hypervisor(包括 Firecracker)。

容器引擎(container engine)是将这三种规范联系在一起的程序。它按照 distribution specification 实现了相应的客户端处理,从而可以在 registry 上检索容器镜像,根据 specification 来解释它获取到的镜像文件,并利用实现了 runtime specification 的程序来启动这个容器。容器引擎为用户提供了工具和 API,用来管理容器镜像、进程和存储。

Kubernetes 是一个容器协调器(container orchestrator),能够在数百甚至数千个服务器上调度以及运行容器。Kubernetes 本身并没有实现任何 OCI 规范。它需要与容器引擎相结合来使用,后者来替 Kubernetes 对容器进行管理和操作。它用来与容器引擎通信的接口就被称为容器运行时接口(CRI, Container Runtime Interface)。

Docker

Docker 是最初的 OCI 容器引擎,由两个用户可见的主要组件组成:一个名为 docker 的命令行界面(CLI)的客户端和一个服务器。服务器在 Docker 自己的软件包中被命名为 dockerd,但当 Docker 在 2017 年创建 Moby 项目时,这个代码库被重新命名为 moby。Moby 项目是一个包含了所有 Docker 和其他容器引擎使用的开源组件开发的总括性项目(umbrella organization)。当宣布 Moby 的时候,许多人发现 Docker 和 Moby 项目之间的关系是很混乱的,就类似于 Fedora 和 Red Hat 之间的关系。

dockerd 提供了一个 HTTP API;一般来说它会监听一个名为/var/run/docker.sock 的 Unix socket,但也可以让它监听一个 TCP socket。docker 命令只是这个 API 的一个客户端程序;服务器负责下载镜像和启动容器进程。客户端支持在前台来启动容器运行,因此在命令行上运行容器的行为就类似于运行其他任意程序一样,但这实际上只是模拟出来的效果。在这种工作模式下,容器进程仍然是由服务器启动的,input 和 output 会通过 API socket 来进行 stream 传输;当进程退出时,服务器会向客户端报告,然后客户端会相应地设置自己的退出状态。

这种设计是跟 systemd 或其他类似的进程监管工具无法兼容的,因为 CLI 从来没有自己的子进程。在进程监督程序下运行 docker CLI,只能监督 CLI 进程本身。这对这些工具的用户来说会有一些影响。例如,任何试图通过将 CLI 作为 systemd 服务运行来限制容器的内存使用量的做法都是无效的;这些限制将只适用于 CLI 以及它完全不存在的子进程上。此外,杀死客户端进程,无法做到终止容器中的所有进程。

如果不限制对 Docker socket 的访问权限,会有很大的安全隐患。默认情况下,dockerd 是以 root 身份运行的。任何能够连接到 Docker socket 的人都可以完全访问相关 API。由于 API 允许以特定的 UID 身份来运行容器,并将任意的文件系统位置 bind 进来,因此对于能够访问 socket 的人来说,可以轻松成为 host 上的 root 用户。2019 年增加了对 rootless 模式运行的支持,并在 2020 年稳定下来,但仍然不是默认使用模式。

Docker 可以被 Kubernetes 用来运行容器,但它并不直接支持 CRI 规范。最初,Kubernetes 包括了一个名为 dockershim 的组件,在 CRI 和 Docker API 之间提供了一个桥梁,但在 2020 年被废弃了。该代码已经从 Kubernetes 仓库中剥离出来,现在称为 cri-dockerd 进行单独维护了。

containerd & nerdctl

Docker 在 2015 年将其软件重构为一些独立的组件;containerd 就是这项工作的成果之一。2017 年,Docker 将 containerd 捐赠给了云原生计算基金会(CNCF, Cloud Native Computing Foundation),该基金会负责管理 Kubernetes 和其他工具的开发工作。此工具也仍然包含在 Docker 中,但它也可以作为一个独立的容器引擎来使用,或者通过一个内置的 CRI 插件与 Kubernetes 一起使用。containerd 的架构是高度模块化的。这种灵活性有助于它作为实验性功能的试验场。例如,可以使用 plugin 来支持不同的存储容器镜像的方式,或者支持其他镜像格式。

如果没有任何额外的插件的话,containerd 实际上是 Docker 的一个子集;其核心功能与 OCI 规范紧密相连。针对 Docker 的 API 设计的工具不能用于 containerd。相反,它提供了一个基于谷歌 gRPC 的 API。不幸的是,如果系统管理员希望能使用访问控制(access control)的话,这里是不具备此功能的;尽管与 Docker 的 API 不兼容,但是 containerd 的 API 的安全方面的假设似乎是跟 Docker API 一致的。

containerd 的文档指出,它遵循的是一个智能客户端模型(这一点是相对于 Docker 的 "dumb client" 而言的)。这意味着很多差异,其中之一就是 containerd 不与 container registry 服务器直接沟通;相反,(智能)客户端需要自己下载他们需要的任何镜像。尽管客户端模型不同,containerd 仍然有一个与 Docker 类似的进程模型;容器进程是从 containerd 进程 fork 出来的。一般来说,如果没有额外的软件的话,containerd 的工作与 Docker 没有什么不同,只是能做得工作比较少。

当 containerd 与 Docker 捆绑在一起时,dockerd 作为智能客户端,接受来自自己的 dumb client 的 Docker API 调用,并在调用 containerd API 之前做一些必需的额外工作;当与 Kubernetes 一起使用时,这些事情是由 CRI 插件来处理的。此外,containerd 直到最近才真正有了自己的客户端。它包括一个名为 ctr 的 bare-bone(底层) CLI,但这只是用来调试的。

在 2020 年 12 月随着 nerdctl 的发布,这一点发生了变化。自其发布以来,独立运行 containerd 的方式就变得更加实用了;nerdctl 的用户界面希望能与 Docker CLI 兼容,并提供了 Docker 用户发现的这个独立的 containerd 安装方式中所缺少的大部分功能。不需要与 Docker API 兼容的用户可能会发现 containerd 和 nertdctl 就足够使用了。

Podman

Podman 是红帽公司赞助的 Docker 的一个替代品,旨在直接替代 Docker。跟 Docker 和 containerd 一样,它是用 Go 语言编写的,并在 Apache 2.0 的许可条款下进行发布,但它不是一个 fork,而是一个独立的重新实现。红帽对 Podman 的资助可能部分是出于它在努力使 Docker 的软件与 systemd 配合工作时所遇到的困难。

从表面上看,Podman 似乎与 Docker 几乎完全一样。它可以使用相同的容器镜像、跟相同的 registry 服务器交流。Podman CLI 是 docker 的克隆,目的是让从 Docker 迁移过来的用户可以将 docker 直接 alias 为 podman,就可以继续像以前一样使用,感觉什么都没有改变一样。

最初,Podman 提供了一个基于 varlink 协议的 API。这意味着虽然 Podman 在 CLI 层面上与 Docker 是兼容的,但直接使用 Docker API 的工具却不能用于 Podman。在 3.0 版本中,varlink API 被废除,转而使用 HTTP API,其目的是与 Docker 提供的 API 相兼容,同时增加一些 Podman 特有的 endpoint。这个新的 API 正在迅速成熟,但为 Docker 设计的工具的用户最好在决定切换到 Podman 之前测试一下兼容性。

由于它在很大程度上是 Docker API 的复制,所以 Podman 的 API 没有任何 access control 功能,但 Podman 有一些架构上的差异,这可能使其变得不那么重要了。在开发初期,Podman 就支持了 rootless 运行模式。在这种模式下,除了 newuidmap 和 newgidmap 所提供的一些微小帮助之外,我们还可以在没有 root 权限或其他特殊权限的情况下创建容器了。与 Docker 有一点不同,Podman 在由非 root 用户调用时,默认会使用 rootless 模式。

Podman 的用户也可以通过简单地禁用 API socket 来规避对其安全的担忧。虽然它的界面与 Docker CLI 基本相同,但 podman 不是单纯的 API 客户端。它可以在不需要任何守护程序的帮助的情况下就自己创建出容器。因此,Podman 与 systemd 等工具就能很好地配合起来了;podman run 在进程监督工具看来是符合预期的,因为容器内的进程是 podman run 的子进程。Podman 的开发者通过为 Podman 容器生成 systemd unit 的命令来鼓励人们以这种方式运行。

除了进程模型方面的改进外,Podman 在其他方面也迎合了 systemd 用户的需求。虽然在容器中运行 systemd 这样的初始系统与 Docker 的每个容器只有一个应用程序的理念相悖,但 Podman 还是不遗余力地想让这种方式变得简单。如果容器指定运行的程序是一个 init 系统,那么 Podman 会自动挂载 systemd 运行所需的所有内核文件系统。它还支持通过 sd_notify()向 systemd 报告容器的状态,或者将 notification socket 移交给容器内的应用程序供其直接使用。

Podman 也有一些想用来吸引 Kubernetes 用户的功能。跟 Kubernetes 一样,它支持 "pod" 的概念,也就是一组共享了同一个 network namespace 的容器。它可以使用 Kubernetes 配置文件来运行容器,也可以生成 Kubernetes 配置文件。然而,与 Docker 和 containerd 不同,Podman 没有办法被 Kubernetes 利用来运行容器。这是故意为之的。红帽没有为 Podman(一个通用的容器引擎)添加 CRI 支持,而是选择了资助开发一个更专业的 CRI-O 形式的替代方案。

CRI-O

CRI-O 基于了许多与 Podman 相同底层设施。因此,CRI-O 和 Podman 之间的关系可以说是类似于 containerd 和 Docker 之间的关系;CRI-O 提供了许多与 Podman 相同的技术,但减少了一些装饰(frills)。不过,这个类比并不是很严格。与 containerd 和 Docker 不同,CRI-O 和 Podman 是完全独立的项目;两者都并不会包含另一个。

正如其名称所暗示的,CRI-O 实现了 Kubernetes CRI。事实上,这就是它所实现的一切了;CRI-O 是专门为 Kubernetes 使用而建立的。它是与 Kubernetes 的发布周期来同步开发的,任何不需要 CRI 的东西都被明确声明为不被支持。CRI-O 不能在没有 Kubernetes 的情况下使用,也不包括自己的 CLI;根据项目的既定目标,任何使 CRI-O 用来满足独立使用需求的工作,都可能被开发者视为不受欢迎的一些分散注意力的工作。

与 Podman 一样,CRI-O 的开发最初是由 Red Hat 所赞助的;与 containerd 一样,它后来在 2019 年被捐赠给了 CNCF。虽然它们现在都在同一个组织的支持下,但 CRI-O 的更加集中的关注点可能使它比 containerd 对 Kubernetes 管理员来说更有吸引力。CRI-O 的开发者可以完全根据 Kubernetes 用户的利益最大化来自由做出决定,而 containerd 和其他容器引擎的开发者则有许多其他类型的用户和使用场景需要考虑。

Conclusion

这些只是最流行的容器引擎中的几个而已;还有一些其他项目,如 Apptainer 和 Pouch,分别迎合了其他不同的生态环境。还有一些工具可用于创建和操作容器镜像,如 Buildah、Buildpacks、skopeo 和 umoci。Docker 在开放容器计划(Open Container Initiative)中功不可没;这个工作所产生的标准和软件为众多项目提供了基础。这个生态系统是强大的;如果一个项目关闭了,那么有多个替代项目早就准备好了可以随时替代它。因此,这项技术的未来不再与某个特定的公司或项目挂钩;Docker 所开创的容器风格似乎在未来很很长一段时间内都可以陪伴着我们了。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

LWN: Docker 以及 OCI 容器生态!_第1张图片

你可能感兴趣的:(docker,大数据,编程语言,python,linux)