buildx是Docker官方提供的一個構建工具,它可以幫助用戶快速、高效地構建Docker鏡像,并支持多種平臺的構建。使用buildx,用戶可以在單個命令中構建多種架構的鏡像,例如x86和arm架構,而無需手工操作多個構建命令。此外buildx還支持Dockerfile的多階段構建和緩存,這可以大大提供鏡像構建的效率和速度。
buildx是一個管理Docker構建的CLI插件,底層使用了BuildKit擴展了Docker構建功能。
BuildKit是Docker官方提供一個高性能構建引擎,可以用來替代Docker原有的構建引擎。相比于原有引擎,BuildKit具有更快的構建速度、更高的并行性、更少的資源占用和更好的安全性。
要安裝并使用buildx,需要Docker Engine版本號大于等于19.03。
跨平臺鏡像構建策略
builder支持是那種不通策略構建跨平臺鏡像。
在內核中使用QEMU仿真支持
如果你正在使用Docker Desktop,則已經支持了QEMU,QEMU是最簡單的構建跨平臺鏡像策略。它不需要對原有的Dockerfile進行任何更改,BuildKit會通過binfmt_misc這一Linux內核功能實現跨平臺程序的執行。
工作原理:
QEMU是一個處理器模擬器,可以模擬不通的CPU架構,我們可以把它理解為是另一種形式的虛擬機。在buildx中,QEMU用于在構建過程中執行非本地架構的二進制文件。例如,在x86主機上構建一個ARM鏡像時,QEMU可以模擬ARM環境并運行ARM二進制文件。
binfmt_misc是Linux內核的一個模塊,它允許用戶注冊可執行文件格式和響應的解釋器。當內核遇到未知格式的可執行文件時,會使用binfmt_misc查找與該文件格式關聯的解釋器(在這種情況下是QEMU)并運行文件。rk3568的平臺上此功能是關閉的。
QEMU和binfmt_misc的結合使得通過buildx跨平臺構建成為可能。這樣我們就可以在一個架構的主機上構建針對其他架構的Docker鏡像,而無需擁有實際的目標硬件。
雖然Docker Desktop預配置了binfmt_misc對其他平臺的支持,但對于其他版本Docker,你可能需要使用tonistiigi/binfmt鏡像啟動一個特權容器來進行支持。
docker run --privileged --rm tonistiigi/binfmt --install all
運行此命令會安裝不同架構的解釋器程序,即/usr/bin目錄下的qemu模擬器:
?在我們編譯服務器上加載了binfmt_misc模塊:
docker buildx使用qemu的作用
Docker Buildx 是 Docker 的一個實驗性功能,它擴展了 Docker 的構建功能,包括使用多節點、qemu 等。QEMU 是一個開源的虛擬機軟件,它可以模擬不同的 CPU 和其他硬件,使得我們可以在一個操作系統上構建另一種操作系統的鏡像。
使用 Docker Buildx 的 qemu 功能,可以幫助我們構建面向多種不同架構(如 ARM、MIPS 等)的 Docker 鏡像。
以下是一個簡單的例子,展示如何使用 Docker Buildx 和 QEMU 來構建一個面向 ARM 架構的 Docker 鏡像:
# 創建一個新的 buildkit 實例
docker buildx create --name mybuilder --use# 啟動 buildkit 實例
docker buildx start mybuilder# 啟用 QEMU 驅動支持
docker buildx inspect --bootstrap# 構建一個面向 ARM 架構的 Docker 鏡像
docker buildx build --platform linux/arm/v7 -t myimage:latest .
在這個例子中,--platform
?參數指定了我們想要構建的目標平臺是 ARM v7。這樣,Docker 會使用 QEMU 來模擬一個 ARM 環境,并在 x86 架構的機器上構建出面向 ARM 架構的 Docker 鏡像。
binfmt_misc文件系統
binfmt-misc是Linux內核提供的一種類似windows上文件關聯的功能,但比文件關聯更強大的是,它不僅可以根據文件后綴名判斷,還可以根據文件內容(Magic Bytes)使用不同的程序打開。一個典型的使用場景就是:使用qemu運行其他架構平臺上的二進制文件。
開啟binfmt-misc
臨時開啟可以使用以下命令:
$ sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
這種方式重啟后會失效,如果想長期生效,可以在/etc/fstab文件中增加一行:
none /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0
可以使用以下命令檢查開啟是否成功:
$ mount | grep binfmt_misc
binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,relatime)
$ ls -l /proc/sys/fs/binfmt_misc
總用量 0
--w------- 1 root root 0 2月 5 22:55 register
-rw-r--r-- 1 root root 0 2月 5 22:55 status
先準備一個arm64架構的程序,執行后發現有報錯:
bash: ./go-test:無法執行二進制文件: 可執行文件格式錯誤
現在,我們執行一下apt install qemu-user-binfmt命令,然后再運行上面的arm64程序,發現能正常運行了。安裝qemu-user-binfmt后,會在/proc/sys/fs/binfmt_misc目錄下創建若干個文件,其中就有一個qemu-aarch64,來看一下這個文件的內容:
root@ubuntu:/proc/sys/fs/binfmt_misc# cat qemu-aarch64
enabled
interpreter /usr/bin/qemu-aarch64-static
flags: OC
offset 0
magic 7f454c460201010000000000000000000200b700
mask ffffffffffffff00fffffffffffffffffeffffff
root@ubuntu:/proc/sys/fs/binfmt_misc#
這個文件描述的是規則文件:
第一行enabled表示該規則啟用?;
第二行 interpreter /usr/bin/qemu-aarch64-static表示使用/usr/bin/qemu-aarch64-static來執行二進制文件;
第三行:OC表示運行的標志位,具體含義如下:
?P: 表示perserve-argv,這意味著在調用模擬器時,原始的參數(argv)將被保留。這對于默寫程序在運行時需要知道它們自己的名稱(即argv[0])的情況很有用
O: 表示offset,這意味著在啟動模擬器之前,需要從二進制文件中讀取一個偏移量,這個偏移量將作為模擬器的一個參數
C:表示credentials,這意味著模擬器將使用于原始程序相同的用戶ID和組ID運行,這有助于確保模擬器在運行時與原始程序相同的權限。
第四行:offset 0表示從0便宜值開始讀取文件;
第五行:?magic 7f454c460201010000000000000000000200b700表示要匹配的模數字節;
arm64架構的ELF文件頭部的magic字段如下,也就是說binfmt_misc文件系統可以根據ELF文件中的magic字段來決定此文件使用哪個架構的模擬器運行:
下面是兩個不同架構的?
mips架構的:? 7f454c4601020100000000000000000000020008
arm64架構的:7f454c460201010000000000000000000200b700
第六行:mask ffffffffffffff00fffffffffffffffffeffffff 表示字節掩碼,用哦過來忽略掉文件中的一些不重要的字節。
在x86_64系統中運行arm64架構的Docker鏡像
現在我們用docker命令運行一個arm64的鏡像:
$ docker run -it arm64v8/ubuntu bash
Unable to find image 'arm64v8/ubuntu:latest' locally
latest: Pulling from arm64v8/ubuntu
005e2837585d: Pull complete
Digest: sha256:ba545858745d6307f0d1064d0d25365466f78d02f866cf4efb9e1326a4c196ca
Status: Downloaded newer image for arm64v8/ubuntu:latest
standard_init_linux.go:207: exec user process caused "no such file or directory"
通過一番探索之后,發現只要執行下命令:apt install qemu-user-static
,再啟動docker容器就正常了。
使用 Dockerfile 中的多階段交叉構建
交叉編譯的復雜度不在于Docker,而是取決于程序本身。比如Go程序就很容易實現交叉編譯,只需要在使用go build構建程序時執行GOOS、GOARCH兩個環境變量即可實現。
創建builder
要使用buildx構建跨 平臺鏡像,我們需要先創建一個builder,可以翻譯為構建器。
使用docker buildx ls命令可以查看builder列表:
root@ubuntu:/proc# docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
mybuild * docker-container mybuild0 unix:///var/run/docker.sock running linux/arm64*, linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/amd64/v4, linux/386
vigilant_hugle docker-container vigilant_hugle0 unix:///var/run/docker.sock stopped
default docker default default running linux/amd64, linux/386
*號表示當前正在使用的builder,當我們運行docker build命令時就是在使用此builder構建鏡像。第二列的DRIVER/ENDPOINT表示使用的驅動程序。buildx支持以下幾種驅動程序:
- docker:使用捆綁到Docker守護進程中的BuildKit庫,就是安裝Docker后默認的BuildKit。
- docker-container:使用Docker新創建一個專用的BuildKit容器。
- kubernetes: 在kubernetes集群中創建一個BuildKit Pod。
- remote:直接連接到手工管理的BuildKit守護進程。
因為使用docker驅動程序的默認builder不支持使用單條命令(默認builder的--platform參數只接收單個值)構建跨平臺鏡像,所以我們需要使用docker-container驅動創建一個新的builder。
命令語法如下:
$ docker buildx create --name=<builder-name> --driver=<driver> --driver-opt=<driver-options>
參數含義如下;
--name: 構建起名稱,必填。
--driver:構建器驅動程序,默認為docker-container。
--driver-opt:驅動程序選項,如選項--driver-opt=image=moby/buildkit:v0.11.3可以安裝指定版本的BuildKit,默認值為moby/buildkit。
我們可以使用如下命令創建一個新的builder:
$ docker buildx create --name mybuilder
mybuilder
再次查看 builder 列表:
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
mybuilder * docker-containermybuilder0 unix:///var/run/docker.sock inactive
default dockerdefault default running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux dockerdesktop-linux desktop-linux running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
?可以發現選中的構建起已經切換到了Mybuilder,如果沒有選中,你需要手動使用docker buildx use mybuilder命令切換構建器。
啟動 builder
?我們新創建的mybuilder當前狀態為inactive,需要啟動才能使用。
$ docker buildx inspect --bootstrap mybuilder
[+] Building 16.8s (1/1) FINISHED=> [internal] booting buildkit 16.8s=> => pulling image moby/buildkit:buildx-stable-1 16.1s=> => creating container buildx_buildkit_mybuilder0 0.7s
Name: mybuilder
Driver: docker-containerNodes:
Name: mybuilder0
Endpoint: unix:///var/run/docker.sock
Status: running
Buildkit: v0.9.3
Platforms: linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
inspect子命令用來檢查構建起狀態,使用--bootstrap參數可以啟動mybuilder構建器,再次查看builder列表,mybuilder狀態已經變成了running。
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
mybuilder * docker-containermybuilder0 unix:///var/run/docker.sock running v0.9.3 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
default dockerdefault default running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
desktop-linux dockerdesktop-linux desktop-linux running 20.10.21 linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
其中PLATFORMS一列所展示的值
?linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6 就是當前構建器所支持的所有平臺了。
現在使用docker ps命令可以看到mybuilder構建器所對應的BuildKit容器已經啟動。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b8887f253d41 moby/buildkit:buildx-stable-1 "buildkitd" 4 minutes ago Up 4 minutes buildx_buildkit_mybuilder0
這個容器就是輔助我們構建跨平臺鏡像用的,不要手動刪除它。
使用 builder 構建跨平臺鏡像?
$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go .
docker buildx build 語法跟 docker build 一樣, --platform 參數表示構建鏡像的目標平臺, -t 表示鏡像的 Tag, . 表示上下文為當前目錄。?
唯一不通的是對--platform參數的支持,docker build的--platform參數只支持傳遞一個平臺信息,如--platform linux/arm64,也就是一次能構建單個平臺的鏡像。
而使用docker buildx build構建鏡像則支持同時傳遞多個平臺信息,中間使用英文逗號分隔,這樣就實現了只用一條命令便可以構建多個跨平臺鏡像的功能。
執行以上命令后,我們將會得到一條警告:
WARNING: No output specified with docker-container driver. Build result will only remain in the build cache. To push result image into registry use --push or to load image into docker use --load
這條警告提示我們沒有為docker-container驅動程序指定輸出,生成結果將只會保留在構建緩存中,使用--push可以將鏡像推送到Docker Hub遠程倉庫,使用--load可以將鏡像保存在本地。
這是因為我們新創建的mybuilder是啟動了一個容器來運行BuildKit,它并不能直接將構建好的跨平臺鏡像輸出到本機或推送到遠程,必須要用戶來手動指定輸出位置。
我們可以嘗試指定--load將鏡像保存在本地主機。
$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go . --load
[+] Building 0.0s (0/0)
ERROR: docker exporter does not currently support exporting manifest lists
結果會得到一條錯誤日志。看來它并不支持將跨平臺鏡像輸出到本地,這其實是因為傳遞了多個--platform的關系,如果--platform只傳遞了一個平臺,則可以使用--load將構建好的鏡像輸出到本機。
那么我們就只能通過--push參數將跨平臺鏡像推送到遠程倉庫了。不過在此之前需要確保使用docker login完成登錄。
$ docker buildx build --platform linux/arm64,linux/amd64 -t jianghushinian/hello-go . --push
現在登錄Docker Hub就可以看見推送上來的跨平臺鏡像了。
我們也可以使用imagestools來檢查跨平臺鏡像的manifest信息,這條命令只能用來獲取倉庫中的image信息,本地的images無法查看。
$ docker buildx imagetools inspect jianghushinian/hello-go
Name: docker.io/jianghushinian/hello-go:latest
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest: sha256:51199dadfc55b23d6ab5cfd2d67e38edd513a707273b1b8b554985ff562104dbManifests:Name: docker.io/jianghushinian/hello-go:latest@sha256:8032a6f23f3bd3050852e77b6e4a4d0a705dfd710fb63bc4c3dc9d5e01c8e9a6MediaType: application/vnd.docker.distribution.manifest.v2+jsonPlatform: linux/arm64Name: docker.io/jianghushinian/hello-go:latest@sha256:fd46fd7e93c7deef5ad8496c2cf08c266bac42ac77f1e444e83d4f79d58441baMediaType: application/vnd.docker.distribution.manifest.v2+jsonPlatform: linux/amd64
?
可以看到,這個跨平臺鏡像包含了兩個目標平臺的鏡像,分別是linux/arm64和linux/amd64。?