支持多架构的容器镜像构建指南
服务器的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-amd64
和xjin/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
,以及os
和arch
信息。
本质上,debian:9.1
这个多架构镜像由多个不同架构的manifest
组成,当我们在不同机器上执行docker pull debian:9.1
时,Docker的行为:
- 获取当前机器的
os
和arch
信息作为target osarch
- 使用
target osarch
,去镜像仓库中拉取对应的digest
这样在镜像使用上感知到的就是一个支持多架构的镜像,本质上是是多架构的镜像元信息集合。
如何构建
由于当前实际使用中最常见的的服务器架构是amd64和arm64,下面主要介绍支持linux/amd64
和linux/arm64
的多架构镜像构建。
docker manifest合并
分别在Linux x86_64
和Linux arm64-v8
的机器上构建xjin/web-service
镜像,分别打上tag为xjin/web-service:amd64
和xjin/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
有指定GOOS
和GOARCH
需要去掉,要在构建过程中根据实际构建机器的硬件(或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参数在不同平台的支持情况。