Contents

支持多架构的容器镜像构建指南

服务器的CPU架构有x86、arm等类型,在当前国内IT行业“去IOE”和“自主可控”的大背景下,对于我们容器化领域的需求之一就是:提供出支持多架构的镜像,既支持在x86架构(amd64为典型)的服务器上运行,也支持在国产arm架构的服务器上运行。因此需要有一个解决方案,帮助我们使用同一个镜像面向多架构,降低镜像维护成本。

多架构的问题

首先,容器镜像必须与其所在的宿主机的CPU架构相同,才可以正常运行。比如我们在amd64机器上构建一个基础镜像也为amd64的Dockerfile进行构建,该镜像是无法在arm64的宿主机上运行的。

为了解决这个问题,我们想当然的会想到,针对各个架构分别打镜像就好啦。假如我们构建一个镜像xjin/web-service的Dockerfile为:

FROM debian:9.1
CMD supervisord

那么我们似乎需要分别使用amd64和arm64的debian基础镜像,去构建两个镜像xjin/web-service-amd64xjin/web-service-arm64。那如果还要支持其他架构的呢,比如386、s390x,那么镜像的维护工作将会变得非常麻烦。

有没有一种办法,能让我们只提供一个镜像出去,各种架构的Docker在拉取镜像时,根据自己的架构去选择所需的镜像呢?

当然可以,比如官方debian:9.1镜像,我们可以分别在arm64和amd64的机器上执行docker run -it --rm debian:9.1,发现都是可以正常运行的,下面分析一下其背后的机制。

原理说明

官方debian:9.1就是一个典型的多架构镜像,执行docker manifest inspect debian:9.1会看到如下结果:

