写点什么

如何构建多架构 Docker 镜像?

2019 年 11 月 28 日

如何构建多架构 Docker 镜像?

在每个黑客的职业生涯中总有这么一个时刻需要为另一种 CPU 架构编译应用程序。这种场景可能出现在为树莓派项目编译应用程序,为嵌入式设备创建自定义镜像,或者让自己的软件支持不同平台。亦或是,我们只是想知道这个过程是怎么样的,好奇最终汇编代码和桌面电脑上无处不在的 x86-64/amd64 架构汇编有何区别。


不论是哪种原因,通常我们都需要整理好行装进行一段朝圣之旅。但是这个旅程不是登上孤独的山顶,而是通向地狱深渊,是一段从开发应用程序的阳光平原走向计算机体系结构的黑暗洞穴之旅:底层系统和嵌入式变成带来的难以捉摸的世界。介于这次跋涉的前景堪忧,大部分黑客最终通过 Ctrl+Z 结束了旅程,回到了地面,一边喘气一边警告同伴交叉编译、QEMU 和 chroot 的恐怖之处。


好了,我可能有点夸张了。但是真相是为其他 CPU 架构构建应用程序没有那么直截了当。多亏了 Docker 19.03 带来实验性的插件,让多架构构建比以往要方便很多。


为了理解 Docker 对多架构构建支持的重要性,首先我们需要了解如何为陌生架构构建应用程序。


背景:为陌生架构编译应用的方法

注:读者如果对本节概念已经了解,或者只是想知道如何构建镜像,可以跳过本节。


让我们快速了解下当前对于为陌生架构编译应用程序的方法。


方法 1:直接在目标硬件上构建

如果我们可以访问目标架构硬件,同时操作系统上有我们所需的所有构建够据,那么就可以直接在硬件上编译应用程序。


例如,对我们特定场景下构建多架构 Docker 镜像,可以在树莓派上安装 Docker 运行时环境,然后和在开发机上一样,直接在上面通过应用程序的 Dockerfile 构建镜像。该方法是可行的,因为树莓派的官方操作系统 Raspbian 支持本地安装 Docker。


但是,如果我们没法办法方便的访问目标硬件呢?我们可以在开发机器上直接构建非本地架构的应用程序吗?


方法 2:模拟目标硬件

还记得和 16 位任天堂游戏机一起的快乐时光吗?当时我只是一个小孩子,但是当我长大一点之后,我发现对诸如《超级玛丽》和《时空之轮》等经典游戏非常怀念。不过我没有机会拥有一台超级任天堂游戏机,但是多亏了像ZSNES这样的模拟器,让我能回到过去,在 32 位个人电脑上体验这些经典游戏带来的乐趣。


通过模拟器,我们不仅能够玩电子游戏,还能够构建非本地二进制文件。当然这里不是使用 ZSNES,而是使用更加强大更灵活的模拟器:QEMU。QEMU 是一个自由且开源的模拟器,支持许多通用架构,包括:ARM、Power-PC 和 RISC-V。通过运行一个全功能模拟器,我们可以启动一个可以运行 Linux 操作系统的通用 ARM 虚拟机,然后在虚拟机中设置开发环境,编译应用程序。


但是,如果仔细思考下,一个全功能虚拟机有一些浪费资源。在该模式下,QEMU 会模拟整个系统,包括诸如定时器、内存控制器、SPI 和 I2C 总线控制器等硬件。但是大部分情况下,我们编译应用程序不会关心以上所提到的硬件特性。还能更好么?


方法 3:通过 binfmt_misc 模拟目标架构的用户空间

在 Linux 系统上,QEMU 有另外一种操作模式,可以通过用户模式模拟器来运行非本地架构的二进制程序。该模式下,QEMU 会跳过方法 2 中描述的对整个目标系统硬件的模拟,取而代之的是通过 binfmt_misc 在 Linux 内核注册一个二进制格式处理程序,将陌生二进制代码拦截并转换后再执行,同时将系统调用按需从目标系统转换成当前系统。最终对于用户来说,他们会发现可以在本机运行这些异构二进制程序。


通过用户态模拟器和 QEMU,我们可以通过轻量级虚拟化(chroot或者容器)来安装其他 Linux 发行版,并像在本地一样编译我们需要的异构二进制程序。


