向Docker告别的时候到了

27,766

在容器的远古时期(大约4年前),Docker是容器游戏中仅有的参与者。但是现在情况不一样了,Docker不再是唯一的一个了,只是另一个容器引擎而已。Docker允许我们构建,运行,拉取,上传,查看容器镜像,但是对每一项任务都有其他可以比Docker做得更好的工具。因此,让我们看看现在的情况,卸载(只是可能)并且忘记Docker的全部信息。

不过,为什么不使用Docker?

如果你是一名Docker的老鸟,我想即使考虑使用不同的工具,也需要一些说服自己的理由。那么,理由就在这里:

首先,Docker是一个尝试做所有事情的工具,通常来说这并不是最好的方式。大多数情况下,最好选择一种专门只做一件事,并且能把这件事做得非常好的工具。

如果你担心切换到不同的工具后,不得不学着使用不同的CLI,不同的API或者通常不太一样的概念,那么这不会成为一个问题。选择接下来文章中的任何工具都是完全无缝的,因为它们(包括Docker)都遵循了OCI(Open Container Initiative)的相同规范。这个规范中包含了容器的运行时,分布式,镜像,涵盖了容器需要的所有特性。

由于OCI的存在,你可以选择一套最适合你的工具集,与此同时,仍然可以使用相同的API和CLI命令,就像Docker一样。

所以,如果你想尝试新的工具,接下来我们比较一下Docker和它的竞争对手都有哪些优缺点和特性,看看是不是有必要考虑放弃Docker,而使用一些新的亮瞎眼的工具。

容器引擎

在比较Docker和其他亮瞎眼的工具时,我们需要将其分解为组件。首先我们要讨论的是容器引擎。容器引擎是一种可以提供操作镜像和容器用户接口的一种工具,有了它你就不需要处理SECCOMP机制或者SELinux策略等一系列的事情。它的工作还包括从远程仓库中拉取镜像并将其扩展到硬盘。它看起来也运行容器,但实际上它的工作是创建容器清单和带有镜像层的目录。然后将他们传递到容器运行时,就像runc或crun(稍后讨论)。

现在有许多可用的容器引擎,但Docker众多的竞争中最突出的是Red Hat开发的Podman。和Docker不同的是,Podman不需要守护进程来运行,也不需要root权限,这是Docker长期以来关注的问题。Podman不仅可以运行容器,还可以运行pod。如果你不熟悉pod的概念,pod就是Kubernetes中的最小可部署计算单元。它由一个或多个容器组成执行任务。这使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 minutes ago  Up 5 minutes ago         cool_albattani      211eaecd307b  mypod
a901868616a5  k8s.gcr.io/pause:3.2                                  6 minutes ago  Up 5 minutes ago         211eaecd307b-infra  211eaecd307b  mypod

最后,Podman提供来和Docker完全一样的CLI命令,你只需要将docker重命名为podman即可。

除了Docker和Podman,还有其他的容器引擎,但我认为它们都是穷途末路,或者不适合本地开发。但想要有一个完整的画面,我们至少要提到还有哪些:

  1. LXD——LXD是LXC(Linux容器)的容器管理器(守护进程)。该工具提供了运行系统容器的能力,这些系统容器提供了类似于VM的容器环境。它位于非常狭窄的空间中,没有许多用户,所以除非有具体的用例,你最好使用Docker或Podman。
  2. CRI-O——当你通过google查询什么是CRI-O时,你可能会发现它被描述成容器引擎。不过,它实际上是容器运行时。它是专门为Kubernetes运行时而构建的,而不是用户使用的终端。
  3. rkt——rkt是由CoreOS开发的容器引擎。这里提到了这个项目仅仅是为了画面的完整性,因为这个项目已经结束了,开发也停止了,因此它不该被使用。

构建镜像

对于容器引擎来说,Docker只有一种选择。但是当构建镜像时,我们可以有很多的选择。

首先介绍一下Buildah。这是Red Hat开发的另一个工具,它和Podman的组合更配。如果你已经安装了Podman,可能会注意到Podman的构建命令实际上是伪装的Buildah,因为它的二进制文件包含在Podman中。

作为它的特性,它和Padman有相同的路线——它是不需要守护进程和root权限,并产生OCI的镜像,所以它保证你的镜像能与Docker构建相同的方式运行。它还能从Dockerfile或者Containerfile构建镜像,这是两种相同的东西,但是有不同的名称。除此之外,Buildah还对镜像层提供了更好的控制,允许你在单一层中提交更改。但与Docker的区别是,由Buildah构建的镜像是属于特定用户的,因此你可以只列出来自己构建的镜像。

现在考虑到Buildah已经包含在了Podman CLI中,你可能会问,为什么还要使用单独的Buildah CLI?Buildah CLI是Podman中包含的命令的超集,所以你可能不需要接触Buildah CLI,通过使用它你可能会发现一些额外的特性(有关Podman和Buildah之间差异的细节,请参阅下面的文章)。

综上所述,我们来看一个演示:

~ $ 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  # equivalent to "RUN apk add --update --no-cache python3"
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz
...

~ $ buildah commit alpine-working-container my-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中是等价的。

下一个要介绍的是谷歌的Kaniko。Kaniko也是从Dockerfile构建镜像,与Buildah类似,它也不需要守护进程。Kaniko与Buildah的主要区别是Kaniko更专注于在Kubernetes中构建镜像。

