目录

  • 1. Using Docker manifest to create multi-arch images on AWS Graviton processors
  • 1.1. Background
  • 1.2. Docker manifest
  • 1.3. Docker buildx
  • 1.4. Docker manifest
  • 1.5. Summary
  • 2. buildx 简介以及安装
  • 2.1. 简介
  • 3. 如何使用 docker buildx 构建跨平台 Go 镜像
  • 3.1. 前提
  • 3.2. docker buildx
  • 3.3. 启用 Buildx
  • 3.4. builder 实例
  • 3.5. 构建驱动
  • 3.6. buildx 的跨平台构建策略
  • 3.7. 一次构建多个架构 Go 镜像实践
  • 3.7.1. 源代码和 Dockerfile
  • 3.7.2. 执行跨平台构建
  • 3.7.3. 验证构建结果
  • 3.8. 如何交叉编译 Golang 的 CGO 项目
  • 3.8.1. 准备交叉编译环境和依赖
  • 3.8.2. 交叉编译 CGO 示例
  • 3.9. 总结


1. Using Docker manifest to create multi-arch images on AWS Graviton processors

Docker manifest is an experimental command to build docker images which support multiple architectures. Let’s see how it works and compare it to docker buildx.

In 2019, docker buildx was created as a way to build multi-architecture images. At the time, developers didn’t have Arm in the cloud or on their desk. Projects were interested in the Arm architecture, but as a secondary option for AWS Graviton or even Raspberry Pi. With buildx, a single build command creates a multi-architecture image. In some cases, an Arm machine is not even needed for the build.

Today, developers are using the Arm architecture on their desk and in the cloud. The trailblazing success of AWS Graviton processors has motivated developers to try out Graviton for themselves. The popularity of Mac computers with Apple silicon means many developers have the same architecture on their desk. Today, I will explain how to build multi-arch images using the docker manifest command. I have found this to be the easiest way to create multi-arch images for many projects.

1.1. Background

Docker buildx provides a build command to create images for multiple architectures. I’m primarily using arm64 and amd64. If you use other architectures, the concept can easily be extended.

The challenge with buildx is that it uses emulation, cross-compilation, or a remote builder for the target architectures which do not match the build machine. Emulation works fine for building images which primarily copy and install files, but it can suffer from functional issues or performance problems for more complex image building. Sometimes cross-compilation is easy, but sometimes it is difficult. Using a remote builder is not convenient for testing changes. Changing the remote builder setup while testing can be confusing.

Docker buildx also confuses developers who are expecting a local image to immediately run after a successful build. Because buildx targets multiple architectures, it must be saved to a repository, not local storage. This requires a push to a repository and then a pull to run. There are some other workarounds, but the use model is different from what many developers expect. Searching Stack Overflow and GitHub confirms that a missing image after successfully running buildx is a common question.

I spend the majority of my time on the Arm architecture. A shrinking number of projects require amd64 support, but when they do, I find myself shying away from buildx. Docker buildx is one way to approach the challenges of multiple architectures, but docker manifest feels easier for me.

Let’s see how it works.

1.2. Docker manifest

Docker includes an experimental feature called docker manifest. Please be aware the feature is experimental and not recommended for production use.

Docker manifest provides a different way of working. It allows separate images to be built for each architecture, which can be joined into a multi-arch image when it’s time to share. This enables me to build and test on any architecture and postpone using multi-arch until later. When it’s time to share a multi-arch image, it can be created in seconds using docker manifest. Docker manifest also makes it easy to update one of the architectures without emulation or remote builders.

Let’s do a quick comparison between buildx and docker manifest.

Here is a small example Dockerfile.

FROM alpine AS builder
RUN apk add build-base
WORKDIR /home
COPY hello.c .
RUN gcc "-DARCH=\"`uname -a`\"" hello.c -o hello

FROM alpine
WORKDIR /home
COPY --from=builder /home/hello .
CMD ["./hello"]

The Dockerfile compiles and runs a hello world C program.

#include <stdio.h>
#include <stdlib.h>

#ifndef ARCH
#define ARCH "Undefined"
#endif

int main()
{
    printf("Hello Arm Developers, architecture from uname is %s\n", ARCH);

    switch (sizeof(void *))
    {
        case 4:
            printf("32-bit userspace\n");
            break;
        case 8:
            printf("64-bit userspace\n");
            break;
        default:
            printf("unknown userspace\n");
    }
    exit(0);
}

