多平台容器镜像构建就看这一篇
文章目录
前言
愿景与现实
早在1995年,就有“write once and run anywhere”(WORA,编写一次即可在任何地方运行)用于描述 Java 应用程序。时过20年,Docker 高声喊出了自己的口号——“Build Once, Run Anywhere”(一次构建,随处可用)。
愿望是美好的,然而,现实总比理想骨干。 Linux、Windows 这些不同的操作系统拥有不同的系统 API; x86、Arm、IBM PowerPC 这些不同的硬件平台的指令集不同,某些同平台的硬件甚至拥有不同的专用指令集用于加速应用。一次构建,随处可用面临着巨大的挑战,要构建能够在不同操作系统、不同硬件平台的运行的应用程序,仍然需要工程师们针对具体的操作系统和硬件平台进行海量的移植工作。
Why and How
既然多平台的支持这么麻烦、充满挑战,我们是不是可以放弃支持?然而随着国产化大潮和 IoT 物联网的来领,我们编写的应用程序不再仅仅会再传统的 Intel x86 兼容的服务器上运行,新时代的工程师们不得不面对更多的硬件平台,放弃多平台的支持无疑是放弃更宽广的未来。多平台的支持,势在必行。
我们正处在一个波澜壮阔的大时代中,新技术、新工具在一次次的迭代升级,不断从Proposal (提议)到 Prototype(原型),再逐渐的实用化。
虚拟化技术使得我们可以做到模拟其它硬件平台;Docker 等容器技术打破混乱,让开发、编译、运行环境一致化;Golang、Rust 这样原生支持多系统多平台的编程语言屏蔽大量底层差异,降低跨平台应用的开发难度。这一系列前人智慧火花汇聚到一起,发生了奇妙的反应——WORA 真正变的触手可及,梦想的阳光已经照进现实。
本篇章会大量分析技术原理及实现细节,对于希望快速 GET 可执行方案的同学,可以直接跳转到 可执行方案回顾 查看。
如何支持多平台
要了解容器镜像是如何支持多平台的,那我们需要仔细聊聊 Manifest。使用过容器技术的同学都知道,我们运行容器所使用的镜像是由多层构成的,而这些层的清单和其它容器信息共同存放在 Manifest 当中。
Manifest
我们通常使用的容器镜像是x86平台的,执行 docker manifest inspect ccr.ccs.tencentyun.com/library/alpine
命令可以查看镜像 ccr.ccs.tencentyun.com/library/alpine
的 Manifest 内容是一个 JSON 对象(如代码段-01所示)。各个字段的解释如下:
mediaType
字段声明这是一个V2 Manifes。schemaVersion
版本。config
镜像配置信息。layers
镜像层信息。
|
|
显而易见的是,Manifest 当中并没有任何字段描述镜像的平台信息。那应该怎么样支持多平台呢?
我们可以设想一个简单粗暴的,无视镜像的平台,强行把交叉编译出来的其它平台的二进制程序添加到镜像内,使用 Repository 名称或者 Tag 名称来区分不同平台的镜像,例如 coredns/coredns:coredns-arm64。在使用的时候,人工或者通过脚本判断应该拉取那个镜像。
Schema 2
Ohhhhh,这当然可以跑起来,但是难免太挫了吧?事实上,早在2015年底 Docker 社区的 manifest v2.2 规格文档(也叫 Schema 2,参见 https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md )中就提及了多平台镜像,该功能通过 manifest list(也叫做 fat manifest)
引用多个不同平台镜像的 Manifest 实现。
首先让我们看看 manifest
是什么样的,执行 docker manifest inspect alpine
命令可以查看Docker Hub 上的多平台镜像 alpine
的 Manifest。
|
|
可以看出来 manifest list
是一个 JSON 数组,数组当中应用了不同平台镜像的 Manifest。所以,推送多平台镜像时,我们需要先分别推送不同平台的镜像层;然后创建 manifest list
, 再引用平台镜像的 Manifest,最后把 manifest list
上传到 Registry 服务。而拉取镜像时,客户端应当设置 HTTP 的请求头字段 Accept
值为 application/vnd.docker.distribution.manifest.v2+json
和application/vnd.docker.distribution.manifest.list.v2+json
,然后检查服务端返回的响应头字段Content-Type
判断是旧镜像格式,新镜像格式或者时镜像清单。
抛开规格文档来说,只要我们使用的 Registry 服务的 Distribution 版本不低于 v2.3,Docker CLI 版本不低于 v1.10 就能过支持多平台镜像功能。
构建多平台镜像
要构建多平台的容器镜像,我们需要确保容器基础镜像和应用程序的代码或者二进制都是目标平台的。
程序代码
一些编程语言的编译器能够为其它平台编译二进制文件,最为著名的包括 Golang 和 Rust。我们将使用 Golang 编写一个演示用 web 程序——通过 HTTP 访问查看 web 服务程序的操作系统、硬件平台等信息。具体代码如代码段-03 所示。
|
|
我们在 MacOS 上使用 go run 运行代码段-01, httpie 工具访问本机:9090
端口,将会看见如下信息。
代码准备好了,现在我们有两种构建方法:手动编译,使用 docker build
构建镜像;使用 docker buildx 工具自动化编译构建。
手动编译构建
前置条件
- Dockerd 启用
experimental
我们需要在 Docker daemon 配置文件中配置 "experimental": true
开启实验性功能:
|
|
修改 Docker daemon 配置需要重启服务是配置生效:
|
|
使用docker version
命令验证是否成功开启实验性功能,正常可以看到Server: Docker Engine
中有Experimental: true
表明开启成功:
|
|
报错:
1
docker manifest annotate is only supported on a Docker cli with experimental cli features enabled
需要执行
export DOCKER_CLI_EXPERIMENTAL="enabled"
交叉编译
在我们的 Golang 代码中没有使用 CGO 的时候,通过简单设置环境变量就能够交叉编译出其它平台和操作系统上能够执行的二进制文件。其中:
GOARCH
用于指定编译的目标平台,如amd64
、arm64
、riscv64
等平台。GOOS
用于指定编译的目标系统,如darwin
、linux
。
本篇中,我们构建能够在 Linux 发行版中执行的容器镜像,所以编译目标系统环境变量GOOS
统一设置为linux
。执行代码段0-4中的脚本构建出二进制文件备用。
|
|
构建各个平台的镜像
首先,我们编写一个 Dockerfile用于构建镜像。
|
|
然后,分别构建不同平台的镜像,可以使用如代码段-05的脚本帮助构建。
|
|
创建 Manifest List
我们使用docker manifest
子命令管理 manifest list。其中,docker manifest create
子命令用于在本地创建一个 manifest list。该命令需要指定待 manifest list 地址和一系列的 manifests。例如需要创建包含amd64
和 arm64
两个平台镜像的 manifest list,则命令如:
|
|
docker manifest create
命令的详细帮助信息如下所示:
|
|
我们按照上述方法创建出来的 manifest list 中并没有说明其中的 manifest 是什么操作系统和平台的,docker manifest annotate
命令用于注释创建出来的 manifest list。例如注释某个 manifest 是 linxu
系统 arm64
平台的,则命令:
|
|
docker manifest annotate
命令的详细帮助信息如下所示:
|
|
注意:
- 创建 manifest list 清单的过程中会检查远端仓库中 manifests 是否存在,所以我们必修提前推送镜像到远端。如果远端仓库是不安全的,在创建的过程中需要添加参数
--inseure
。 - 使用
docker manifest annotate
注释 manifest list 的时候不需要使用--insecure
。
为了方便使用,我们可以使用下述代码段-05中的脚本创建 manifest list。
|
|
推送 manifest list
当我们完成 manifest list 的创建工作后,它还是存储在本地的。这时候,还需要推送到远端的镜像仓库。与推送普通镜像不同,推送 manifest list 需要使用 docker manifest push
命令进行。如果我们要推送 kofj/multi-demo
这个 manifest list,则命令如:
|
|
buildx 自动构建
软件依赖
Docker >= 19.03: 自该 Docker 版本包含 buildx。
Linux kernel >= 4.8: 自该Linux内核版本 binfmt_misc 支持 fix-binary (F) flag。fix_binary 标志允许内核在容器或chroot内使用binfmt_misc注册的二进制格式处理程序,即使该处理程序二进制文件不是该容器或chroot内可见的文件系统的一部分。
binfmt_misc file system mounted: 需要挂载binfmt_misc文件系统,以便用户空间工具可以控制此内核功能,即注册和启用处理程序。
Docker Desktop >= 2.1.0 如果是使用的 Docker Desktop。
Environment | Docker 安装包 | Kernel | binfmt-support | (F) Flag |
---|---|---|---|---|
需求 | >= 19.03 | >= 4.8 | >= 2.1.7 | yes |
Ubuntu: | ||||
18.04 (bionic) | 17.12.1 docker.io | 4.15.0 | 2.1.8 | yes |
19.04 (disco) | 18.09.5 docker.io | 5.0 | 2.2.0 | yes |
19.10 (eoan) | 19.03.2 docker.io | 5.3 | 2.2.0 | yes |
20.04 (focal) | 19.03.2 docker.io | 5.5 | 2.2.0 | yes |
Debian: | ||||
9 (stretch) | - | 4.9.0 | 2.1.6 | no |
10 (buster) | 18.09.1 docker.io | 4.19.0 | 2.2.0 | yes |
11 (bullseye/testing) | 19.03.4 docker.io | 5.4 | 2.2.0 | yes |
腾讯云 | ||||
Ubuntu 16.04 (xenial) | 18.09.7 docker.io | 4.4.0 | 2.1.6-1 | no |
Ubuntu 18.04 (bionic) | 19.03.6 docker.io | 4.15.0 | 2.1.8-2 | yes |
亚马逊 EC2: | ||||
Ubuntu 16.04 (xenial) | 18.09.7 docker.io | 4.4.0 | 2.1.6 | no |
Ubuntu 18.04 (bionic) | 18.09.7 docker.io | 4.15.0 | 2.1.8 | yes |
Travis (谷歌 GCP): | ||||
Ubuntu 14.04 (trusty) | 17.09.0 docker-ce | 4.4.0 | 2.1.4 | no |
Ubuntu 16.04 (xenial) | 18.06.0 docker-ce | 4.15.0 | 2.1.6 | no |
Ubuntu 18.04 (bionic) | 18.06.0 docker-ce | 4.15.0 | 2.1.8 | yes |
Github Actions (微软 Azure): | ||||
Ubuntu 16.04 (xenial) | 3.0.8 moby-engine | 4.15.0 | 2.1.6 | no |
Ubuntu 18.04 (bionic) | 3.0.8 moby-engine | 5.0.0 | 2.1.8 | yes |
配置 Buildx
buildx 从 19.03 开始与 Docker CE 捆绑发布,但是需要我们在 Docker CLI 上启用实验性功能开开启。可以通过两种方式启用它:
- 将
"experimental": "enabled”
添加到 Docker CLI 的配置文件~/.docker/config.json
。 - 另外一种方法时设置环境变量
DOCKER_CLI_EXPERIMENTAL=enabled
。
使用 Docker Desktop 的同学可以通过 UI 菜单 Preferences
→ Command Line
进入 Docker CLI 配置界面,通过Switch 开关 Enable experimental features
启用实验性功能。
如果需要使用最新版本的 buildx,可以从 https://github.com/docker/buildx/releases/latest 下载最新的二进制发行版,并将其复制到~/.docker/cli-plugins
文件夹中,重命名为docker-buildx
,然后更改执行权限:
|
|
最后让我们验证 buildx 是否已经可用了:
|
|
配置 binfmt_misc
QEMU 是一个很棒的开源项目,它可以模拟很多平台。将 QEMU 和 Docker 结合起来使用能使得我们更容易的构建跨平台的容器镜像。集成 QEMU依赖于 Linux 内核功能 。Linux 内核中的 binfmt_misc
功能可以使得内核识别任意类型的可以执行文件格式,并传递到特定的用户空间应用程序和虚拟机(https://zh.wikipedia.org/wiki/Binfmt_misc)。当 Linux 遇到一种无法识别的可执行文件格式(比如说其它平台的可执行文件格式)时,它会检查有没有配置任何“用户空间应用程序”用于处理它。如果检测到了,就将可执行文件传递给该应用程序。
为此,我们需要在内核当中注册其它平台的可执行文件格式。
对于使用 Docker Desktop(MacOS 和 Windows 上都是)的同学,因为默认配置了 binfmt_misc
,可以跳过这一步。而使用 Linux 发行版操作系统的同学则需要自行安装配置 binfmt_misc
,以便能够非原生的其它平台的镜像。
要在宿主机上执行其它 CPU 平台的指令,需要安装 QEMU 模拟器。因为程序执行时会在当前程序可见的文件系统中查找动态库,而在容器或chroot环境中注册的处理程序在其它的 cgroup namespace 中可能无法找到,所以需要静态编译连接的QEMU。同时,我们需要安装一个包含足够新的update-binfmts二进制文件的包,以便能够支持fix-binary(F)标志,并在注册QEMU模拟器时实际使用,这样才能结合 buildx 一起镜像跨平台构建。
QEMU 和 binfmt_misc 支持工具可以通过宿主机或者Docker 容器镜像安装。但是,使用Docker镜像安装配置能让事情变得更加简单。镜像 docker/binfmt
中包含QEMU二进制文件和在binfmt_misc中注册QEMU的安装脚本。
|
|
执行完后,我们验证下是否注册成功了。成功注册后,/proc/sys/fs/binfmt_misc
目录中会有多个qemu-
前缀的文件。查看 /proc/sys/fs/binfmt_misc/qemu-aarch64
文件内容,可以看到 falgs 标志为 OCF
,说明这个处理程序是通过 (F)标志注册的,能够正常的结合 buildx 完成跨平台构建。
|
|
使用 buildx 构建
前置依赖注备好后,我们终于可以使用 buildx 构建多平台镜像了。与其它方案不同的是,使用 buildx 可以让我们不必改动 dockerfile。
Buildx 始终使用 BuildKit 引擎构建镜像,不需要配置环境变量DOCKER_BUILDKIT=1
。BuildKit 可以很好的用于多个平台的构建,而不仅适用于我们当前构建镜像时所使用的平台和操作系统。进行构建时,使用 --platform
标志可以用于指定构建输出的目标平台(例如 linux/amd64
,linux/arm64
,linux/riscv64
)。
首先,我们先准备好 Dockerfile 文件:
|
|
然后,让我们尝试下运行 buildx。
|
|
居然报错 error: auto-push is currently not implemented for docker driver, please create a new builder instance
了!别担心,这是因为 Docker 默认的 builder 是不支持多平台构建的。我们可以通过 docker buildx ls
查看当前节点上的 builder 有哪些。
|
|
为了使用多平台构建功能,我们需要新建一个 builder,并设置当前 builder 为新建的。
|
|
现在,让我们再次执行 buildx,看着一切向着期待的方向发展了。
注意事项
⚠️注意^1^:到目前位置,buildx支持 linux/amd64, linux/386, linux/arm/v7, linux/arm/v6, linux/arm64, linux/ppc64le, linux/s390x
。所以 docker/binfmt
镜像仅注册了 arm、ppc64le 和 s390x 的处理程序。如果你需要构建、运行 RISC-V 平台的容器镜像,建议使用 multiarch/qemu-user-static 镜像镜像配置。
|
|
⚠️注意^2^:在 软件依赖 中我们提到需要 Linux 内核版本 >= 4.8.0;如果在内核版本为 3.10.0 的系统(比如 CentOS)上运行 docker/binfmt
,会出现报错 Cannot write to /proc/sys/fs/binfmt_misc/register: write /proc/sys/fs/binfmt_misc/register: invalid argument
,这是由于内核不支持 (F)标志造成的。出现这种情况,建议您升级系统内核或者换使用较高版本内核的 Linux 发行版。
小结
多年前,大规模部署应用程序是一项非常耗费人力、财力、时间,还需要大量技能和技巧的事务,工程师们还需要应对应用程序所运行的每一台服务器的环境差异。这对大公司而言是个极其沉重的负担,小公司更是无力应对。
正如多年前人们无法想象大规模部署复杂的应用程序只需要一个 kubectl create
命令,不久前我们也不会想到构建多平台的容器镜像只需要一个 docker buildx build
。但是,我们还有更加广阔的想象空间,自动化流程、更多平台的支持、更智能简单的工具,你能想到的都有可能在不久的将来变成现实。
技术的发展进步,不断降低了生产活动中社会平均劳动时间,提升了生产力,能够释放劳动者去做更多有益的探索。让我们不断学习、拥抱、应用新技术,在时代的浪潮中勇往直前。
可执行方案回顾
- 确保使用的 Linux 发行版内核>=4.8.0(推荐使用 Ubuntu 18.04 以上的 TLS 发行版),且 Docker >= 19.03;
- 启用Docker CLI 实验性功能:
export DOCKER_CLI_EXPERIMENTAL=enabled
; - 配置其它平台的模拟器:
docker run --privileged docker/binfmt:66f9012c56a8316f9244ffd7622d7c21c1f6f28d
; - 新建 Docker builder 实例支持多平台构建:
docker buildx create --use --name mybuilder
; - 在项目目录中执行构建:
docker buildx build --platform linux/amd64,linux/arm64,linux/arm -t ccr.ccs.tencentyun.com/${YOUR_NAMESPACE}/multi-arch:2020-10-12 . --push
。
参考资料
- https://docs.docker.com/buildx/working-with-buildx/
- https://mirailabs.io/blog/multiarch-docker-with-buildx/
- https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/
- https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list
- https://github.com/docker/docker.github.io/blob/master/registry/compatibility.md#manifest-push-with-docker-110
- https://nexus.eddiesinentropy.net/2020/01/12/Building-Multi-architecture-Docker-Images-With-Buildx/
- https://www.docker.com/blog/multi-platform-docker-builds/
- https://blog.51cto.com/yangzhiming/2459368
- https://medium.com/@arunrajeevan/handling-multi-platform-deployment-using-manifest-file-in-docker-317736a2a039#####
文章作者 疯魔慕薇
上次更新 2020-10-13