所谓三十年河东,三十年河西,曾经在容器领域叱咤风云的 Docker 如今已风光不再。抛开情怀,我们不得不承认,Docker 已经被后浪拍死在沙滩上了……

除了 Docker,我们还有哪些选择?_Linux

大约 4 年前的容器领域,Docker 是唯一的选择。

然而,如今情况已然大不同,Docker 不再是是唯一的选择,它只不过是一个容器引擎而已。我们可以用 Docker 构建、运行、拉取、推送或检查容器镜像,但是这里的每一项任务,都可以用其他工具替代,甚至有些工具比 Docker 还好。

所以,下面就让我们来探索一下这个领域,然后卸载和忘记 Docker 吧。



为什么说不要用 Docker 了?


如果长期以来你一直在使用 Docker,那么说服你考虑其他工具可能需要多费点唇舌。

首先,Docker 是一个整体化的工具,它试图做好所有的事情,但往往只会适得其反。在大多数情况下,我们应该选择专门的工具,它可能只做一件事情,但会做到最好。

可能你因为担心需要学习使用不同的 CLI、不同的 API 或接受不同的概念,所以会害怕使用其他工具。但是,请不用担心。本文介绍的任何工具都可以完美地无缝衔接,因为它们(包括 Docker)都遵循同一个 OCI(OpenContainer Initiative,开放容器计划)规范。OCI 包括容器运行时、容器分发和容器镜像的规范,涵盖了使用容器所需的所有功能。

因为有了 OCI,所以你可以自由选择适合自己的需求的工具,与此同时,你可以继续使用与 Docker 相同的 API 和 CLI 命令。

因此,如果你愿意尝试新工具,那么我们就来比较一下 Docker 与其竞争对手的优缺点和功能,看看是否有必要考虑放弃 Docker,并尝试使用一些新鲜出炉的工具。



容器引擎


在比较 Docker 与其他工具时,我们需要分别讨论它的各个组件,首先要讨论的就是容器引擎。

容器引擎是一种工具,它提供了处理镜像与容器的用户界面,这样你就不需要与 SECCOMP 规则或 SELinux 策略苦苦纠缠了。除此之外,容器引擎还可以从远程仓库提取镜像,并将其解压到本地磁盘上。它似乎也运行容器,但是实际上,它的工作是创建容器清单以及镜像层的目录。接着,它将这些文件传递给 runc 或 crun 等容器运行时。

除了 Docker,我们还有哪些选择?_Linux_02

目前有很多容器引擎可供我们使用,不过 Docker 最主要的竞争对手是红帽开发的 Podman。与 Docker 不同,Podman 不需要运行守护进程,也不需要 root 特权,这些都是 Docker 长期以来一直倍受关注的问题。从名字就可以看出来,Podman 不仅可以运行容器,还可以运行 pod。

如果你不熟悉 pod 的话,我可以简单介绍一下:pod 是 Kubernetes 的最小计算单元,由一个或多个容器 (主容器与负责支持主容器的 sidercar 容器) 组成。因此,Podman 用户以后可以很轻松地将他们的工作负载迁移到 Kubernetes。下面,我们通过一个简单的演示来说明如何在一个 Pod 中运行两个容器:

~ $ podman pod create --name mypod
~ $ podman pod list
POD ID        NAME    STATUS    CREATED         # OF CONTAINERS   INFRA ID
211eaecd307b  mypod   Running   2 minutes ago   1                 a901868616a5

~ $ podman run -d --pod mypod nginx  # First container
~ $ podman run -d --pod mypod nginx  # Second container
~ $ podman ps -a --pod

CONTAINER ID IMAGE                          COMMAND               CREATED        STATUS            PORTS  NAMES               POD           POD NAME
3b27d9eaa35c  docker.io/library/nginx:latest  nginx -g daemon o...  2 seconds ago Up 1 second ago         brave_ritchie      211eaecd307b  mypod
d638ac011412 docker.io/library/nginx:latest nginx -g daemon o...  5 minutesago  Up 5 minutes ago         cool_albattani      211eaecd307b mypod
a901868616a5 k8s.gcr.io/pause:3.2                                  6 minutesago  Up 5 minutes ago         211eaecd307b-infra  211eaecd307b mypod

最后一点,Podman 提供的 CLI 命令与 Docker 完全相同,因此你只需执行

alias docker=podman

然后就像什么都没有发生过一样。

除了 Docker 和 Podman 之外,还有其他容器引擎,但我并不看好它们的发展,或者不适合用于本地开发。

不过,如果你想对容器引擎有一个较为完整的了解,我也可以介绍一些:

LXD:LXD 是 LXC(Linux 容器)的容器管理器(守护进序)。这个工具提供了运行系统容器的能力,而这些系统容器提供了类似于虚拟机的容器环境。该工具比较小众,没有太多用户,所以除非你有非常特殊的用例,否则最好还是使用 Docker 或 Podman。