Copy the Dockerfile and hello.c to your computer for the steps below.

Let’s see how to use it with buildx to build a multi-architecture image.

1.3. Docker buildx

Using buildx involves setting up a builder and running the buildx command.

Below is a script showing how to use buildx. Copy the file, change the HUBU variable to your Docker Hub username, and run the script to try it.

#!/bin/bash

HUBU=hubuser
IMG=hello-world

docker buildx create --name mybuilder
docker buildx use mybuilder
docker buildx build --platform linux/amd64,linux/arm64 -t $HUBU/$IMG --push .

The script automatically pushes the multi-arch image to Docker Hub. Without the push argument, the results will not be available. It’s easy enough to pull the image and run it, but with a larger image it does take time to push to the registry and pull it back.

There are numerous tutorials about using buildx if you want to learn more.

1.4. Docker manifest

The docker manifest command is useful to combine multiple existing images into a single multi-architecture image.

To keep track of the images, I use the image tag to indicate the architecture.

To build the same Dockerfile without buildx use this command. This produces a single image, just for the architecture of the current machine.

docker build -t hello-world:$(arch) .

The image is available locally to run. On AWS Graviton, the tag will be aarch64 to differentiate it from the same command run on an x86_64 machine.

After building an image on each machine you want to support, tag the images for a Docker Hub account and push. Again, change to your Docker Hub username to try it.

docker tag hello-world:$(arch) jasonrandrews/hello-world:$(arch)
docker push jasonrandrews/hello-world:$(arch)

The image below shows both tags in Docker Hub.

The last step is to join the two tags into a single multi-architecture image.

docker manifest create jasonrandrews/hello-world:latest \
--amend jasonrandrews/hello-world:aarch64 \
--amend jasonrandrews/hello-world:x86_64

docker manifest push --purge jasonrandrews/hello-world:latest
The purge option is not needed the first time, but I found that to update one of the images and update the multi-arch image it was needed.

After the manifest push, a new multi-arch image with the latest tag is available. Pulling hello-world:latest from either architecture now works, and a user doesn’t need to pay attention to the computer they are using.

1.5. Summary

The experimental docker manifest command offers a way to create multi-architecture images by joining multiple images using a manifest. Docker manifest provides additional features not covered here, and is useful to create, inspect, and modify manifest lists. As the docker manifest command matures and developers increase AWS Graviton usage, it provides another way to build docker images.

Give it a try and see how it works.

2. buildx 简介以及安装

注意: 使用 docker buildx ls 查看可以支持编译的系统架构时若只能看到 amd64/i386 这样的情况说明没有安装 QEMU 虚拟系统, 安装方法如下:

sudo apt install -y qemu-user-static binfmt-support

需要注意的是, qemu-user-static 需要 linux 内核 4.8 以上, binfmt-support 需要 2.1.7 版本及以上。不过如果你使用的不是非常旧的 Linux 系统, 基本不用担心这些限制

buildx 是一个包含很多功能的扩展工具包, 支持多平台构建只是其中一个功能点。

而 buildx 默认使用的是 docker 驱动, 这个是不支持多平台构建的。所以我们需要切换使用 docker-container 驱动, 这个驱动是特别支持多平台构建的

如果你用 docker 驱动来构建多平台镜像, 会得到以下错误

ERROR: multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")

所以, 我们切换使用 docker-container 驱动

# --name 名称任意命令, 不影响
sudo docker buildx create --name=container --driver=docker-container --use --bootstrap
  1. 构建多平台架构镜像
    好了, 完成上述步骤后, 现在你可以开始构建多平台架构镜像了
sudo docker buildx build --platform linux/amd64,linux/arm64 -t lingen/myddd-starter:latest .

--platform linux/amd64,linux/arm64, 我们指定构建 linux/amd64 与 linux/arm64 两个架构下的镜像

如果一切正常, 你就可以看到构建多平台的日志输出

上传镜像:

sudo docker buildx build --platform linux/amd64,linux/arm64 -t lingen/myddd-starter:latest . --push

2.1. 简介

buildx 可用于在单个平台上实现, 跨 CPU 架构编译。

buildx 实现 依赖 QEMU (开源的模拟器), 支持多种 cpu 架构, 比如 ARM、Power-PC 和 RISC-V。

QEMU 可以模拟一个完整的操作系统(开销较大)。