Kaniko意味着通过使用gcr.io/kaniko-project/executor可以作为镜像被运行,这对于Kubernetes来说是有意义的,但是对于本地构建来说不太方便,并且在一定程度上违背了目的,因为你需要使用Docker来运行Kaniko镜像来构建你的镜像。这等于说,如果你在寻找在Kubernetes集群中构建镜像的工具,那么Kaniko可能是一个不错的选择,因为它是不需要守护进程,而且更安全。

以我的个人经验来看——我在Kubernetes/OpenShift集群中使用了Kaniko和Buildah来构建镜像,都能很好地完成工作,但在使用Kaniko时,我遇到了一些将镜像导入registry时的随机构建崩溃和失败。

第三个竞争者是buildkit,也被称为是下一代docker。它是Moby项目的一部分,可以使用DOCKER_BUILDKIT=1 启用Docker。那么,这到底会带来什么呢?它引入了许多改进和很牛的特性,包括并行构建步骤、跳过未使用阶段、更好的增量构建和无root构建。但另一方面,它仍然需要运行守护进程。所以,如果你不想摆脱Docker,但是想要一些新的特性和更好的改进,那么使用buildkit可能是最好的选择。

这里我们也有一些值得被提到的具体用例,但它们并不是我的首选:

  1. Source-To-Image (S2I) 是一个不使用Dockerfile直接从源代码构建镜像的工具包。这个工具在简单的、预期的场景和工作流中工作得很好,但是如果你需要很多的定制,或者项目没有预期的布局,那么它很快就会变得很烦人和笨拙。如果你对Docker还不是很有信心,或者在OpenShift集群上构建映像,你可能会考虑使用S2I,因为使用S2I构建是一个内置特性。
  2. Jib是谷歌开发的另一个工具,专门用于构建Java镜像。它包括Maven和Gradle插件,可以轻松地构建镜像,而不会干扰dockerfile。
  3. 最后但并非不重要的是Bazel,它是谷歌的另另另另一个工具。它不仅用于构建容器镜像,而是一个完整的构建系统。如果你只是想构建一个镜像,那么钻研Bazel可能有点过头,但绝对是一个很好的学习体验,所以如果你愿意,rules_docker部分是一个很好的起点。

容器运行时

最后一个难题是容器运行时,它负责运行容器。容器运行时是整个容器生命周期的一部分,除非你对su速度,安全有非常具体的要求,否则你可能不会修改它。所以,如果读到这里你觉得厌倦了,那么你可以跳过这一部分。另一方面,如果你只是想知道有哪些选择,具体如下:

runc是基于OCI容器运行时规范而创建的最流行的容器运行时。Docker(通过containerd)、Podman和CRI-O都在使用它,所以几乎所有东西都希望使用LXD。可以添加的东西不多,它是所有东西的默认值,所以即使你在阅读本文后放弃Docker,也很可能仍然会使用runc。

runc的另一种类似的方法是crun。这是Red Hat开发的工具,完全用C编写(runc是用Go编写的)。这使得它比runc更快,内存效率更高。考虑到它也是OCI兼容的运行时,如果你想自检的话,应该可以很容易地切换到它。尽管它现在还不是很流行,但在预览版中,它将作为一个替代OCI运行时的RHEL 8.3版本,考虑到它是Red Hat的产品,我们可能最终会看到它作为Podman或CRI-O的默认版本。

说到CRI-O。前面我说过,CRI-O不是一个真正的容器引擎,而是容器运行时。这是因为CRI-O不包括像推送镜像这样的特性,而这正是你所期望的容器引擎特性。作为运行时的CRI-O在内部使用runc运行容器。你不应该在机器上尝试使用这个运行时,因为它被构建为用于Kubernetes节点上的运行时,您可以看到它被描述为“Kubernetes需要的所有运行时,仅此而已”。因此,除非您正在设置Kubernetes集群(或OpenShift集群),否则您可能不应该接触这个。

本节的最后一个内容是containerd,它是CNCF的一个毕业项目。它是一个守护进程,充当各种容器运行时和操作系统的API。在后台它依赖于runc,是Docker引擎的默认运行时。它也被谷歌Kubernetes引擎(GKE)和IBM Kubernetes服务(IKS)使用。它是Kubernetes容器运行时接口的一个实现(与CRI-O相同),因此它是Kubernetes集群运行时的一个很好的候选对象。

镜像检测和分布式

容器栈的最后一部分是图像的检测与分布式。这有效地替代了docker检查,还增加了远程registry之间复制/镜像镜像的能力。

这里我要提到的唯一可以完成这些任务的工具是Skopeo。它由Red Hat制作,是Buildah, Podman和CRI-O的配套工具。除了我们都从Docker中知道的基本的skopeo检查之外,skopeo还能够使用skopeo copy复制镜像,它允许您在远程registry之间镜像映像,而无需首先将它们拉到本地registry。如果您使用本地registry,此功能也可以作为拉取/下载。

另外,我还想提一下Dive,这是一个检查、探索和分析镜像的工具。它对用户更友好一些,提供了更可读的输出,可以更深入地挖掘你的镜像,并分析和衡量其效率。它也适合在CI管道中使用,它可以测量你的镜像是否“足够高效”,或者换句话说——它是否浪费了太多空间。

结论

本文的目的并不是要说服你完全抛弃Docker,而是向您你展示构建、运行、管理和分发容器及其映像的整个场景和所有选项。包括Docker在内的每一种工具都有其优缺点,评估哪一组工具最适合你的工作流和用例是很重要的,我希望本文能在这方面帮助你。

欢迎关注我的公众号,如果你有喜欢的外文技术文章,可以通过公众号留言推荐给我。