下面我们会看到这将会是构建多架构 Docker 镜像的可选方式。


方法 4:使用交叉编译器

最后,我们还有一种在嵌入式系统社区标准的做法:交叉编译。


交叉编译器是一个特殊的编译器,它运行在主机架构上,但是可以为不同的目标架构生成的二进制程序。例如,我们可以有一个 amd64 架构的 C++交叉编译器,目标架构是一个 aarch64(64 位 ARM)的嵌入式设备(例如一个智能手机或者其他东西)。基于这种方式的一个现实中的例子是,世界上数十亿安卓设备都使用这种方式来构建软件。


从性能上考虑,这种方式拥有和直接在目标硬件上构建(方法 1)相同的效率,因为它没有运行在模拟器上。但是交叉编译的变数取决于使用的编程语言(对于Go语言就非常方便)。


搞糊涂了吗?对于 Docker 镜像来说会更复杂……

注意前面提到的所有编译方式都只是生成单一的应用程序二进制文件。对于现代容器来说,当我们引入 Docker 镜像的时候,不仅仅是关于构建单独的二进制文件,而是构建一整个异构容器镜像!这比之前说的要更加麻烦。


如果这些听上去都很痛苦,不要难过,因为构建非本地平台二进制程序本来就很痛苦。在此之上增加 Docker 带来的复杂度,看起来应该留给专家来处理。


感谢最新版本 Docker 运行时环境带来的实验性扩展,构建多架构镜像现在比以前方便多了。


构建多架构 Docker 镜像

为了能够更方便的构建多架构 Docker 镜像,我们可以使用最近发布的 Docker 扩展:buildx。buildx 是下一代标准 docker build 命令的前端,既我们熟悉的用于构建 Docker 镜像的命令。通过借助BuildKit的所有功能,buildx 扩展了表中 docker build 命令的功能,成为 Docker 构建系统的新后端。


让我们花几分钟看下如何使用 buildx 来构建多架构镜像。


步骤 1:开启 buildx

要使用 buildx,首先要确认我们的 Docker 运行时环境已经是最新版本 19.03。新版本中,buildx 事实上已经默认和 Docker 捆绑在一起,但是需要通过设置环境变量 DOCKER_CLI_EXPERIMENTAL 来开启。让我们在当前命令行会话中开启:


$ export DOCKER_CLI_EXPERIMENTAL=enabled
复制代码


通过检查版本来验证目前我们已经可以使用 buildx:


$ docker buildx versiongithub.com/docker/buildx v0.3.1-tp-docker 6db68d029599c6710a32aa7adcba8e5a344795a7
复制代码


可选步骤:从源码构建

如果要使用最新版本的 buildx,或者在当前环境下设置 DOCKER_CLI_EXPERIMENTAL 环境变量不生效(例如我发现在 Arch Linux 系统中设置无效),我们可以从源码构建 buildx:


$ export DOCKER_BUILDKIT=1$ docker build --platform=local -o . git://github.com/docker/buildx$ mkdir -p ~/.docker/cli-plugins && mv buildx ~/.docker/cli-plugins/docker-buildx
复制代码


步骤 2:开启 binfmt_misc 来运行非本地架构 Docker 镜像

如果读者使用的是 Mac 或者 Windows 版本 Docker 桌面版,可以跳过这个步骤,因为 binfmt_misc 默认开启。


如果使用是 Linux 系统,需要设置 binfmt_misc。在大部分发行版中,这个操作非常简单,但是现在可以通过运行一个特权 Docker 容器来更方便的设置:


$ docker run --rm --privileged docker/binfmt:66f9012c56a8316f9244ffd7622d7c21c1f6f28d
复制代码


通过检查 QEMU 处理程序来验证 binfmt_misc 设置是否正确:


$ ls -al /proc/sys/fs/binfmt_misc/total 0drwxr-xr-x 2 root root 0 Nov 12 09:19 .dr-xr-xr-x 1 root root 0 Nov 12 09:16 ..-rw-r--r-- 1 root root 0 Nov 12 09:25 qemu-aarch64-rw-r--r-- 1 root root 0 Nov 12 09:25 qemu-arm-rw-r--r-- 1 root root 0 Nov 12 09:25 qemu-ppc64le-rw-r--r-- 1 root root 0 Nov 12 09:25 qemu-s390x--w------- 1 root root 0 Nov 12 09:19 register-rw-r--r-- 1 root root 0 Nov 12 09:19 status
复制代码


