了解容器运行时的演进与标准化之路

Published: 2023-10-17

Tags: Docker k8s

本文总阅读量

容器的爆炸式增长是由 Docker 引发的,但不久之后,工具、标准和缩写词似乎出现爆炸式增长。

那么“docker”到底是什么?“CRI” 和 “OCI” 等术语是什么意思?我们还应该关心吗?本文将做整理。

Docker 容器调用链

Docker 与 “容器” 常一同出现,但容器与 Docker 这个名字并不紧密相关,除了 Docker,我们也可以使用其他工具来运行容器。

容器生态系统由许多令人兴奋的技术、大量术语和相互竞争的大公司组成。

幸运的是,这些公司偶尔会以脆弱的休战方式走到一起,就一些标准达成一致。

围绕容器有两大标准:

  • Open Container Initiative (OCI):一组容器标准,对镜像格式、运行时、分发和配置文件等进行了规范。
  • Container Runtime Interface (CRI) in Kubernetes:接口规范,允许用户在 Kubernetes 中选择不同容器运行时,如 containerd、CRI-O 等。

稍后会更加详细的解释各个组件的作用。下图展示了 Docker、Kubernetes、CRI、OCI、containerd 和 runc 在这个生态系统中组合在一起:

首先是关于 Docker 的介绍

对于很多人来说,“Docker” 这个名字就是“容器” 的同义词。Docker 提供了易于使用的工具来创建和使用容器,该工具名为 docker,但 Docker 跟容器是不同的概念。

Docker 怎样工作?

在我们使用 docker ... 命令时,它调用了更低级别的工具来构建镜像,从仓库拉取镜像、创建、启动和管理容器。

Docker 堆栈中的底层工具有哪些?

从底层往上层看

  • 低层级(low-level)的容器运行时:runc,它基于 Linux 的功能来创建和运行容器。遵循 OCI 标准,包含 libcontainer(一个用于创建容器的 Go 库)
  • 高层级( high-level)的容器运行时:containerd,位于低层级运行时之上,并添加了一系列功能,例如拉取镜像、存储和网络,它同样支持 OCI 规范。
  • Docker 守护进程:dockerd,在后台保持运行的长时间运行的进程,它提供标准 API,并与容器运行时通过 gRPC 协议进行通信。
  • 最高层级的工具:docker-cli,使用户能够使用 docker ... 命令与 Docker 守护进程交互,用户无需了解较低级别服务也可操作容器。

libcontainer 借助 Linux 的 Namespace 用来进行资源隔离、使用 Cgroups 进行资源限制,提供底层容器基础能力,runc 作为一个完整工具在此基础上实现了 OCI 规范定义的交互和生命周期流程管理。

因此,当我们使用 docker 运行容器时,实际上是通过 Docker 守护进程来运行它,该守护进程调用 containerd,然后使用 runc 管理容器。

稍后会对这几个层级的工具进一步介绍。

Kubernetes 是否使用 Docker?

Kubernetes 使用 Docker 吗?好吧,现在不再使用了 —— 但以前是。另一个常见的问题是 “容器如何在 Kubernetes 中运行?”。

最初,Kubernetes 使用 Docker(Docker Engine)来运行容器,随着时间的推移,Kubernetes 演变成一个与容器无关的平台,容器运行时接口 CRI API 让 Kubernetes 能够接入不同的容器运行时。

Docker Engine 是一个比 Kubernetes 更早的项目,并不支持 CRI,为了融合现有生态,Kubernetes 项目包含了一个名为 dockershim 的组件,它允许 Kubernetes 使用 Docker 运行时来运行容器,弥合了旧世界和新世界之间的鸿沟。

Death of the shim 从 Kubernetes 1.24 开始,dockershim 组件被完全删除,并且 Kubernetes 不再支持 Docker 作为容器运行时,但这并不意味着 Kubernetes 不能运行 “Docker 格式” 的容器。

作为 Kubernetes 集群中替代 Docker 引擎的继任者 —— containerd,兼容 CRI 协议,可以在 Kubernetes 中运行 Docker 格式和 OCI 格式的镜像,可以完全替代 docker 命令及 Docker 守护进程。另外除了 containerd,CRI-O 是也是一个可选项。

