Z.S.K.'s Records

Docker之交叉编译

最近由于业务需要,在做一些关于国产化相关的事情,需要将平台从x86架构迁移到arm64上run起来,最重要的环节则在于镜像都需要rebuild一遍,在这个过程中还是碰到一些问题,拿出来分享一下,希望对其他人会有所帮助.

把所有的镜像都rebuild,这是个很辛苦且枯燥的事情,还好日常工作中都会良好的版本管理,每个应用的版本都对应有相应的commit号,这个可以直接使用稳定的版本进行rebuild,可以省去联调的工作

先简单介绍下binfmt_misc,后面docker交叉编译会用上.

binfmt_misc

binfmt_misc(Miscellaneous Binary Format)是Linux内核从很早开始就引入的机制,可以通过要打开文件的特性来选择到底使用哪个程序来打开它,不光可以通过文件的扩展名来判断的,还可以通过文件开始位置的特殊的字节(Magic Byte)来判断

这里不过多介绍,简单来说说是通过注册一个“解释器”和一个文件识别方式,以达到运行文件的时候调用自定义解释器的目的,docker buildx即是通过这种方式

启用binfmt_misc也比较简单,可以使用现成的docker直接启动:

1
docker run --rm --privileged docker/binfmt:66f9012c56a8316f9244ffd7622d7c21c1f6f28d

检查是否开启:

1
2
3
4
5
6
7
ls -al /proc/sys/fs/binfmt_misc/
-rw-r--r-- 1 root root 0 11月 18 00:12 qemu-aarch64
-rw-r--r-- 1 root root 0 11月 18 00:12 qemu-arm
-rw-r--r-- 1 root root 0 11月 18 00:12 qemu-ppc64le
-rw-r--r-- 1 root root 0 11月 18 00:12 qemu-s390x
--w------- 1 root root 0 11月 18 00:09 register
-rw-r--r-- 1 root root 0 11月 18 00:12 status

注意: 如果是centos的话,建议升级内核到4+以上

编译方法

大致有3种编译方法:

  1. 直接在对应架构的机器上进行
  2. 通过模拟器的方式,最常用的模拟器是开源的 QEMU
  3. 通过binfmt_misc模拟目标硬件的用户空间
  4. 通过交叉编译,像andriod程序,一般就是通过交叉编译而来,还有如go语言等,本身就有交叉编译器

这里要重点说的是docker的交叉编译

docker buildx

docker 在19.03的版本支持了一个实验性的功能 - - buildx,大大地减少了编译工作,但也还是有些坑,慢慢道来

利用 Docker 19.03 引入的插件 buildx,可以很轻松地构建多平台 Docker 镜像。buildx 是 docker build ... 命令的下一代替代品,它利用 BuildKit 的全部功能扩展了 docker build 的功能

这里不详细介绍该如此开启buildx了,大家可参考官方的handbook,这里假设开启了”experimental”: true

使用以下命令确认开启了:

1
2
docker info | grep -i exp
Experimental: true

Docker buildx需要开启binfmt_misc, 开启方法如下

验证是否启用了相应的处理器:

1
2
3
4
5
6
7
cat /proc/sys/fs/binfmt_misc/qemu-aarch64
enabled
interpreter /usr/bin/qemu-aarch64
flags: OCF
offset 0
magic 7f454c460201010000000000000000000200b7mask
ffffffffffffff00fffffffffffffffffeffff

Docker 默认会使用不支持多 CPU 架构的构建器,我们需要手动切换。

先创建一个新的构建器:

1
2
3
4
# 新建构建器
docker buildx create --use --name mybuilder
# 启动构建器
docker buildx inspect mybuilder --bootstrap

准备工作就绪后,下面以例子来说明,既然是交叉编译,那就得先有一个初始的x86 的Dockerfile,然后将其编译出arm64的镜像

下文说到的体系架构指的就是x86跟arm64.

Docker Manifests

这里先解释下docker的manifests特性,因为下面会用到

1
FROM golang:alpine AS builder

相信大家在编写Dockerfile的时候会经常这么用,正常来说,这么写的镜像都是从docker hub拉取的镜像,docker hub上的镜像支持docker的manifests特性,简单来说就是一个镜像地址,当我们docker pull的时候,它会根据当前机器体系架构来拉取对应于这个体系架构的镜像,比如你是x86的,则给你拉取的是x86的镜像,是arm64的机器拉取的则是arm64的,原理就在于每个镜像都维护着一个manifest(清单),这个清单上记录着每个体系架构对应用镜像层,在docker pull的时候,docker这时做为一个client端,与docker hub的服务端通信的时候,会先看这个镜像存不存在manifests,如果存在则拉取匹配当前机器体系架构的镜像,如果不存在,则就直接拉取.

了解这个对下面的实践很重要

实践

这里以go语言的应用为例,直接在x86的CentOS机器上,如果不做特别说明,以下的所有操作都是在该机器上执行

dockerfile如下:

1
2
3
4
5
6
7
8
9
10
11
FROM golang:alpine AS builder
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o hello . # 注意这里

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

这里要注意go build命令,

1
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o hello .

简单说一下go的交叉编译:

