为了在不同操作系统和处理器架构上运行应用,为不同平台单独构建程序版本是很常见的场景。当开发应用的平台与部署的目标平台不同时,实现这一目标并不容易。例如在 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
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
- 通过 QEMU 的用户态模式创建轻量级的虚拟机,在虚拟机系统中构建镜像。
- 在一个 builder 实例中加入多个不同目标平台的节点,通过原生节点构建对应平台镜像。
- 分阶段构建并且交叉编译到不同的目标架构。
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