在工作和生活中,我們可能經常需要將某個程序跑在不同的 CPU 架構上,比如讓某些不可描述的軟件運行在樹莓派或嵌入式路由器設備上。特別是 Docker 席卷全球之后,我們可以輕松地在 ARM 設備上通過容器部署各種好玩的應用,而不用在意各種系統的差異性。
但是想要跨平台構建 Docker 鏡像可不是一件輕松的活,要么到不同 CPU 架構的系統上全部構建一遍,要么就得在當前系統上通過虛擬化技術模擬不同的 CPU 架構,最后可能還要想辦法合並鏡像,費力不討好。
不過值得慶幸的是,Docker 19.03 引入了一個新的實驗性插件,該插件使得跨平台構建 Docker 鏡像比以往更加容易了。在介紹這個新特性之前,我們先來了解一下跨 CPU 架構構建程序的基礎知識。
01
跨 CPU 架構編譯程序的方法
先來快速回顧一下當前跨 CPU 架構編譯程序的不同方法。
方法一:直接在目標硬件上編譯
如果你能夠訪問目標 CPU 架構的系統,並且該操作系統支持運行構建所需的各種工具,那么你可以直接在目標系統上編譯程序。
以構建 Docker 鏡像為例,你可以在樹莓派上安裝 Docker,然后在樹莓派上通過 Dockerfile 直接構建 arm 平台的鏡像。
如果無法訪問目標 CPU 架構的系統該怎么辦?有沒有辦法通過某種方式直接在當前系統上構建目標 CPU 架構的程序?請看下文...
方法二:模擬目標硬件
還記得我們小時候在各種網吧台球室之類的場合玩的街機游戲嗎?放張圖給你們回憶一下:
如果現在我們想重新體驗以前玩過的街機游戲該怎么辦?這時候就需要用到模擬器(Emulator)了。借助模擬器,我們可以讓時光倒流,體驗經典游戲的樂趣。
模擬器除了可以用來玩游戲之外,還可以用來跨 CPU 架構構建程序。最常用的模擬器是開源的 QEMU[1],QEMU 支持許多常見的 CPU 架構,包括 ARM、Power-PC 和 RISC-V 等。通過模擬一個完整的操作系統,可以創建通用的 ARM 虛擬機,該虛擬機可以引導 Linux,設置開發環境,也可以在虛擬機內編譯程序。
然而,模擬整個操作系統還是有點浪費,因為在這種模式下,QEMU 將會模擬整個系統,包括計時器、內存控制器、總線控制器等硬件。但編譯程序根本不需要關心這些,還可以再精簡些。
方法三:模擬目標硬件的用戶空間
在 Linux 上,QEMU 除了可以模擬完整的操作系統之外,還有另外一種模式叫用戶態模式(User mod)。該模式下 QEMU 將通過 binfmt_misc[2] 在 Linux 內核中注冊一個二進制轉換處理程序,並在程序運行時動態翻譯二進制文件,根據需要將系統調用從目標 CPU 架構轉換為當前系統的 CPU 架構。最終的效果看起來就像在本地運行目標 CPU 架構的二進制文件。
通過 QEMU 的用戶態模式,我們可以創建輕量級的虛擬機(chroot[3] 或容器),然后在虛擬機系統中編譯程序,和本地編譯一樣簡單輕松。后面我們就會看到,跨平台構建 Docker 鏡像用的就是這個方法。
方法四:使用交叉編譯器
最后介紹一種嵌入式系統社區常用的方法:交叉編譯(cross-compilation)。
交叉編譯器是專門為在給定的系統平台上運行而設計的編譯器,但是可以編譯出另一個系統平台的可執行文件。例如,amd64 架構的 Linux 系統上的 C++ 交叉編譯器可以編譯出運行在 aarch64(64-bit ARM) 架構的嵌入式設備上的可執行文件。再舉個真實的例子,安卓設備的 APP 基本上都是通過這種方法來編譯的。
從性能角度來看,該方法與方法一沒什么區別,因為不需要模擬器的參與,幾乎沒有性能損耗。但交叉編譯不具有通用性,它的復雜度取決於程序使用的語言,如果使用 Golang 的話,那就超級容易了。
在全民容器時代,我們討論構建時不僅包括構建單個可執行文件,還包括構建容器鏡像。而且構建容器鏡像比上面說的方法更復雜,再加上 Docker 本身的復雜性,這幾乎是一個老大難的問題。
但引入了新的實驗性插件之后,構建多平台架構的 Docker 鏡像就比以前容易多了,至於這個插件到底是啥,下文會詳細介紹。
02
構建多平台 Docker 鏡像
利用 Docker 19.03 引入的插件 buildx[4],可以很輕松地構建多平台 Docker 鏡像。buildx 是 docker build ... 命令的下一代替代品,它利用 BuildKit[5] 的全部功能擴展了 docker build 的功能。
下面就來演示一下如何在短短幾分鍾內使用 buildx 構建出不同平台的 Docker 鏡像。步驟如下:
啟用 buildx 插件
要想使用 buildx,首先要確保 Docker 版本不低於 19.03,同時還要通過設置環境變量 DOCKER_CLI_EXPERIMENTAL 來啟用。可以通過下面的命令來為當前終端啟用 buildx 插件:
🐳 → export DOCKER_CLI_EXPERIMENTAL=enabled
驗證是否開啟:
🐳 → docker buildx version
github.com/docker/buildx v0.3.1-tp-docker 6db68d029599c6710a32aa7adcba8e5a344795a7
如果在某些系統上設置環境變量 DOCKER_CLI_EXPERIMENTAL 不生效(比如 Arch Linux),你可以選擇從源代碼編譯:
🐳 → export DOCKER_BUILDKIT=1
🐳 → docker build --platform=local -o . git://github.com/docker/buildx
🐳 → mkdir -p ~/.docker/cli-plugins && mv buildx ~/.docker/cli-plugins/docker-buildx
啟用 binfmt_misc
如果你使用的是 Docker 桌面版(MacOS 和 Windows),默認已經啟用了 binfmt_misc,可以跳過這一步。
如果你使用的是 Linux,需要手動啟用 binfmt_misc。大多數 Linux 發行版都很容易啟用,不過還有一個更容易的辦法,直接運行一個特權容器,容器里面寫好了設置腳本:
🐳 → docker run --rm --privileged docker/binfmt:66f9012c56a8316f9244ffd7622d7c21c1f6f28d
建議將 Linux 內核版本升級到 4.x 以上,特別是 CentOS 用戶,你可能會遇到錯誤。
驗證是 binfmt_misc 否開啟:
🐳 → ls -al /proc/sys/fs/binfmt_misc/
總用量 0
總用量 0
-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
驗證是否啟用了相應的處理器:
🐳 → cat /proc/sys/fs/binfmt_misc/qemu-aarch64
enabled
interpreter /usr/bin/qemu-aarch64
flags: OCF
offset 0
magic 7f454c460201010000000000000000000200b7
mask ffffffffffffff00fffffffffffffffffeffff
從默認的構建器切換到多平台構建器
Docker 默認會使用不支持多 CPU 架構的構建器,我們需要手動切換。
先創建一個新的構建器:
🐳 → docker buildx create --use --name mybuilder
啟動構建器:
🐳 → docker buildx inspect mybuilder --bootstrap
[+] Building 5.0s (1/1) FINISHED
=> [internal] booting buildkit 5.0s
=> => pulling image moby/buildkit:buildx-stable-1 4.4s
=> => creating container buildx_buildkit_mybuilder0 0.6s
Name: mybuilder
Driver: docker-container
Nodes:
Name: mybuilder0
Endpoint: unix:///var/run/docker.sock
Status: running
Platforms: linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
查看當前使用的構建器及構建器支持的 CPU 架構,可以看到支持很多 CPU 架構:
🐳 → docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
mybuilder * docker-container
mybuilder0 unix:///var/run/docker.sock running linux/amd64, linux/arm64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default docker
default default running linux/amd64, linux/386
構建多平台鏡像
現在我們就可以構建支持多 CPU 架構的鏡像了!假設有一個簡單的 golang 程序源碼:
🐳 → cat hello.go
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("Hello, %s!\n", runtime.GOARCH)
}
創建一個 Dockerfile 將該應用容器化:
🐳 → cat 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"]
這是一個多階段構建 Dockerfile,使用 Go 編譯器來構建應用,並將構建好的二進制文件拷貝到 alpine 鏡像中。
現在就可以使用 buildx 構建一個支持 arm、arm64 和 amd64 多架構的 Docker 鏡像了,同時將其推送到 Docker Hub[6]:
🐳 → docker buildx build -t yangchuansheng/hello-arch --platform=linux/arm,linux/arm64,linux/amd64 . --push
需要提前通過 docker login 命令登錄認證 Docker Hub。
現在就可以通過 docker pull mirailabs/hello-arch 拉取剛剛創建的鏡像了,Docker 將會根據你的 CPU 架構拉取匹配的鏡像。
背后的原理也很簡單,之前已經提到過了,buildx 會通過 QEMU 和 binfmt_misc 分別為 3 個不同的 CPU 架構(arm,arm64 和 amd64)構建 3 個不同的鏡像。構建完成后,就會創建一個 manifest list[7],其中包含了指向這 3 個鏡像的指針。
如果想將構建好的鏡像保存在本地,可以將 type 指定為 docker,但必須分別為不同的 CPU 架構構建不同的鏡像,不能合並成一個鏡像,即:
🐳 → docker buildx build -t yangchuansheng/hello-arch --platform=linux/arm -o type=docker .
🐳 → docker buildx build -t yangchuansheng/hello-arch --platform=linux/arm64 -o type=docker .
🐳 → docker buildx build -t yangchuansheng/hello-arch --platform=linux/amd64 -o type=docker .
測試多平台鏡像
由於之前已經啟用了 binfmt_misc,現在我們就可以運行任何 CPU 架構的 Docker 鏡像了,因此可以在本地系統上測試之前生成的 3 個鏡像是否有問題。
首先列出每個鏡像的 digests:
🐳 → docker buildx imagetools inspect yangchuansheng/hello-arch
Name: docker.io/yangchuansheng/hello-arch:latest
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest: sha256:ec55f5ece9a12db0c6c367acda8fd1214f50ee502902f97b72f7bff268ebc35a
Manifests:
Name: docker.io/yangchuansheng/hello-arch:latest@sha256:38e083870044cfde7f23a2eec91e307ec645282e76fd0356a29b32122b11c639
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/arm/v7
Name: docker.io/yangchuansheng/hello-arch:latest@sha256:de273a2a3ce92a5dc1e6f2d796bb85a81fe1a61f82c4caaf08efed9cf05af66d
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/arm64
Name: docker.io/yangchuansheng/hello-arch:latest@sha256:8b735708d7d30e9cd6eb993449b1047b7229e53fbcebe940217cb36194e9e3a2
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/amd64
運行每一個鏡像並觀察輸出結果:
So cool!
03
總結
回顧一下,本文帶大家了解了在不同的 CPU 架構上運行軟件的挑戰性,以及 buildx 如何幫助我們解決了其中的一些挑戰。使用 buildx,我們無需對 Dockerfile 進行任何修改,就可以創建支持多種 CPU 架構的 Docker 鏡像,然后將其推送到 Docker Hub。任何安裝了 Docker 的系統都可以拉取到與它的 CPU 架構相對應的鏡像。
未來 buildx 可能會成為 docker build 命令的一部分,最終所有上面提到的功能都會變成默認的功能,下沉到基礎設施中交叉編譯程序的做法將會變成遠古時代的愚蠢行為。
04
參考資料
Building Multi-Arch Images for Arm and x86 with Docker Desktop [8]
Getting started with Docker for Arm on Linux [9]
Leverage multi-CPU architecture support [10]
腳注
[1]
QEMU: https://www.wikiwand.com/zh-hans/QEMU
[2]
binfmt_misc: https://en.wikipedia.org/wiki/Binfmt_misc
[3]
chroot: https://en.wikipedia.org/wiki/Chroot
[4]
buildx: https://github.com/docker/buildx
[5]
BuildKit: https://github.com/moby/buildkit
[6]
Docker Hub: https://hub.docker.com/
[7]
manifest list: https://docs.docker.com/engine/reference/commandline/manifest/
[8]
Building Multi-Arch Images for Arm and x86 with Docker Desktop: https://engineering.docker.com/2019/04/multi-arch-images/
[9]
Getting started with Docker for Arm on Linux: https://engineering.docker.com/2019/06/getting-started-with-docker-for-arm-on-linux/
[10]
Leverage multi-CPU architecture support: https://docs.docker.com/docker-for-mac/multi-arch/