前言
愿景與現(xiàn)實
早在1995年,就有“write once and run anywhere”(WORA,編寫一次即可在任何地方運行)用于描述Java應用程序。時過20年,Docker高聲喊出了自己的口號——“Build Once,Run Anywhere”(一次構建,隨處可用)。
愿望是美好的,然而,現(xiàn)實總比理想骨感。Linux、Windows這些不同的操作系統(tǒng)擁有不同的系統(tǒng)API;x86、Arm、IBM PowerPC這些不同的硬件平臺的指令集不同,某些同平臺的硬件甚至擁有不同的專用指令集用于加速應用。一次構建,隨處可用面臨著巨大的挑戰(zhàn),要構建能夠在不同操作系統(tǒng)、不同硬件平臺的運行的應用程序,仍然需要工程師們針對具體的操作系統(tǒng)和硬件平臺進行海量的移植工作。
Why and How
既然多平臺的支持這么麻煩、充滿挑戰(zhàn),我們是不是可以放棄支持?然而隨著國產化大潮和IoT物聯(lián)網的來臨,我們編寫的應用程序不僅僅會在X86服務器上運行,新時代的工程師們不得不面對更多的硬件平臺,放棄多平臺的支持無疑是放棄更寬廣的未來。多平臺的支持,勢在必行。
我們正處在一個波瀾壯闊的大時代中,新技術、新工具在一次次的迭代升級,不斷從Proposal(提議)到Prototype(原型),再逐漸的實用化。
虛擬化技術使得我們可以做到模擬其它硬件平臺;Docker等容器技術打破混亂,讓開發(fā)、編譯、運行環(huán)境一致化;Golang、Rust這樣原生支持多系統(tǒng)多平臺的編程語言屏蔽大量底層差異,降低跨平臺應用的開發(fā)難度。這一系列前人智慧火花匯聚到一起,發(fā)生了奇妙的反應——WORA真正變的觸手可及,夢想的陽光已經照進現(xiàn)實。
本篇章會大量分析技術原理及實現(xiàn)細節(jié),對于希望快速GET可執(zhí)行方案的同學,可以直接跳轉到最后部分【可執(zhí)行方案回顧】查看。
如何支持多平臺
要了解容器鏡像是如何支持多平臺的,那我們需要仔細聊聊Manifest。使用過容器技術的同學都知道,我們運行容器所使用的鏡像是由多層構成的,而這些層的清單和其它容器信息共同存放在Manifest當中。
Manifest
我們通常使用的容器鏡像是x86平臺的,執(zhí)行docker manifest inspect harbor-community.tencentcloudcr.com/multi-arch/alpine命令可以查看鏡像harbor-community.tencentcloudcr.com/multi-arch/alpine的Manifest內容是一個JSON對象(如代碼段-01所示)。各個字段的解釋如下:
1.mediaType字段聲明這是一個V2 Manifes。
2.schemaVersion版本。
3.config鏡像配置信息。
4.layers鏡像層信息。
// 代碼段-01
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1507,
"digest": "sha256:f70734b6a266dcb5f44c383274821207885b549b75c8e119404917a61335981a"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2813316,
"digest": "sha256:cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08"
}
]
}
顯而易見的是,Manifest當中并沒有任何字段描述鏡像的平臺信息。那應該怎么樣支持多平臺呢?
我們可以設想一個簡單粗暴的,無視鏡像的平臺,強行把交叉編譯出來的其它平臺的二進制程序添加到鏡像內,使用Repository名稱或者Tag名稱來區(qū)分不同平臺的鏡像,例如coredns/coredns:coredns-arm64。在使用的時候,人工或者通過腳本判斷應該拉取那個鏡像。
Schema 2
Ohhhhh,這當然可以跑起來,但是難免太挫了吧?事實上,早在2015年底Docker社區(qū)的manifest v2.2規(guī)格文檔(也叫Schema 2,參見https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md)中就提及了多平臺鏡像,該功能通過manifest list(也叫做fat manifest)引用多個不同平臺鏡像的Manifest實現(xiàn)。
首先讓我們看看manifest是什么樣的,執(zhí)行docker manifest inspect alpine命令可以查看Docker Hub上的多平臺鏡像alpine的Manifest。
// 代碼段-02
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 528,
"digest": "sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 528,
"digest": "sha256:71465c7d45a086a2181ce33bb47f7eaef5c233eace65704da0c5e5454a79cee5",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v6"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 528,
"digest": "sha256:c929c5ca1d3f793bfdd2c6d6d9210e2530f1184c0f488f514f1bb8080bb1e82b",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v7"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 528,
"digest": "sha256:3b3f647d2d99cac772ed64c4791e5d9b750dd5fe0b25db653ec4976f7b72837c",
"platform": {
"architecture": "arm64",
"os": "linux",
"variant": "v8"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 528,
"digest": "sha256:90baa0922fe90624b05cb5766fa5da4e337921656c2f8e2b13bd3c052a0baac1",
"platform": {
"architecture": "386",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 528,
"digest": "sha256:5d950b30f229f0c53dd7dd7ed6e0e33e89d927b16b8149cc68f59bbe99219cc1",
"platform": {
"architecture": "ppc64le",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 528,
"digest": "sha256:a5426f084c755f4d6c1d1562a2d456aa574a24a61706f6806415627360c06ac0",
"platform": {
"architecture": "s390x",
"os": "linux"
}
}
]
}
可以看出來manifest list是一個JSON數(shù)組,數(shù)組當中應用了不同平臺鏡像的Manifest。所以,推送多平臺鏡像時,我們需要先分別推送不同平臺的鏡像層;然后創(chuàng)建manifest list,再引用平臺鏡像的Manifest,最后把manifest list上傳到Registry服務。而拉取鏡像時,客戶端應當設置HTTP的請求頭字段Accept值為application/vnd.docker.distribution.manifest.v2+json和application/vnd.docker.distribution.manifest.list.v2+json,然后檢查服務端返回的響應頭字段Content-Type判斷是舊鏡像格式,新鏡像格式或者時鏡像清單。
拋開規(guī)格文檔來說,只要我們使用的Registry服務的Distribution版本不低于v2.3,Docker CLI版本不低于v1.10就能過支持多平臺鏡像功能。
構建多平臺鏡像
要構建多平臺的容器鏡像,我們需要確保容器基礎鏡像和應用程序的代碼或者二進制都是目標平臺的。
程序代碼
一些編程語言的編譯器能夠為其它平臺編譯二進制文件,最為著名的包括Golang和Rust。我們將使用Golang編寫一個演示用web程序——通過HTTP訪問查看web服務程序的操作系統(tǒng)、硬件平臺等信息。具體代碼如代碼段-03所示。
// 代碼段-03
package main
import (
"net/http"
"runtime"
"github.com/gin-gonic/gin"
)
var (
r = gin.Default()
)
func main() {
r.GET("/", indexHandler)
r.Run(":9090")
}
func indexHandler(c *gin.Context) {
var osinfo = map[string]string{
"arch": runtime.GOARCH,
"os": runtime.GOOS,
"version": runtime.Version(),
}
c.JSON(http.StatusOK, osinfo)
}
我們在MacOS上使用go run運行代碼段-03,httpie工具訪問本機:9090端口,將會看見如下信息。
代碼準備好了,現(xiàn)在我們有兩種構建方法:手動編譯,使用docker build構建鏡像;使用docker buildx工具自動化編譯構建。
手動編譯構建
前置條件
·Dockerd啟用experimental
我們需要在Docker daemon配置文件中配置"experimental":true開啟實驗性功能:
$vi/etc/docker/daemon.json
{
"experimental":true
}
修改Docker daemon配置需要重啟服務使配置生效:
$sudo systemctl restart docker.service
使用docker version命令查看版本信息,配置生效后可以看到Server:Docker Engine中有Experimental:true:
$ sudo docker version
Client: Docker Engine - Community
Version: 19.03.12
API version: 1.40
Go version: go1.13.10
Git commit: 48a66213fe
Built: Mon Jun 22 15:45:36 2020
OS/Arch: linux/amd64
Experimental: false
Server: Docker Engine - Community
Engine:
Version: 19.03.12
API version: 1.40 (minimum version 1.12)
Go version: go1.13.10
Git commit: 48a66213fe
Built: Mon Jun 22 15:44:07 2020
OS/Arch: linux/amd64
Experimental: true
containerd:
Version: 1.2.13
GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429
runc:
Version: 1.0.0-rc10
GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd
docker-init:
Version: 0.18.0
GitCommit: fec3683
`
如果您使用的Docker CLI版本低于v20.10,執(zhí)行docker manifest命令會看到報錯提示docker manifest is only supported on a Docker cli with experimental cli features enabled,此時我們需要執(zhí)行export DOCKER_CLI_EXPERIMENTAL="enabled"開啟客戶端實驗特性支持。在v20.10及以上版本的Docker CLI會默認開啟實驗特性,無需額外操作。
交叉編譯
在我們的Golang代碼中沒有使用CGO的時候,通過簡單設置環(huán)境變量就能夠交叉編譯出其它平臺和操作系統(tǒng)上能夠執(zhí)行的二進制文件。其中:
GOARCH用于指定編譯的目標平臺,如amd64、arm64、riscv64等平臺。
GOOS用于指定編譯的目標系統(tǒng),如darwin、linux。
本篇中,我們構建能夠在Linux發(fā)行版中執(zhí)行的容器鏡像,所以編譯目標系統(tǒng)環(huán)境變量GOOS統(tǒng)一設置為linux。執(zhí)行代碼段0-4中的命令構建出二進制文件備用。
// 代碼段-04
#!/bin/bash
IMAGE?=kofj/multi-demo
NOCOLOR:='\033[0m'
RED:='\033[0;31m'
GREEN:='\033[0;32m'
BUILD_ARCH?=$(uname -m)
BUILD_OS?=$(uname -s)
BUILD_PATH:=build/docker/linux
LINUX_ARCH?=amd64 arm64 riscv64
LDFLAGS:="-s -w -X github.com/kofj/multi-arch-demo/cmd/info.BuildArch=$(BUILD_ARCH) -X github.com/kofj/multi-arch-demo/cmd/info.BuildOS=$(BUILD_OS)"
for arch in ${LINUX_ARCH}; do
echo ===================;
echo ${GREEN}Build binary for ${RED}linux/$$arch${NOCOLOR};
echo ===================;
GOARCH=$$arch GOOS=linux go build -o ${BUILD_PATH}/$$arch/webapp -ldflags=${LDFLAGS} -v cmd/main.go;
done
構建各個平臺的鏡像
首先,我們編寫一個Dockerfile用于構建鏡像。
FROM scratch
LABEL authors="Fanjian Kong"
ADD webapp/app/
WORKDIR/app
CMD["/app/webapp"]
然后,分別構建不同平臺的鏡像,可以使用如代碼段-05的腳本幫助構建。
//代碼段-05
#!/bin/bash
IMAGE?=kofj/multi-demo
NOCOLOR:='33[0m'
RED:='33[0;31m'
GREEN:='33[0;32m'
LINUX_ARCH?=amd64 arm64 riscv64
BUILD_PATH:=build/docker/linux
for arch in${LINUX_ARCH};do
echo===================;
echo${GREEN}Build docker image for${RED}linux/$$arch${NOCOLOR};
echo===================;
cp Dockerfile.slim${BUILD_PATH}/$$arch/Dockerfile;
docker build-t${IMAGE}:$$arch${BUILD_PATH}/$$arch;
done