{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:2335c729b8a6764c52a3cbfe43d1450d5e782638c986d237ffc30ca33881c3e3",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:5a5cd10fece3a8a19c9d76484d6f81ff36cbd8b324f4e1a2d1870670e0839000",
         "platform": {
            "architecture": "arm",
            "os": "linux",
            "variant": "v5"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:d74cc69431f03bbfbbf9fd52c1eabd6ca491280a03da267acb63b65b81e30c8a",
         "platform": {
            "architecture": "arm",
            "os": "linux",
            "variant": "v7"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:10656f9d3452a3825f879d52b7ab6f997eddd071bb08c79b353189655cbb8dbd",
         "platform": {
            "architecture": "arm64",
            "os": "linux",
            "variant": "v8"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:4599b85efe839220e3c00b5d380910fbe968ffe933a9155a6f013fb416ffa1f1",
         "platform": {
            "architecture": "386",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:34b94575d7b39cbbdc2facecd0e8fe87b203179fe0221811fb13a1d911311756",
         "platform": {
            "architecture": "ppc64le",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 529,
         "digest": "sha256:b01d35a1891549568b1f5fb66b329dded1e9cd45d6cb74f0c02aeb4c72a1417f",
         "platform": {
            "architecture": "s390x",
            "os": "linux"
         }
      }
   ]
}

可以看出debian:9.1由多个manifest组成,每个manifest包含了不同系统架构所对应镜像的唯一的digest,以及osarch信息。

本质上,debian:9.1这个多架构镜像由多个不同架构的manifest组成,当我们在不同机器上执行docker pull debian:9.1时,Docker的行为:

  1. 获取当前机器的osarch信息作为target osarch
  2. 使用target osarch,去镜像仓库中拉取对应的digest

这样在镜像使用上感知到的就是一个支持多架构的镜像,本质上是是多架构的镜像元信息集合。

如何构建

由于当前实际使用中最常见的的服务器架构是amd64和arm64,下面主要介绍支持linux/amd64linux/arm64的多架构镜像构建。

docker manifest合并

分别在Linux x86_64Linux arm64-v8的机器上构建xjin/web-service镜像,分别打上tag为xjin/web-service:amd64xjin/web-service:arm64-v8,并将两个镜像推送到远程镜像仓库。

将两个镜像的manifest组合,并推送到镜像仓库:

docker manifest create xjin/web-service:mutil-arch xjin/web-service:amd64 xjin/web-service:arm64-v8
docker manifest push xjin/web-service:mutil-arch

docker buildx

buildx是 docker 的多平台镜像构建插件,其本质是翻译不同的指令集,并在此之上进行构建。要想使用 buildx,首先要确保 Docker 版本不低于 19.03,同时还要通过设置环境变量 DOCKER_CLI_EXPERIMENTAL 来启用。可以通过下面的命令来为当前终端启用 buildx 插件:

export DOCKER_CLI_EXPERIMENTAL=enabled

我使用的macOS上的Docker Desktop不需要进行此配置,可以执行docker buildx version验证是否开启buildx

直接使用buildx构建支持多架构的镜像,并推送:

docker buildx build --platform linux/arm64,linux/amd64 -t xjin/web-service:multi-arch .
docker push xjin/web-service:multi-arch

在使用buildx时,可以利用一些Dockerfile支持的架构相关变量,可参考使用 buildx 构建多种系统架构支持的 Docker 镜像文章中的使用举例。

实践过程

注意,多架构镜像构建的前提条件:Dockerfile中使用的基础镜像必须是多架构的,可以用docker manifest inspect 查看。下面分别介绍C、Golang、Java三种语言编写的服务的实践。

Golang

以一个简单的服务举例,Dockerfile如下:

FROM golang:alpine AS builder
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN go build -o hello .

FROM alpine
RUN mkdir /app
WORKDIR /app
COPY --from=builder /app/hello .
CMD ["./hello"]

构建并推送:

docker buildx build -t xjin/hello-world:multi-arch --platform=linux/arm64,linux/amd64 . --push

需要注意的是:

  • Golang支持交叉编译,如果我们之前Dockerfile里的go build有指定GOOSGOARCH需要去掉,要在构建过程中根据实际构建机器的硬件(或buildx模拟硬件)作为GOOS。

C

这里以编译一个可提供Redis服务的镜像举例,Dockerfile如下:

# Pull base image.
FROM debian:9.1
# Install Redis.
COPY redis-6.2.6.tar.gz /tmp/
RUN buildDeps='gcc libc6-dev make sudo' && \ 
  #install dependencies
  apt-get update && \
  apt-get install -y $buildDeps && \
  #install basic tools
  apt-get install -y supervisor vim openssh-server tcpdump less host dnsutils dsniff htop netcat && \
  #make install redis-server 
  mkdir -p /tmp/redis && \
  tar -xzf /tmp/redis-6.2.6.tar.gz -C /tmp/redis --strip-components=1 && \
  make -C /tmp/redis MALLOC=jemalloc && \
  make -C /tmp/redis install && \
  #clean
  rm -rf /tmp/*
# Define default command.
ENTRYPOINT [ "supervisord" ]
#CMD ["redis-server", "/redis/redis.conf"]
# Expose ports.
EXPOSE 6379

构建并推送:

docker buildx build -t xjin/redis:6.2.6-multi-arch --platform linux/arm64,linux/amd64 . --push

需要注意的是:

  • C编写的服务一定要基于多架构基础镜像重新编译,直接使用二进制包很多情况下会出问题。
  • 部分C组件编译时可能会从编译机上获取配置值,因此需要到指定的机器上进行编译。比如jemalloc会根据编译机确定PAGESIZE,我个人在Redis支持ARM64的镜像构建中也踩到了这个坑,可以参考该文章:Redis - 适配全国产操作系统的那些坑

Java

Java项目比起Golang和C更为简单,因为它编译的结果是对OS平台无依赖的字节码,构建过程可以参考上述Golang的构建。需要注意的有两点:

  • 注意检查使用的tomcat、jdk等基础镜像本身是多架构的,openjdk目前不支持arm。
  • 需要检查是否有JVM参数在不同平台的支持情况。

参考

  1. https://yeasy.gitbook.io/docker_practice/image/manifest
  2. https://yeasy.gitbook.io/docker_practice/buildx/multi-arch-images
  3. https://javamana.com/2021/03/20210324070711639P.html