为了在不同操作系统和处理器架构上运行应用,为不同平台单独构建程序版本是很常见的场景。当开发应用的平台与部署的目标平台不同时,实现这一目标并不容易。例如在 x86 架构上开发一个应用程序并将其部署到 ARM 平台的机器上,通常需要准备 ARM 平台的基础设施用于开发和编译。

一次构建多处部署的镜像分发大幅提高了应用的交付效率,对于需要跨平台部署应用但基础设施不够充分的场景,利用 docker buildx 构建跨平台的镜像是一种快捷高效的解决方案。

前提

docker hubpython3.9.6
docker pulldocker rundocker

docker buildx

docker builddockerbuildxbuildx

启用 Buildx

debrpmdockerbuildx
dockerbuildx
docker-buildx~/.docker/cli-plugins
docker


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


buildx


docker buildx build .


buildxdocker buildbuild-t-f

builder 实例

docker buildxdockerdocker
docker buildx createDOCKER_HOSTdocker context


$ export DOCKER_HOST=tcp://10.10.150.66:2375$ docker buildx create --driver docker-container --platform linux/amd64,linux/arm64 --name remote-builderremote-builder


docker buildx ls


$ docker buildx lsNAME/NODE DRIVER/ENDPOINT STATUS PLATFORMSremote-builder docker-container remote-builder0 tcp://10.10.150.66:2375 inactive linux/amd64*, linux/arm64*default * docker default default running linux/amd64, linux/386


docker buildx create--append --name 


$ docker buildx create --name default --append remote-builder0


docker buildx inspectdocker buildx stopdocker buildx rm
docker buildx use 

构建驱动

buildx 实例通过两种方式来执行构建任务,两种执行方式被称为使用不同的「驱动」:

dockerdocker-container
dockerbuildxdockerdocker/var/lib/overlay2docker imagesdocker container--output
docker container

buildx 的跨平台构建策略

buildx
  1. 通过 QEMU 的用户态模式创建轻量级的虚拟机,在虚拟机系统中构建镜像。
  2. 在一个 builder 实例中加入多个不同目标平台的节点,通过原生节点构建对应平台镜像。
  3. 分阶段构建并且交叉编译到不同的目标架构。
binfmt_misc


$ docker run --privileged --rm tonistiigi/binfmt --install all


这种方式不需要对已有的 Dockerfile 做任何修改,实现的成本很低,但显而易见效率并不高。

将不同系统架构的原生节点添加到 builder 实例中可以为跨平台编译带来更好的支持,而且效率更高,但需要有足够的基础设施支持。

如果构建项目所使用的程序语言支持交叉编译(如 C 和 Go),可以利用 Dockerfile 提供的分阶段构建特性:首先在和构建节点相同的架构中编译出目标架构的二进制文件,再将这些二进制文件复制到目标架构的另一镜像中。下文会使用 Go 实现一个具体的示例。这种方式不需要额外的硬件,也能得到较好的性能,但只有特定编程语言能够实现。

一次构建多个架构 Go 镜像实践

源代码和 Dockerfile

main.go


package main import ( "fmt" "runtime") func main() { fmt.Println("Hello world!") fmt.Printf("Running in [%s] architecture.\n", runtime.GOARCH)}


定义构建过程的 Dockerfile 如下:


FROM --platform=$BUILDPLATFORM golang:1.14 as builder ARG TARGETARCH WORKDIR /appCOPY main.go /app/main.goRUN GOOS=linux GOARCH=$TARGETARCH go build -a -o output/main main.go FROM alpine:latestWORKDIR /rootCOPY --from=builder /app/output/main .CMD /root/main


构建过程分为两个阶段:

golangalpine

执行跨平台构建

执行构建命令时,除了指定镜像名称,另外两个重要的选项是指定目标平台和输出格式。

docker buildx build--platform--platformdocker-containerdocker imagesBUILDPLATFORMTARGETPLATFORMBUILDARCHTARGETARCH
docker buildx build--output=[PATH,-,type=TYPE[,KEY=VALUE]
dest--output type=local,dest=./outputdestdestdestdockerpush=truetype=image,push=true
docker buildx build


$ docker buildx build --platform linux/amd64,linux/arm64,linux/arm -t http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo -o type=registry .


linux/amd64linux/arm64linux/arm

构建过程可拆解如下:

docker--platform

验证构建结果

docker buildx imagetools


$ docker buildx imagetools inspect http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latestName: http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latestMediaType: application/vnd.docker.distribution.manifest.list.v2+jsonDigest: sha256:e2c3c5b330c19ac9d09f8aaccc40224f8673e12b88ff59cb68971c36b76e95ca Manifests: Name: http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latest@sha256:cb6a7614ee3db03c8858e3680b1585f32a6fe3de9b371e37e25cf42a83f6e0ba MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/amd64 Name: http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latest@sha256:034aa0077a452a6c2585f8b4969c7c85d5d2bf65f801fcc803a00d0879ce900e MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/arm64 Name: http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latest@sha256:db0ee3a876fb789d2e733471385eef0a056f64ee12d9e7ef94e411469d054eb5 MediaType: application/vnd.docker.distribution.manifest.v2+json Platform: linux/arm/v7


latestsha256





如何交叉编译 Golang 的 CGO 项目

cgo

准备交叉编译环境和依赖

gcc
libopus-dev
amd64golang:1.14linux/arm64gcc-aarch64-linux-gnulibc6-dev-arm64-cross


$ apt-get update$ apt-get install gcc-aarch64-linux-gnu


libc6-dev-arm64-cross
dpkglibopus-dev:arm64


$ dpkg --add-architecture arm64$ apt-get update$ apt-get install -y libopus-dev:arm64


交叉编译 CGO 示例

cgo


package main /*#include <stdlib.h>*/import "C"import "fmt" func Random() int { return int(C.random())} func Seed(i int) { C.srandom(C.uint(i))} func main() { rand := Random() fmt.Printf("Hello %d\n", rand)}


将使用的 Dockerfile 如下:


FROM --platform=$BUILDPLATFORM golang:1.14 as builder ARG TARGETARCHRUN apt-get update && apt-get install -y gcc-aarch64-linux-gnu WORKDIR /appCOPY . /app/ RUN if [ "$TARGETARCH" = "arm64" ]; then CC=aarch64-linux-gnu-gcc && CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ CGO_ENABLED=1 GOOS=linux GOARCH=$TARGETARCH CC=$CC CC_FOR_TARGET=$CC_FOR_TARGET go build -a -ldflags '-extldflags "-static"' -o /main main.go


apt-getgcc-aarch64-linux-gnugo buildCCCC_FOR_TARGET
amd64arm64RUN
arm64CCCC_FOR_TARGETamd64gcc

最后使用 buildx 执行构建的命令如下:


$ docker buildx build --platform linux/amd64,linux/arm64 -t http://registry.cn-hangzhou.aliyuncs.com/waynerv/cgo-demo -o type=registry .


总结

Buildxdocker
buildx
linux/arm64