CRI-O:如果在网上搜索 cri-o 是什么,你可能会发现它被描述成了一种容器引擎。但实际上,它是一种容器运行时。它既不是容器引擎,也不适合“常规”使用。我的意思是说,它是专门作为 Kubernetes 运行时(CRI)而创建的,并不是给最终用户使用的。

rkt:rkt(读作“rocket”)是 CoreOS 开发的容器引擎。这里提到这个项目只是为了清单的完整性,因为这个项目已经结束了,它的开发也停止了,因此你不应该再使用它。



构建镜像


对于容器引擎,实际上 Docker 的替代品只有一种选择(即 Podman)。但是,在构建镜像方面,我们有很多选择。

首先,我们来看一看 Buildah。这也是一款红帽开发的工具,可以很好地与 Podman 协同工作。如果你已经安装了 Podman,可能会注意到 podman build 子命令,因为它的二进制文件已经包含在 Podman 中了,实际上这个命令只是经过包装的 Buildah。

至于功能,Buildah 沿用了 Podman 的方针:没有守护进程,不需要 root 特权,而且生成的是符合 OCI 的镜像,因此你的镜像的运行方式与使用 Docker 构建的镜像完全相同。它还能使用 Dockerfile 或 Containerfile 构建镜像, Dockerfile 与 Containerfile 实际上是同一个东西,只是叫法不同罢了。除此之外,Buildah 还提供了对镜像层更精细的控制,支持提交大量的变更到单个层。我认为,它与 Docker 之间有一个出乎意料的区别(但这个区别是好事),那就是使用 Buildah 构建的镜像特定于用户,因此你可以只列出自己构建的镜像。

你可能会问,既然 Podman CLI 中已经包含了 Buildah,为什么还要使用单独的 Buildah CLI 呢?其实,Buildah CLI 是 podman build 所包含的命令的超集,因此你可能不需要直接使用 BuildahCLI,但是通过使用它,你可能会发现一些额外的功能。

下面,我们来看一个示例:

~ $ buildah bud -f Dockerfile .

~ $ buildah from alpine:latest  # Create starting container - equivalent to"FROM alpine:latest"
Getting image source signatures
Copying blob df20fa9351a1 done
Copying config a24bb40132 done
Writing manifest to image destination
Storing signatures
alpine-working-container # Name of the temporary container
~ $ buildah run alpine-working-container -- apk add--update --no-cache python3  # equivalentto "RUN apk add --update --no-cache python3"
fetchhttp://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetchhttp://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz
...

~ $ buildah commit alpine-working-containermy-final-image  # Create final image
Getting image source signatures
Copying blob 50644c29ef5a skipped: already exists
Copying blob 362b9ae56246 done
Copying config 1ff90ec2e2 done
Writing manifest to image destination
Storing signatures
1ff90ec2e26e7c0a6b45b2c62901956d0eda138fa6093d8cbb29a88f6b95124c

~ # buildah images
REPOSITORY              TAG     IMAGE ID      CREATED         SIZE
localhost/my-final-image latest  1ff90ec2e26e 22 seconds ago  51.4 MB

从上面的脚本可以看出,你可以直接使用 buildah bud 构建镜像,其中 bud 代表使用 Dockerfile 进行构建,你也可以使用其他脚本化的方法,比如使用 Buildahs 的 from、run 和 copy,它们分别对应 Dockerfile 中的 FROM、RUN、COPY 命令。

接下来是 Google 的 Kaniko。Kaniko 也是利用 Dockerfile 构建容器镜像,而且与 Buildah 类似,它也不需要守护进程。但它与 Buildah 的主要区别在于,Kaniko 更加侧重于 Kubernetes 中的镜像构建。

Kaniko 本身也要作为镜像(gcr.io/kaniko-project/executor) 运行,这对于Kubernetes 来说是没有问题的,但对于本地构建来说不是很方便,并且在某种程度上违背了构建镜像的目的,因为你需要使用 Docker 运行 Kaniko 镜像才能构建镜像。话虽如此,如果你正在寻找在 Kubernetes 集群中构建镜像的工具 (例如在 CI/CD 管道中),那么 Kaniko 可能是一个不错的选择,因为它不需要守护进程,而且更安全。

以我个人的经验来看,我认为两者都能很好地完成工作,但是使用 Kaniko 时,我遇到了一些随机的构建故障,而且在将镜像推送到仓库时也出现了失败的情况。

我要介绍的第三个工具是 buildkit,也可以称之为 docker build 二代。它是 Moby 项目的一部分(与 Docker一样),只需设置 DOCKER_BUILDKIT=1 docker build,就可以启动这个工具,并作为 Docker 的一个实验性功能使用。那么,这个工具究竟能给你带来什么?它带来了很多改进和很酷的功能,包括并行构建、跳过未使用的阶段、更好的增量构建以及不需要 root 权限等构建。但是,它仍然需要运行守护进程 (buildkitd)。因此,如果你不想摆脱 Docker,同时又想要一些新的功能和改进,那么可以考虑一下 buildkit。

