当前位置: 首页 > 科技观察

Docker镜像如何实现“一次构建,到处运行”?

时间:2023-03-13 15:38:30 科技观察

在每个黑客的职业生涯中,都会遇到一个应用程序需要针对另一种CPU架构进行编译的时刻。在为RaspberryPi项目编译应用程序、为嵌入式设备创建自定义图像或使您自己的软件支持不同平台时,可能会出现这种情况。或者,我们只是想知道这个过程是什么样的,或者想知道最终的汇编代码与台式机上无处不在的x86-64/amd64架构汇编有何不同。不管是什么原因,通常我们都需要收拾行李去朝圣。但这趟旅程并非通往孤山之巅,而是通往地狱的深渊,从应用程序开发的阳光平原到计算机体系结构的黑暗洞穴:底层系统和嵌入式变化的难以捉摸的世界。鉴于这次长途跋涉的前景黯淡,大多数黑客最终按下Ctrl+Z并返回表面,喘着粗气并警告其他黑客交叉编译、QEMU和chroot的恐怖。好吧,我可能有点夸张了。但事实是,为其他CPU架构构建应用程序并不那么简单。由于Docker19.03中的实验性插件,多架构构建比以往任何时候都更容易。要了解Docker支持多架构构建的重要性,我们首先需要了解如何为不熟悉的架构构建应用程序。背景:不熟悉架构的应用编译方法注意:如果读者已经了解本节的概念,或者只是想知道如何构建镜像,可以跳过本节。让我们快速浏览一下当前为不熟悉的体系结构编译应用程序的方法。方法1:直接在目标硬件上构建如果我们可以访问目标架构硬件,并且操作系统拥有我们需要的所有构建数据,那么就可以直接在硬件上编译应用程序。比如在我们的具体场景中构建一个多架构的Docker镜像,我们可以在树莓派上安装Docker运行环境,然后像在开发机上一样通过应用的Dockerfile直接在其上构建镜像。这种方法之所以可行,是因为树莓派的官方操作系统Raspbian支持本地安装Docker。但是,如果我们无法轻松访问目标硬件怎么办?我们可以直接在开发机器上构建非原生架构的应用程序吗?方法2:模拟目标硬件还记得使用16位任天堂游戏机的快乐时光吗?那时我还是个孩子,但随着年龄的增长,我发现了对《超级玛丽》、《时空之轮》等经典游戏的怀念之情。我还没有机会拥有超级任天堂游戏机,但多亏了像ZSNES这样的模拟器,我可以回到过去并在32位PC上享受这些经典游戏。使用模拟器,我们不仅可以玩视频游戏,还可以构建非本机二进制文件。当然,我们没有使用ZSNES,而是使用了一个更强大、更灵活的模拟器:QEMU。QEMU是一个免费的开源仿真器,支持许多常见的架构,包括:ARM、Power-PC和RISC-V。通过运行一个全功能的模拟器,我们可以启动一个可以运行Linux操作系统的通用ARM虚拟机,然后在虚拟机中搭建开发环境,编译应用程序。但是,仔细想想,功能齐全的虚拟机有点浪费资源。在这种模式下,QEMU模拟整个系统,包括定时器、内存控制器、SPI和I2C总线控制器等硬件。但大多数情况下,我们在编译应用程序时并不关心上面提到的硬件特性。会更好吗?方法三:通过binfmt_misc模拟目标架构的用户空间在Linux系统上,QEMU还有另一种运行模式,可以通过用户态模拟器运行非原生架构的二进制文件。在这种模式下,QEMU将跳过方法2中描述的整个目标系统硬件的模拟,而是通过binfmt_misc在Linux内核中注册一个二进制格式的处理程序,在执行之前拦截并转换不熟悉的二进制代码,同时系统根据需要将调用从目标系统转换为当前系统。最终,对于用户来说,他们会发现他们可以在本地运行这些异构的二进制程序。通过用户态模拟器和QEMU,我们可以通过轻量级虚拟化(chroot或container)安装其他Linux发行版,将我们需要的异构二进制程序编译为本地。我们将在下面看到,这将是构建多架构Docker镜像的一种可选方式。方法4:使用交叉编译器最后,我们还有另一种嵌入式系统社区的标准做法:交叉编译。交叉编译器是一种特殊的编译器,它在主机架构上运行,但为不同的目标架构生成二进制文件。例如,我们可以有一个针对aarch64(64位ARM)嵌入式设备(例如智能手机或其他设备)的amd64架构的C++交叉编译器。基于这种方法的一个真实示例是全球数十亿的Android设备如何使用这种方法来构建软件。从性能的角度来看,这种方法与直接在目标硬件上构建(方法1)具有相同的效率,因为它不在模拟器上运行。但是交叉编译的变量取决于使用的编程语言,如果是Go语言就很方便了。使困惑?对于Docker镜像来说更复杂...请注意,前面提到的所有编译方法只生成一个应用程序二进制文件。对于现代容器,当我们引入Docker镜像时,它不仅仅是构建单个二进制文件,而是构建一个完整的异构容器镜像!这比前面说的还要麻烦。如果所有这些听起来很痛苦,请不要难过,因为构建非本机平台二进制文件已经很痛苦了。在这之上增加Docker的复杂性似乎应该留给专家。由于对最新版本的Docker运行时环境进行了实验性扩展,构建多架构镜像现在比以往任何时候都容易。构建多架构Docker镜像为了更方便的构建多架构Docker镜像,我们可以使用最近发布的Docker扩展:buildx。buildx是下一代标准dockerbuild命令的前端,这是用于构建Docker映像的熟悉命令。Buildx扩展了表中dockerbuild命令的功能,通过利用BuildKit的所有功能成为Docker构建系统的新后端。让我们花几分钟看看如何使用buildx构建多架构镜像。第一步:打开buildx要使用buildx,首先要确认我们的Docker运行环境是最新的19.03版本。在新版本中,buildx其实是默认绑定了Docker的,但是需要通过设置环境变量DOCKER_CLI_EXPERIMENTAL来开启。让我们在当前命令行会话中启用它:$exportDOCKER_CLI_EXPERIMENTAL=enabled通过检查我们已经可以使用buildx的版本来验证:$dockerbuildxversiongithub.com/docker/buildxv0.3.1-tp-docker6db68d029599c6710a32aa7adcba8e5a344795a7可选步骤:如果需要,从源代码构建使用最新版本的buildx,或者设置DOCKER_CLI_EXPERIMENTAL环境变量在当前环境下不生效(比如我发现在ArchLinux系统中设置无效),我们可以从源码构建buildx:$exportDOCKER_BUILDKIT=1$dockerbuild--platform=local-o.git://github.com/docker/buildx$mkdir-p~/.docker/cli-plugins&&mvbuildx~/.docker/cli-plugins/docker-buildx第2步:启用binfmt_misc运行非本地架构Docker镜像如果读者使用Mac或Windows版本的DockerDesktop可以跳过这一步,因为binfmt_misc默认是开启的。如果使用Linux系统,需要设置binfmt_misc。在大多数发行版中,这是非常简单的,但现在通过运行特权Docker容器更容易设置:al/proc/sys/fs/binfmt_misc/total0drwxr-xr-x2rootroot0Nov1209:19.dr-xr-xr-x1rootroot0Nov1209:16..-rw-r--r--1rootroot0Nov1209:25qemu-aarch64-rw-r--r--1rootroot0Nov1209:25qemu-arm-rw-r--r--1rootroot0Nov1209:25qemu-ppc64le-rw-r--r--1rootroot0Nov1209:25qemu-s390x--w------1rootroot0Nov1209:19register-rw-r--r--1rootroot0Nov1209:19statusThen,verifythatthespecifiedarchitecturehandlerisenabled,forexample:$cat/proc/sys/fs/binfmt_misc/qemu-aarch64enabledinterpreter/usr/bin/qemu-aarch64flags:OCFFFFSET0MAGIC7F454C460201010000000000000000000000000000b7maskffffffffffffff00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAER图像构造切换到多架构。Docker使用旧的。要创建新的多架构构建器,请运行:$dockerbuildxcreate--use--namemybuilder验证新构建器是否可用:$dockerbuildxlsNAME/NODEDRIVER/ENDPOINTSTATUSPLATFORMSmybuilder*docker-containermybuilder0unix:///var/run/docker。sockinactivedefaultdockerdefaultdefaultrunninglinux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6搞定。Docker现在使用支持构建多架构镜像的新构建器。第4步:构建多架构镜像好了,现在我们终于可以开始构建多架构镜像了。为了演示此功能,我们需要一个示例应用程序。让我们创建一个简单的Go应用程序,输出当前运行环境的架构信息:$cathello.gopackagemainimport("fmt""runtime")funcmain(){fmt.Printf("Hello,%s!\n",runtime.GOARCH)}让我们创建一个Dockerfile来容器化这个应用程序$catDockerfileFROMgolang:alpineASbuilderRUNmkdir/appADD./app/WORKDIR/appRUNgobuild-ohello.FROMalpineRUNmkdir/appWORKDIR/appCOPY--from=builder/app/hello.CMD["./hello"]This是一个多阶段的Dockerfile,它使用Go编译器构建我们的应用程序,然后使用AlpineLinux映像创建已构建二进制文件的最小映像。现在,让我们使用buildx构建一个支持arm、arm64和amd64架构的多架构镜像,并一次性推送到DockerHub:$dockerbuildxbuild-tmirailabs/hello-arch--platform=linux/arm,linux/arm64,linux/amd64.--push是的,就是这样。现在在DockerHub上,我们有支持arm、arm64和amd64架构的多架构Docker镜像。当我们运行dockerpullmiralabs/hello-arch时,Docker会根据机器的架构获取匹配的镜像。如果读者问buildx是如何做到这一点的?那么,在幕后,buildx使用QEMU和binfmt_misc创建了三个Docker镜像(arm、arm64和amd64架构各一个)。构建完成后,Docker会创建一个清单,其中包含三个映像及其对应的架构。换句话说,“多体系结构图像”实际上是每个体系结构的图像列表。第5步:测试多架构图像让我们快速测试多架构图像以确保它们都能正常工作。由于我们设置了binfmt_misc,任何架构的镜像都可以在开发机器上执行。首先,列出每个镜像的散列值:$dockerbuildximagetoolsinspectmirailabs/hello-archName:docker.io/mirailabs/hello-arch:latestMediaType:application/vnd.docker.distribution.manifest.list.v2+jsonDigest:sha256:bbb246e520a23e41b0c6d38b933eece68a8407eede054994cff43c9575edce96Manifests:名称:docker.io/mirailabs/hello-arch:latest@sha256:5fb57946152d26e64c8303aa4626fe503cd5742dc13a3fabc1a890adfc2683dfMediaType:application/vnd.docker.distribution.manifest.v2+jsonPlatform:linux/arm:rail.archio-v7名称:doabcker@sha256:cc6e91101828fa4e464f7eddec3fa7cdc73089560cfcfe4af16ccc61743ac02bMediaType:application/vnd.docker.distribution.manifest.v2+jsonPlatform:linux/arm64Name:docker.io/mirailabs/hello-arch:latest@sha256:cd0b32276cdd5af510fb1df5c410f766e273fe63afe3cec5ff7da3f80f27985dMediaType:application/vnd.docker.distribution.manifest.v2+jsonPlatform:linux/amd64借助这些哈希值,我们可以一张一张地运行图像并观察输出:$dockerrun--rmdocker.io/mirailabs/hello-arch:latest@sha256:5fb57946152d26e64c8303aa4626fe503cd5742dc13a3fabc1a890adfc2683dfHello,arm!$dockerrun--rmdocker.io/mirailabs/hello-arch:latest@sha256:cc6e91101828fa4e464f7eddec3fa7cdc73089560cfcfe4af16ccc61743ac02bHello,arm64!$dockerrun--rmdocker.io/mirailabs/hello-arch:latest@sha256:cd0b32276cdd5af510fb1df5c410f766e273fe63afe3cec5ff7da3f80f27985dHello,amd64!看上去很简单,不是吗?总结一下,在这篇文章中我们了解了软件支持多CPU架构的挑战,以及Docker的实验性扩展buildx如何帮助我们解决这些挑战通过使用buildx,我们可以快速构建一个多架构的Docker镜像,支持arm,arm64和amd64架构,无需修改Dockerfile。同时可以将这个镜像推送到DockerHub,任何Docker支持的平台都可以根据自己的架构拉取相应的镜像。未来buildx能力很可能会成为标准dockerbuild命令的一部分,我们不需要做额外的设置就可以使用这个特性。将交叉编译的应用程序比作坠入深渊的故事,正在变成原始时代的鬼故事。勇往直前,多架构无所畏惧!