QEMU 还有一种用户态模式, 基于 binfmt_misc 模拟目标硬件的用户空间, 该模式可以创建轻量级的虚拟机 (chroot 或者 容器)。通过该方式 提供多种平台的虚拟机, 然后基于 Dockerfile 进行编译, 平台不通, 但 Dockerfile 是同一份。

小结:

buildx 会通过 QEMUbinfmt_misc 分别为 3 个不同的 CPU 架构 (arm, arm64 和 amd64) 构建 3 个不同的镜像。构建完成后, 就会创建一个 manifest list, 其中包含了指向这 3 个镜像的指针。

版本要求: Docker 19.03

比如我的 x86_64 环境, docker 恰好是 19.03

# uname -a
Linux control02 5.9.11-1.el7.elrepo.x86_64 #1 SMP Tue Nov 24 09:45:34 EST 2020 x86_64 x86_64 x86_64 GNU/Linux

# docker version
Client: Docker Engine - Community
 Version:           19.03.13
 API version:       1.40
 Go version:        go1.13.15
 Git commit:        4484c46d9d
 Built:             Wed Sep 16 17:03:45 2020
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.13
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       4484c46d9d
  Built:            Wed Sep 16 17:02:21 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.3.7
  GitCommit:        8fba4e9a7d01810a393d5d25a3621dc101981175
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683

centos 安装

选一个 buildx github 发布版本 安装即可
https://github.com/docker/buildx/releases/tag/v0.8.2

mkdir -vp ~/.docker/cli-plugins/
curl --silent -L "https://github.com/docker/buildx/releases/download/v0.8.2/buildx-v0.8.2.linux-amd64" > ~/.docker/cli-plugins/docker-buildx  
chmod a+x ~/.docker/cli-plugins/docker-buildx

参考: https://icloudnative.io/posts/multiarch-docker-with-buildx/

3. 如何使用 docker buildx 构建跨平台 Go 镜像

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

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

3.1. 前提

大部分镜像托管平台支持多平台镜像, 这意味着镜像仓库中单个标签可以包含不同平台的多个镜像, 以 docker hub 的 python 镜像仓库为例, 3.9.6 这个标签就包含了 10 个不同系统和架构的镜像(平台 = 系统 + 架构)。

通过 docker pulldocker run 拉取一个支持跨平台的镜像时, docker 会自动选择与当前运行平台相匹配的镜像。由于该特性的存在, 在进行镜像的跨平台分发时, 镜像的消费端是无感知, 我们只需要关心镜像的生产, 即如何构建跨平台的镜像。

3.2. docker buildx

默认的 docker build 命令无法完成跨平台构建任务, 需要为 docker 命令行工具安装 buildx 插件扩展其功能。buildx 能够使用由 Moby BuildKit 提供的构建镜像额外特性, 它能够创建多个 builder 实例, 在多个节点并行地执行构建任务, 以及跨平台构建。

3.3. 启用 Buildx

macOS 或 Windows 系统的 Docker Desktop, 以及 Linux 发行版通过 deb 或者 rpm 包所安装的 docker 内置了 buildx, 不需要另行安装。

如果你的 docker 没有 buildx 命令, 可以下载二进制包进行安装:

  1. 首先从 Docker buildx 项目的 release 页面找到适合自己平台的二进制文件。
  2. 下载二进制文件到本地并重命名为 docker-buildx, 移动到 docker 的插件目录 ~/.docker/cli-plugins
  3. 向二进制文件授予可执行权限。

如果本地的 docker 版本高于 19.03, 可以通过以下命令直接在本地构建并安装, 这种方式更为方便:

$ 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 .

buildx 和 docker build 命令的使用体验基本一致, 还支持 build 常用的选项如 -t-f 等。

3.4. builder 实例

docker buildx 通过 builder 实例对象来管理构建配置和节点, 命令行将构建任务发送至 builder 实例, 再由 builder 指派给符合条件的节点执行。我们可以基于同一个 docker 服务程序创建多个 builder 实例, 提供给不同的项目使用以隔离各个项目的配置, 也可以为一组远程 docker 节点创建一个 builder 实例组成构建阵列, 并在不同阵列之间快速切换。

使用 docker buildx create 命令可以创建 builder 实例, 这将以当前使用的 docker 服务为节点创建一个新的 builder 实例。要使用一个远程节点, 可以在创建示例时通过 DOCKER_HOST 环境变量指定远程端口或提前切换到远程节点的 docker context。下面以远程节点创建一个新的 builder 实例并通过命令行选项指定其驱动、目标平台和实例名称:

$ 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 将列出所有可用的 builder 实例和实例中的节点:

$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS  BUILDKIT PLATFORMS
default * docker
  default default         running 20.10.18 linux/amd64, linux/386

实例创建之后可以继续向它添加新的节点, 通过 docker buildx create 命令的 --append <node> 选项可将节点加入到 --name <builder> 选项指定的 builder 实例:

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

docker buildx inspectdocker buildx stopdocker buildx rm 命令用于管理一个实例的生命周期。

docker buildx use <builder> 将切换到所指定的 builder 实例。

3.5. 构建驱动

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

  • docker 驱动: 使用 Docker 服务程序中集成的 BuildKit 库执行构建。
  • docker-container 驱动: 启动一个包含 BuildKit 的容器并在容器中执行构建。

docker 驱动无法使用一小部分 buildx 的特性(如在一次运行中同时构建多个平台镜像), 此外在镜像的默认输出格式上也有所区别: docker 驱动默认将构建结果以 Docker 镜像格式直接输出到 docker 的镜像目录(通常是 /var/lib/overlay2), 之后执行 docker images 命令可以列出所输出的镜像; 而 docker container 则需要通过 --output 选项指定输出格式为镜像或其他格式。

为了一次性构建多个平台的镜像, 下文将使用 docker container 驱动的 builder 实例。

3.6. buildx 的跨平台构建策略

根据构建节点和目标程序语言不同, buildx 支持以下三种跨平台构建策略:

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

QEMU 通常用于模拟完整的操作系统, 它还可以通过用户态模式运行: 以 binfmt_misc 在宿主机系统中注册一个二进制转换处理程序, 并在程序运行时动态翻译二进制文件, 根据需要将系统调用从目标 CPU 架构转换为当前系统的 CPU 架构。最终的效果就像在一个虚拟机中运行目标 CPU 架构的二进制文件。Docker Desktop 内置了 QEMU 支持, 其他满足运行要求的平台可通过以下方式安装:

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

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

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

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

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

3.7.1. 源代码和 Dockerfile

下面将以一个简单的 Go 项目作为示例, 假设示例程序文件 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 /app
COPY main.go /app/main.go
RUN GOOS=linux GOARCH=$TARGETARCH go build -a -o output/main main.go 

FROM alpine:latest
WORKDIR /rootCOPY --from=builder /app/output/main .
CMD /root/main

构建过程分为两个阶段:

  • 在一阶段中, 我们将拉取一个和当前构建节点相同平台的 golang 镜像, 并使用 Go 的交叉编译特性将其编译为目标架构的二进制文件。
  • 然后拉取目标平台的 alpine 镜像, 并将上一阶段的编译结果拷贝到镜像中。

3.7.2. 执行跨平台构建

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

docker buildx build 通过 --platform 选项指定构建的目标平台。Dockerfile 中的 FROM 指令如果没有设置 --platform 标志, 就会以目标平台拉取基础镜像, 最终生成的镜像也将属于目标平台。当使用 docker-container 驱动时, 这个选项可以接受用逗号分隔的多个值作为输入以同时指定多个目标平台, 所有平台的构建结果将合并为一个整体的镜像列表作为输出, 因此无法直接输出为本地的 docker images 镜像。此外 Dockerfile 中可通过 BUILDPLATFORMTARGETPLATFORMBUILDARCHTARGETARCH 等参数使用该选项的值。