除上图展示的链路,下图也能更加清晰的展示 Docker 及 K8S 使用 containerd 的不同方式。

OCI 和 CRI 规范

介绍了有一会儿 Docker,接下来了解 OCI 及 CRI 规范

开放容器倡议(OCI)

OCI 是最早为容器制定一些标准的组织之一,它由 Docker 等公司于 2015 年成立并提供支持,维护容器镜像格式以及容器运行方式的规范。

OCI 规范背后的想法是标准化容器是什么以及它应该能够做什么,这意味着我们可以自由选择符合规范的不同运行时。

例如:可以为 Linux 主机使用一种符合 OCI 的运行时,为 Windows 主机使用不同的运行时。

Kubernetes 容器运行时接口(CRI)

另一个标准是容器运行时接口(CRI),由 Kubernetes 项目创建的 API。

CRI 是 Kubernetes 用于控制创建和管理容器的不同运行时的接口,Kubernetes 尽量不关心用户使用哪个容器运行时,它只需要能够向它发送指令 —— 为 Pod 创建容器、终止它们等等,这就是 CRI 的用武之地。

CRI 是现在或将来可能存在的任何类型的容器运行时的抽象,因此 CRI 使 Kubernetes 更容易使用不同的容器运行时。

Kubernetes 项目不需要单独添加对每个运行时的支持,CRI API 描述了 Kubernetes 如何与任何运行时交互,只要给定的容器运行时实现了 CRI API,运行时就可以按照自己喜欢的方式创建和启动容器。

以下是 CRI 如何融入容器生态系统的可视化图例:

因此,我们可以选择使用 Containerd 在 Kubernetes 中运行容器,也可以使用 CRI-O,这是因为这两个运行时都实现了 CRI 规范。

判断 Kubernetes 中的容器运行时

命令

$ kubectl get nodes -o wide

阿里云的 ACK(Kubernetes 集群)从输出的 CONTAINER-RUNTIME 列可以看到值为 containerd://1.6.20,即默认运行时是 containerd

containerd 和 CRI-O(high-level)

在 Docker 调用链中,我们看到它调用更低层级的工具执行任务,接下来介绍 containerd 和 CRI-O

  • containerd 容器运行时由 Docker 公司开发,CRI-O 由 Red Hat 等公司开发。
  • 它们都实现了 CRI 规范,在 Kubernetes 中,我们可以二选一使用。
  • 它们都能从镜像仓库中拉取映像,管理它们,然后移交给较低级别​​的运行时(例:runc)管理。

容器运行时经不断的发展,上图可以看到从最初的 Docker 兼容模式,到 containerd + cri 插件,再到新版本 containerd 支持 CR ,中间链路在减少,安全和效率在提升。

containerd 和 CRI-O 都是容器运行时的热门选择,相较于 containerd,CRI-O 更加轻量化,且专为 Kubernetes 打造。

runc 和其它低层级运行时(low-level)

runc 是一个兼容 OCI 的低层级容器运行时,被 containerd、CRI-O 等上层运行时调用。

runc 被称为 OCI 的参考实现,它完整实现了 OCI 规范,它与 Linux 系统功能(例如 namespace 和 cgroups)交互,为容器提供所有低层功能,用来支持创建和运行容器进程。

除了 runc,还有其它运行时可供选择:

crun: 基于 C 语言实现的 OCI 容器运行时

runc 由 Go 语言开发,相较而言,基于 C 语言的 crun 性能更好(效率大概是 runc 的两倍)、二进制文件更小(300K vs 15MB),运行中内存占用也会更低。

同时 runc 支持更多的实验特性(🧪),因为 runc 是 OCI 规范的标准实现,实验性质的功能不适合在 runc 中引入,但可以使用 crun 进行验证,如果方案可行,会被引入 OCI 规范,进而更新 runc 的实现。

runc 和 crun 都实现了 OCI 标准,因此可以相互替换,crun 已经经过大量的线上测试,可用于生产环境。

总结

至此,我们了解到 Docker 只是容器生态系统中的一部分。

标准化使得容器生态百家争鸣,不依赖具体的公司的工具,用户可以选择适合自己的方案而不被工具裹挟。

混乱的时代已过,OCI 和 CRI 在新篇章起了一个好头。

参考