然后,验证下指定架构处理程序已经启用,例如:


$ cat /proc/sys/fs/binfmt_misc/qemu-aarch64enabledinterpreter /usr/bin/qemu-aarch64flags: OCFoffset 0magic 7f454c460201010000000000000000000200b7mask ffffffffffffff00fffffffffffffffffeffff
复制代码


步骤 3:将默认 Docker 镜像构建器切换成多架构构建器

默认情况下,Docker 会使用旧的构建器,不支持多架构构建。


为了创建一个新的支持多架构的构建器,需要运行:


$ docker buildx create --use --name mybuilder
复制代码


验证新的构建器已经生效:


$ docker buildx lsNAME/NODE    DRIVER/ENDPOINT             STATUS   PLATFORMSmybuilder *  docker-container  mybuilder0 unix:///var/run/docker.sock inactivedefault      docker  default    default                     running  linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
复制代码


搞定。现在 Docker 会使用新的构建器,支持构建多架构镜像。


步骤 4:构建多架构镜像

好了,现在我们终于可以开始构建一个多架构镜像了。为了演示这个功能,我们需要一个示例应用。


让我们创建一个简单的 Go 应用程序,输出当前运行环境的架构信息:


$ cat hello.gopackage main
import ( "fmt" "runtime")
func main() { fmt.Printf("Hello, %s!\n", runtime.GOARCH)}
复制代码


让我们创建一个 Dockerfile 来容器化这个应用:


$ cat DockerfileFROM golang:alpine AS builderRUN mkdir /appADD . /app/WORKDIR /appRUN go build -o hello .
FROM alpineRUN mkdir /appWORKDIR /appCOPY --from=builder /app/hello .CMD ["./hello"]
复制代码


这是一个多阶段 Dockerfile,通过 Go 编译器构建我们的应用程序,然后将构建出来的二进制程序使用 Alpine Linux 镜像创建成最小镜像。


现在,让我们使用 buildx 来构建一个支持 arm、arm64 和 amd64 架构的多架构镜像,并一次性推送到 Docker Hub:


$ docker buildx build -t mirailabs/hello-arch --platform=linux/arm,linux/arm64,linux/amd64 . --push
复制代码


是的,就是这样。现在Docker Hub上我们有了支持 arm、arm64 和 amd64 架构的多架构 Docker 镜像。当我们运行 docker pull mirailabs/hello-arch 时,Docker 会根据机器的架构来获取匹配的镜像。


如果读者要问 buildx 是如何实现这个魔法的?好吧,在命令的背后,buildx 使用 QEMU 和 binfmt_misc 创建了三个 Docker 镜像(arm、arm64 和 amd64 架构每个创建一个)。当构建完成后,Docker 会创建一个清单,其中包含这三个镜像以及他们对应的架构。换句话说,“多架构镜像”实际上是一个清单,列举了每个架构对应的镜像。


步骤 5:测试多架构镜像

让我们来快速测试下多架构镜像,以确保它们都能够正常工作。由于我们已经设置了 binfmt_misc,因此在开发机器上已经能够执行任何架构的镜像了。


首先,列出每个镜像的散列值:


$ docker buildx imagetools inspect mirailabs/hello-archName:      docker.io/mirailabs/hello-arch:latestMediaType: application/vnd.docker.distribution.manifest.list.v2+jsonDigest:    sha256:bbb246e520a23e41b0c6d38b933eece68a8407eede054994cff43c9575edce96
Manifests: Name: docker.io/mirailabs/hello-arch:latest@sha256:5fb57946152d26e64c8303aa4626fe503cd5742dc13a3fabc1a890adfc2683df MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/arm/v7
Name: docker.io/mirailabs/hello-arch:latest@sha256:cc6e91101828fa4e464f7eddec3fa7cdc73089560cfcfe4af16ccc61743ac02b MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/arm64
Name: docker.io/mirailabs/hello-arch:latest@sha256:cd0b32276cdd5af510fb1df5c410f766e273fe63afe3cec5ff7da3f80f27985d MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/amd64
复制代码


有了这些散列值的帮助,我们可以逐一运行镜像,并观察其输出:


$ docker run --rm docker.io/mirailabs/hello-arch:latest@sha256:5fb57946152d26e64c8303aa4626fe503cd5742dc13a3fabc1a890adfc2683dfHello, arm!
$ docker run --rm docker.io/mirailabs/hello-arch:latest@sha256:cc6e91101828fa4e464f7eddec3fa7cdc73089560cfcfe4af16ccc61743ac02bHello, arm64!
$ docker run --rm docker.io/mirailabs/hello-arch:latest@sha256:cd0b32276cdd5af510fb1df5c410f766e273fe63afe3cec5ff7da3f80f27985dHello, amd64!
复制代码


看上去很简单,不是么?


总结

概括一下,本文我们了解了软件支持多 CPU 架构带来的挑战,以及 Docker 的实验性扩展 buildx 如何帮助我们解决这些挑战。通过使用 buildx,我们可以快速构建一个多架构 Docker 镜像,支持 arm、arm64 和 amd64 架构,而不需要修改 Dockerfile。同时这个镜像可以推送到 Docker Hub,任何 Docker 支持的平台都可以根据自己的架构拉取对应的镜像。


未来,buildx 能力很有可能称为标准 docker build 命令的一部分,我们可以不需要为使用这个功能做额外设置。


前进,无惧多架构!


参考文献


原文链接:


https://mirailabs.io/blog/multiarch-docker-with-buildx/


2019 年 11 月 28 日 09:282500

评论

发布
暂无评论
发现更多内容

IDEA 2020.3 更新了,机器学习都整上了

楼下小黑哥

Java IDEA

第九周作业

走走,停停……

区块链农产品溯源应用开发,农产品可追溯系统

135深圳3055源中瑞8032

iOS面试基础知识 (二)

iOSer

ios 面试题 iOS面试

架构一期第十二周作业

Airs

企业面临大危机,CRM崩溃告急,程序员竟用特殊手段化解危机!

Philips

敏捷开发

全人类的数字化迁徙在加速完成 | 读《区块链:分布式商业与智数未来》

邓瑞恒Ryan

创业 读书笔记 金融 经济 战略

架构师训练营第一期 - 第十二周学习总结

卖猪肉的大叔

极客大学架构师训练营

Java并发编程:如何防止在线程阻塞与唤醒时死锁

码农架构

Java 并发编程

第六周期作业

走走,停停……

重点人员动态管控系统开发,智慧公安情报研判平台

135深圳3055源中瑞8032

亿级流量背后战场,京东11.11大促全方位技术揭秘

京东科技开发者

云计算

最简单的 K8S 部署文件编写姿势,没有之一!

Kevin Wan

golang Kubernetes

盘点2020 | 热点事件回顾这一年经历的共同记忆

HPioneer

盘点2020

智慧社区服务平台开发,平安小区建设

t13823115967

智慧城市 平安小区

《Tensorflow:实战Google深度学习框架》.pdf

田维常

盘点 2020 | 一枚程序员的跑步之路

Simon

程序员 跑步 锻炼 盘点2020

架构师训练营第十二周课程笔记及心得

Airs

DolphinDB与Spark的性能对比测试报告

DolphinDB

spark 流计算 时序数据库 DolphinDB 数据库开发

智能警务平台搭建,公安一体化警务实战解决方案

t13823115967

智慧公安 智慧警务系统开发

mybatis分页插件如何实现?

田维常

mybatis

IPFS四币循环系统软件开发|IPFS四币循环APP开发

开發I852946OIIO

系统开发

三分钟看懂新一代.Net Core3.1工作流引擎平台

Learun

敏捷开发

第十周作业

走走,停停……

智慧社区管理平台开发解决方案,社区智能管控系统建设

WX13823153201

智慧社区管理平台开发

智慧警务大数据平台开发,大数据可视化分析系统

135深圳3055源中瑞8032

架构之书:田园与《Agile Web Development with Rails》

lidaobing

ruby-on-rails 架构

第七周作业

走走,停停……

第八周作业

走走,停停……

甲方日常 69

句子

工作 随笔杂谈 日常

架构师训练营第一期 - 第十二周课后作业

卖猪肉的大叔

极客大学架构师训练营

微服务架构下如何保证事务的一致性

微服务架构下如何保证事务的一致性

如何构建多架构 Docker 镜像?-InfoQ