1
2
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go .
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build main.go .

GOOS指定的是目标机器的操作系统

GOARCH指定的是体系架构(i386、amd64、arm64)等

由于go是编译型语言,编译出来的都是二进制文件,这个二进制文件的运行环境一定是要跟编译时指的是编译参数一致,就好比,windows下的exe文件无法在linux上执行一样,如果指定了go编译的参数为amd64,则它就不能在arm64上运行,反过来同理

X86机器编译X86镜像

所以,x86正常编译的build命令为:

1
docker build -t xxx/yyy/zzz:latest .

由于是直接在x86的linux机器上,不存在交叉编译,所以可以直接使用build命令,最终产生的镜像就是一个可以运行在X86的机器上,如果运行在arm64的机器上,则会提示以下错误:

以上的错误基本都是运行的镜像与体系架构不匹配导致的

X86机器上编译arm64镜像

编译x86非常方便,也是我们最常规的操作,如果将上面的Dockerfile使用交叉编译编译出arm64的镜像呢

这里要注意的是,由于go本身就支持交叉编译,因此基础镜像是可以不用调整的,修改如下:

1
2
3
4
5
6
7
8
9
10
11
FROM golang:alpine AS builder # 注意这里
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o hello . # 修改的这里

FROM alpine # 注意这里
RUN mkdir /app
WORKDIR /app
COPY --from=builder /app/hello .
CMD ["./hello"]

使用以下的命令编译出来的镜像就是arm64的了

1
docker buildx build -t xxx/yyy/zzz:latest --platform=linux/arm64 . --push

–push 表示构建之后直接将该镜像推送到镜像仓库中

可以看到,Dockerfile文件修改只修改了1行,已经在注释中标注出来了

二个From 为何都不需要改呢? 这个放到下文解析–platform中再说

go build那行的命令GOARCH已经修改为arm64了,表明目标体系架构为arm64,那最终生成的二进制在arm64是可以运行的.

这里要重点说一下docker buildx --platform这个参数, 顺便解释一下为何两个From的基础镜像都不需要修改的原因

首先–platform这个参数决定了Dockerfile文件中所有From后面的镜像的体系架构以及最终生成的镜像的体系架构,像上面那样,直接是

FROM golang:alpine AS builder 相当于FROM --platform=arm64 golang:alpine AS builder,而

FROM alpine 相当于FROM –platform=arm64 alpine`, 拉取下来的就是arm64的镜像,因此上面是可以不用修改的

这里说的是From中没有指定--platform的情况则会自动添加上docker buildx --platform中的参数,如果本身就存在的话,则是以From本身的--platform决定

但是一定要确保,最后一个From的镜像体系架构一定要跟目标体系架构保持一致,也就是说,如果上面的例子中最后一个From改成

From alpine-amd64 (不一定有这个镜像,这里只是形象说明),docker buildx不会报错,会正常编译通过,在docker run 这个镜像的时候就会出现exec format error

这里可能有人会问: 那为什么arm64的镜像可以在x86上run起来呢?

这个其实就是binfmt_misc在起作用,模拟了目标体系架构

但这有一个问题:

很多时候,由于各种不可明说的原因,我们并不会每次都直接去docker hub上拉取镜像,而是将镜像推送到公司内的镜像仓库,如果恰好镜像仓库还不支持manifest这个特性的话(比如我司),那上面的例子就有坑了,因此,必须要这么写

1
2
3
4
5
6
7
8
9
10
11
FROM xxx/yyy/golang:alpine AS builder # 注意这里
RUN mkdir /app
ADD . /app/
WORKDIR /app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o hello . # 注意这里

FROM xxx/yyy/alpine-arm64 # 注意这里
RUN mkdir /app
WORKDIR /app
COPY --from=builder /app/hello .
CMD ["./hello"]

最后的一个FROM一定要是arm64的,由于是在x86的机器上使用交叉编译,如果镜像仓库又不支持manifest,则只能显示地指定使用arm64的镜像,这样打出来的镜像还会是arm64的

那为什么第一个FROM没有修改呢? 因为go本身是支持交叉编译的,虽然拉下来的是x86的镜像, 但go build出来的是arm64的,最后塞到alpine-arm64镜像里去的也是arm64的,这个是不冲突的,当然,第一个FROM换成arm64的golang:alpine也是没问题的

arm64机器编译arm64镜像

目标体系架构跟机器一致,这个就更没什么好说的了,直接使用最开始的Dockerfile就行

总结

这个例子中使用的是go,由于go本身就支持交叉编译,因此还算是方便

对于一些前端js的编译,也比较方便,因为js编译出来的都是一堆js文件,跟机器体系架构关系不大,

java的也是如此,天然的是通过java虚拟机的方式,屏蔽了底层的差异,跟机器体系架构关系不大

因此总结来说

对于交叉编译来说,保证最后一个FROM的镜像跟目标体系架构的一致即可

另外,docker buildx还有很多其它的选项,值得深入研究一番,有时间下次更

参考文章:

转载请注明原作者: 周淑科(https://izsk.me)

 wechat
Scan Me To Read on Phone
I know you won't do this,but what if you did?