docker buildx build 支持丰富的输出行为, 通过 --output=[PATH,-,type=TYPE[,KEY=VALUE] 选项可以指定构建结果的输出类型和路径等, 常用的输出类型有以下几种:

  • local: 构建结果将以文件系统格式写入 dest 指定的本地路径, 如 --output type=local,dest=./output
  • tar: 构建结果将在打包后写入 dest 指定的本地路径。
  • oci: 构建结果以 OCI 标准镜像格式写入 dest 指定的本地路径。
  • docker: 构建结果以 Docker 标准镜像格式写入 dest 指定的本地路径或加载到 docker 的镜像库中。同时指定多个目标平台时无法使用该选项。
  • image: 以镜像或者镜像列表输出, 并支持 push=true 选项直接推送到远程仓库, 同时指定多个目标平台时可使用该选项。
  • registry: type=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/amd64、 linux/arm64 和 linux/arm 三种平台的镜像, 并将输出结果直接推送到远程的阿里云私人镜像仓库中。

构建过程可拆解如下:

  1. docker 将构建上下文传输给 builder 实例。
  2. builder 为命令行 --platform 选项指定的每一个目标平台构建镜像, 包括拉取基础镜像和执行构建步骤。
  3. 导出构建结果, 镜像文件层被推送到远程仓库。
  4. 生成一个清单 JSON 文件, 并将其作为镜像标签推送给远程仓库。

3.7.3. 验证构建结果

运行结束后可以通过 docker buildx imagetools 探查已推送到远程仓库的镜像:

$ docker buildx imagetools inspect http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latest
Name: http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latest
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest: 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

最后在不同的平台以 latest 标签拉取并运行镜像, 验证构建结果是否正确。
使用 Docker Desktop 时, 其本身集成的虚拟化功能可以运行不同平台的镜像, 可以直接以 sha256 值拉取镜像:

$ docker run --rm http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latest@sha256:cb6a7614ee3db03c8858e3680b1585f32a6fe3de9b371e37e25cf42a83f6e0baHello world!Running in [amd64] architecture.

$ docker run --rm http://registry.cn-hangzhou.aliyuncs.com/waynerv/arch-demo:latest@sha256:034aa0077a452a6c2585f8b4969c7c85d5d2bf65f801fcc803a00d0879ce900eHello world!Running in [arm64] architecture.

3.8. 如何交叉编译 Golang 的 CGO 项目

支持交叉编译到常见的操作系统和 CPU 架构是 Golang 的一大优势, 但以上示例中的解决方案只适用于纯 Go 代码, 如果项目中通过 cgo 调用了 C 代码, 情况会变得更加复杂。

3.8.1. 准备交叉编译环境和依赖

为了能够顺利编译 C 代码到目标平台, 首先需要在编译环境中安装目标平台的 C 交叉编译器(通常基于 gcc), 常用的 Linux 发行版会提供大部分平台的交叉编译器安装包, 可以直接通过包管理器安装。

其次还需要安装目标平台的 C 标准库(通常标准库会作为交叉编译器的安装依赖, 不需要单独安装), 另外取决于你所调用的 C 代码的依赖关系, 可能还需要安装一些额外的 C 依赖库(如 libopus-dev 之类)。

我们将使用 amd64 架构的 golang:1.14 官方镜像作为基础镜像执行编译, 其使用的 Linux 发行版为 Debian。假设交叉编译的目标平台是 linux/arm64, 则需要准备的交叉编译器为 gcc-aarch64-linux-gnu, C 标准库为 libc6-dev-arm64-cross, 安装方式为:

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

libc6-dev-arm64-cross 会同时被安装。

得益于 Debian 包管理器 dpkg 提供的多架构安装能力, 假如我们的代码依赖 libopus-dev 等非标准库, 可通过 : 的方式安装其 arm64 架构的安装包:

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

3.8.2. 交叉编译 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 /app
COPY . /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

Dockerfile 中通过 apt-get 安装了 gcc-aarch64-linux-gnu 作为交叉编译器, 示例程序较为简单因此不需要额外的依赖库。在执行 go build 进行编译时, 需要通过 CC 和 CC_FOR_TARGET 环境变量指定所使用的交叉编译器。

为了基于同一份 Dockerfile 执行多个目标平台的编译(假设目标架构只有 amd64/arm64), 最下方的 RUN 指令使用了一个小技巧, 通过 Bash 的条件判断语法来执行不同的编译命令:

  • 假如构建任务的目标平台是 arm64, 则指定 CC 和 CC_FOR_TARGET 环境变量为已安装的交叉编译器(注意它们的值有所不同)。
  • 假如构建任务的目标平台是 amd64, 则不指定交叉编译器相关的变量, 此时将使用默认的 gcc 作为编译器。

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

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

3.9. 总结

有了 Buildx 插件的帮助, 在缺少基础设施的情况下, 我们也能使用 docker 方便地构建跨平台的应用镜像。

但默认通过 QEMU 虚拟化目标平台指令的方式有明显地性能瓶颈, 如果编写应用的语言支持交叉编译, 我们可以通过结合 buildx 和交叉编译获得更高的效率。

本文最后介绍了一种进阶场景的解决方案: 如何对使用了 CGO 的 Golang 项目进行交叉编译, 并给出了编译到 linux/arm64 平台的示例。