这里,我也会列出一些其他的工具,它们有各自的特定用途,但不是我的首选:

Source-To-Image(S2I):这是一个不使用 Dockerfile,直接根据源代码构建镜像的工具包。这个工具在简单的预期场景和工作流中表现良好,但如果你需要多一些自定义,如果你的项目的结构不符合预期,那么它就变得非常烦人和笨拙。如果你对 Docker 不太满意,或者你在 OpenShift 集群上构建镜像,则可以考虑使用 S2I,因为使用 S2I 构建镜像是它的一个内置功能。

Jib:这是一款由 Google 开发的工具,专门用于构建 Java 镜像。它提供了 Maven 和 Gradle 插件,可以让你轻松地构建镜像,而无需在意 Dockerfile。

Bazel:这也是一款由 Google 开发的工具。它不仅可用于构建容器镜像,而且是一个完整的构建系统。如果你只是想构建镜像,那么使用 Bazel 可能会有点大材小用,但绝对是一种不错的学习体验,如果你愿意,可以先从 rules_docker 着手。



容器运行时


最后我们来说说负责运行容器的容器运行时。容器运行时是整个容器生命周期的一部分,除非你对速度、安全性等有一些非常特殊的要求,否则请不要乱动它。

看到这里,如果你感到厌倦了,则可以跳过这一部分。但是,如果你想了解一下在容器运行时方面,都有哪些选择,则可以看看下面这些:

runc 是一款流行的容器运行时,且符合 OCI 容器运行时规范。Docker(通过containerd)、Podman 和 CRI-O 都在使用它,因此无需我多言。它几乎是所有容器引擎的默认设置,因此即便你在阅读本文后抛弃了 Docker,很可能仍然会使用 runc。

除了 Docker,我们还有哪些选择?_Linux_03

runc 的另一种替代品是 crun。这是一款由红帽开发的工具,全部用 C 语言编写(runc 是用 Go 编写的),所以它比 runc 更快,内存效率更高。由于它也是兼容 OCI 的运行时,所以如果你想试试看的话,应该能很快上手。虽然它现在还不是很流行,但是它即将作为 RHEL 8.3 版本的备选 OCI 运行时,出现在技术预览中,而且考虑到它是红帽的产品,所以最终很可能会成为 Podman 或 CRI-O 的默认配置。

说到 CRI-O,前面我说过,它并不是容器引擎,而是容器运行时。这是因为 CRI-O 没有推送镜像之类的功能,但这些功能是容器引擎应该具备的。CRI-O 内部使用 runc 来运行容器。你不应该在自己的机器上尝试使用这个运行时,因为它的设计就是 Kubernetes 节点上的运行时,而且它是“Kubernetes 所需的唯一的运行时”。因此,除非你要建立 Kubernetes 集群,否则就不应该考虑 CRI-O。

最后一个是 containerd,它是云原生计算基金会即将推出的一个项目。它是一个守护进程,可作为各种容器运行时和操作系统的 API 接口。它后台依赖于 runc,它是 Docker 引擎的默认运行时。Google Kubernetes Engine(GKE)和 IBM Kubernetes Service(IKS)也在使用它。它是 Kubernetes容器运行时接口的一个实现(与 CRI-O 一样),因此是 Kubernetes 集群运行时的理想选择。



镜像的检查与分发


最后一部分内容是镜像的检查与分发,主要是为了替代 docker inspect,并增加在远程仓库之间复制镜像的能力(可选)。

在这里,我要提到的唯一可以完成这些任务的工具是 Skopeo。它由红帽开发,是 Buildah、Podman 和 CRI-O 的附属工具。除了基本的 skopeo inspect(Docker 有相应的命令),Skopeo 还可以通过 skopeo copy 令来复制镜像,因此你可以直接在远程仓库之间复制镜像,无需将它们拉取到本地。如果你使用本地仓库,那么这个功能也可以作为拉取/推送。

另外,我还想提一下 Dive,这是一款检查、探索和分析镜像的工具。它更加人性化,提供了更加方便阅读的输出,而且还可以更深入地挖掘镜像,并分析和测量镜像的效率。此外,它也很适合在 CI 管道中使用,用于衡量你的镜像是否“足够高效”,或者换句话说,是否浪费了太多空间。



总结


本文的目的并不是要说服你完全抛弃 Docker,而是为了向你展示构建、运行、管理和分发容器及其镜像的整个过程以及所有备选工具。每个工具(包括 Docker)都有其优缺点,因此,评估哪些工具最适合你的工作流程和情况才是最重要的,希望本文能在这方面为你提供一些帮助。