容器鏡像多架構支持介紹

容器鏡像多架構支持介紹

簡介

出於開發需要,我們經常會需要瀏覽公共鏡像庫,以選取合適的基礎鏡像,在瀏覽過程中,不經意地會發現部分鏡像的一個tag下列出了許多種架構,如下圖所示,debian:bullseye這個鏡像的tag共享了八種平台架構之多。難道debian的維護團隊每天都在用那麼多架構的機器不停地構建並推送鏡像?而一個tag又是怎麼共享這麼多平台架構的?接下來本文將詳細地介紹這些概念。

基礎概念

鏡像manifest清單

manifest清單在廣義上是指容器鏡像的元數據文件,是獲取容器鏡像的入口。一個鏡像tag對應着一個清單文件,這個文件包含有眾多字段,解釋如下:

  • schemaVersion:鏡像清單的格式版本,目前使用的格式一般為2;
  • mediaType:清單文件的MIME格式,不同的清單文件可能有不同的MIME格式,用於表示該清單文件中包含的內容;
  • config:鏡像清單更為詳細的元數據,可能包括鏡像的創建時間、平台與架構、環境變量、入口點命令、標籤、鏡像層級以及構建歷史等信息;
  • layers:鏡像的層級信息,記錄了鏡像的每一層所存放的位置;
  • annotations:鏡像的註解,輔助實現某些功能。

如下是一個典型的清單文件,當拉取鏡像時,客戶端會依次執行如下步驟:

  1. 將輸入的鏡像tag轉換成一個完整的URL;
  2. 通過URL訪問遠程存儲庫中的manifest.json文件;
  3. 解析該文件,通過該文件中的config和layers字段獲取同目錄下的其餘文件,從而獲取該鏡像的元數據和鏡像層級文件;
  4. 將獲取到的鏡像元數據與鏡像層級文件保存至本地存儲目錄。

可以看到,清單文件實際上充當了鏡像獲取入口點的作用,只要獲取了清單文件,便可以進一步獲取鏡像的元數據和層級文件,從而完成拉取鏡像的過程。

然而,另一方面也可以看到這個清單文件中是沒有架構相關的信息的,這意味着一個tag並不能包含多個架構的鏡像,而客戶端想要僅通過清單文件便能拉取其對應架構的鏡像也顯而易見是不可能的,頂多在元數據文件拉取完畢後恍然發現和本地架構不匹配,隨後默默地彈出一個warning。

為了能夠讓一個鏡像tag支持多種架構,社區的開發者們使用了一種很巧妙的解決方法:清單組清單。一個清單組的清單文件並不直接表示鏡像信息,而是使用了一個列表指向了該清單中包含的多份子清單文件,每一份子清單文件均表示一種架構的鏡像清單。

如下是一個典型的清單組清單,該清單使用了特定的mediaType:application/vnd.oci.image.index.v1+json,用以表示該文件是一份清單組,與此同時使用manifests列表納入所有不同架構的子清單信息。如果此時linux-arm64架構的客戶端想要拉取該鏡像,那麼它首先會獲取清單組清單文件,通過架構過濾manifests列表從而獲取目標清單,然後才會通過目標清單獲取鏡像的元數據以及鏡像層級。

{
    "schemaVersion":2,
    "mediaType":"application/vnd.oci.image.index.v1+json",
    "manifests":[
        {
            "mediaType":"application/vnd.oci.image.manifest.v1+json",
            "digest":"sha256:209888c481a024798fc058a4809c3b8e90a847edaa521b467ad11920fec643b4",
            "size":1359,
            "platform":{
                "architecture":"amd64",
                "os":"linux"
            }
        },
        {
            "mediaType":"application/vnd.oci.image.manifest.v1+json",
            "digest":"sha256:1e7b1a1f8a23e3a626c9e23aab9d1cfea7fa442ed392fb43ab3d54ec5db24ddc",
            "size":1421,
            "platform":{
                "architecture":"arm64",
                "os":"linux"
            }
        }
    ]
}

binfmt_misc

binfmt_misc是Linux內核的一項功能,全稱是混雜二進制格式的內核支持(Kernel Support for miscellaneous Binary Formats),它能夠使Linux支持運行幾乎任何格式的程序,包括編譯後的Java、Python或Emacs程序。

為了能夠讓binfmt_misc運行任意格式的程序,至少需要做到兩點:特定格式二進制程序的識別方式,以及其對應的解釋器位置。雖然binfmt_misc聽上去很強大,其實現的方式卻意外地很容易理解,類似於bash解釋器通過腳本文件的第一行(如#!/usr/bin/python3)得知該文件需要通過什麼解釋器運行,binfmt_misc也預設了一系列的規則,如讀取二進制文件頭部特定位置的魔數,或者根據文件擴展名(如.exe、.py)以判斷可執行文件的格式,隨後調用對應的解釋器去運行該程序。Linux默認的可執行文件格式是elf,而binfmt_misc的出現拓寬了Linux的執行限制,將一點展開成一個面,使得各種各樣的二進制文件都能選擇它們對應的解釋器執行。

註冊一種格式的二進制程序需要將一行有:name:type:offset:magic:mask:interpreter:flags格式的字符串寫入/proc/sys/fs/binfmt_misc/register中,各個字段的含義如下:

  • name:用於標識的字符串,將用於在/proc/sys/fs/binfmt_misc目錄下創建同名文件

  • type:識別方式類型,「M」表示字符序列識別,「E」表示擴展名識別

  • offset:字符序列在文件中的偏移量,忽略的話默認為0,擴展名識別方式下忽略

  • magic:用於匹配的位元組序列,可以使用如\x0a之類的字符表示十六進制, 擴展名識別方式下用於表示擴展名,注意忽略擴展名前的點號

  • mask:掩碼,用於遮蓋字符序列中的部分字符,和字符序列的長度一樣,默認為全0xff,擴展名識別方式下忽略

  • interpreter:用於調用二進制程序的解釋器程序,需指定完整路徑

  • flags:用於控制解釋器執行方式的標誌位,目前有POCF四個標誌

    • P – preserve-argv[0]:保留解釋器作為argv[0]的位置,否則argv[0]為二進制程序本身

    • O – open-binary:讀取二進制程序後再返迴文件描述符給解釋器,否則將二進制程序的完整路徑傳遞給解釋器,區別在於前者不需要二進制程序的讀權限

    • C – credentials:使用二進制程序的所屬身份與權限,否則使用解釋器的所屬身份與權限,解釋器一般使用root用戶運行,而使用二進制程序的身份則能提升安全性

    • F – fix binary:在解釋器註冊後立即加載解釋器程序,否則二進制程序調用時再加載,區別在於切換mount命名空間或chroot後,解釋器路徑或許不再可用,此時通過路徑調用解釋器會出問題,而如果在註冊後立即加載的話,那麼不論什麼環境下都能夠調用解釋器

下圖分別展示了python解釋器(:frankming-py:E::mypy::/usr/bin/python3.9:POCF)和arm64解釋器(:qemu-aarch64:M:0:\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00:\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/qemu-aarch64:POCF)的註冊。其中,python解釋器採用了擴展名的識別類型,當執行後綴名為.mypy的可執行文件時,binfmt_misc將調用/usr/bin/python3.9解釋器來執行;arm64解釋器採用了魔數的識別類型,當執行的可執行文件符合預設的魔數時,binfmt_misc將調用qemu-aarch64解釋器來執行。

binfmt_misc模塊自Linux 2.6.12-rc2版本中引入,先後經歷了幾次功能上的略微改動,一是3.18版本中將解釋器路徑長度限制從原來的255位元組拓寬到1920位元組,二是在4.8版本中新增「F」(fix binary,固定二進制)標誌位,使mount命名空間變更和chroot後的環境中依然能夠正常調用解釋器執行二進制程序。由於我們需要構建多架構容器,必須使用「F」標誌位才能binfmt_misc在容器中正常工作,因此內核版本需要在4.8以上才可以。CentOS 7目前使用的內核是3.10,如果想要讓CentOS 7構建多架構容器,那麼只能夠採用升級內核的方法解決,可安裝elrepo中的kernel-ml內核軟件包,也可自己編譯內核並替換。

通過modinfo binfmt_misc命令可以確認binfmt_misc模塊是否可用,它提供文件形式的交互操作,一般情況下binfmt_misc將掛載到/proc/sys/fs/binfmt_misc目錄下,如果沒有掛載的話,可以手動將之掛載上:mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc

由於人工註冊解釋器的方式過於繁瑣,社區的開發者們提供了專門的程序用來註冊各類架構的解釋器,並封裝成容器鏡像,鏡像中就包含了註冊程序以及各類qemu模擬程序。可以通過podman pull tonistiigi/binfmt:latest命令下載,podman run --privileged --rm tonistiigi/binfmt:latest --install all一行命令即可註冊可支持的所有架構解釋器。該鏡像中內置了常見的qemu-<arch>模擬器程序,得益於「F」標誌位,這些模擬器程序只需存在於鏡像中,宿主機上不需要任何其他額外的配置。

# podman run --privileged --rm tonistiigi/binfmt:latest --install all
installing: ppc64le OK
installing: riscv64 OK
installing: s390x OK
installing: arm64 OK
installing: arm OK
installing: mips64le OK
installing: mips64 OK
...

總的來說,比起一般情況顯式調用解釋器去執行非原生架構程序,binfmt_misc產生的一個重要意義在於透明性。有了binfmt_misc後,用戶在執行程序時不需要再關心要用什麼解釋器去執行,好像任何架構的程序都能夠直接執行一樣,而可配置的「F」標誌位更是錦上添花,使解釋器程序在安裝時立即就被加載進內存,後續的環境改變也不會影響執行過程。

drone

Drone是一套由go語言編寫的輕量級CI/CD工具,它基於容器,使用單個文件描述管道,有一個社區插件平台能夠自定義並分享插件,並天生支持任何源碼管理工具、任何平台和任何語言。輕量化、易於使用是Drone的特點。

image.png

Drone在架構上分為控制節點和工作節點兩種,其中控制節點負責API接收,數據存儲以及UI呈現等功能,而工作節點則負責具體的構建。控制節點中維護着一個構建隊列,當收到構建請求時,控制節點會將其放入構建隊列中,與此同時工作節點會監聽控制節點中的構建隊列,當裏面有滿足條件的構建請求時,工作節點會消費該構建請求,並對其執行構建流水線。

Drone提供了較為豐富的插件已幫助完成CI流水線的構建,插件倉庫地址為:Drone Plugins。其中比較常用的插件有git和docker,git插件用於構建開始時克隆代碼,而docker插件則用於構建容器鏡像成果物。

Drone的docker插件目前不直接具備構建多架構鏡像的能力,如果想要通過Drone去構建不同架構的鏡像,目前只能通過不同架構構建流水線並發執行的方式實現,因此針對這方面的能力,需要定製化開發。經研究,可以嘗試在buildah插件的基礎上增加platform配置項,表示buildah bud構建命令中的--platform參數,用以構建多架構鏡像。

用法

binfmt註冊所有可支持的架構:

podman run --privileged --rm tonistiigi/binfmt:latest --install all

buildah操作manifest:

# 創建一個manifest
buildah manifest create openeuler-base:22.03
# 將arm64鏡像加入該manifest
buildah manifest add openeuler-base:22.03 openeuler:22.03-linux-arm64
# 將amd64鏡像加入該manifest
buildah manifest add openeuler-base:22.03 openeuler:22.03-linux-amd64
# 查看該manifest
buildah manifest inspect openeuler-base:22.03

buildah構建多架構鏡像:

# 依次構建
buildah bud --manifest openeuler-base:22.03 --arch amd64
buildah bud --manifest openeuler-base:22.03 --arch arm64

# 並發構建
buildah bud --manifest openeuler-base:22.03 --jobs=2 --platform=linux/amd64,linux/arm64

# 上傳鏡像
buildah manifest push --tls-verify=false --all openeuler-base:22.03 docker://openeuler-base:22.03

drone構建多架構鏡像(改造後buildah鏡像):

---
kind: pipeline
type: docker
name: default
steps:
- name: test
  image: drone/buildah-plugin:latest
  privileged: true
  network_mode: host
  settings:
    username:
      from_secret: docker_username
    password:
      from_secret: docker_password
    registry: frankming.org
    repo: frankming.org/test
    dockerfile: frankming/Dockerfile
    insecure: true
    platform: linux/amd64,linux/arm64

Q&A

局域網內無法便捷地獲取docker hub中的鏡像?

需要通過代理獲取。對於buildah/podman,可以通過設置HTTP_PROXY、HTTPS_PROXY環境變量的方式,例如:HTTPS_PROXY=socks5://x.x.x.x:x buildah pull tonistiigi/binfmt:latest;而對於docker,則略微麻煩一點,需要設置systemd配置文件:

mkdir -p /etc/systemd/system/docker.service.d
cat > /etc/systemd/system/docker.service.d/http-proxy.conf << EOF
[Service]
Environment="HTTP_PROXY=socks5://x.x.x.x:x" "HTTPS_PROXY=socks5://x.x.x.x:x" "NO_PROXY=localhost,127.0.0.1,10.0.0.0/8"
EOF
systemctl daemon-reload
systemctl restart docker

跨架構構建鏡像的速度和原生相比有差異嗎?差了多少呢?

由於採用qemu以模擬不同架構,跨架構構建鏡像的速度必然是比原生要慢的。目前測試來看,在amd64平台構建arm64鏡像的速度只有原生的二分之一到三分之一。

獲取、推送鏡像時報錯x509: certificate signed by unknown authority?

需要配置禁止驗證服務器證書。對於buildah/podman,可以通過添加--tls-verify=false參數,也可以在配置文件中添加:

cat >> /etc/containers/registries.conf << EOF
[[registry]]
location = "frankming.org"
insecure = true
EOF

而對於docker,需要在配置文件中添加:

echo "$(jq '."insecure-registries"|=.+["frankming.org"]' /etc/docker/daemon.json)" > /etc/docker/daemon.json
systemctl reload docker

drone構建鏡像時報錯:Error response from daemon: Get “//frankming.org/v2/“: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

一般該問題是dns超時導致的,如果私有倉庫域名的IP地址不輕易改變的話,那麼可以直接寫入域名和IP地址到宿主機的/etc/hosts文件中,隨後為DRONE_RUNNER_VOLUMES環境變量添加/etc/hosts:/etc/hosts掛載項,最後重啟drone runner,使drone runner訪問域名時使用本地緩存,而不經過dns服務器。

構建arm64鏡像時時不時地報錯:qemu: uncaught target signal 11?

這是由於qemu的問題造成的,可以通過獲取最新版binfmt容器鏡像的方式解決,也可以手動編譯qemu7.0以上版本後再次註冊來解決,如果先前已註冊,那麼需註銷後再註冊。

tar -xvf qemu-7.1.0.tar.xz
dnf install -y make ninja-build pixman-devel
./configure
make

docker run --privileged --rm tonistiigi/binfmt:latest --uninstall qemu-aarch64
docker run --privileged --rm tonistiigi/binfmt:latest --install all

參考文檔

Image Manifest V 2, Schema 2 | Docker Documentation

possibility to set a proxy directly in podman instead of set the system wide environment variable · Issue #4543 · containers/podman · GitHub

Docker 代理脫坑指南 – 來份鍋包肉 – 博客園 (cnblogs.com)

解決Docker容器iptables不能用 – redcat8850 – 博客園 (cnblogs.com)

Kernel Support for miscellaneous Binary Formats (binfmt_misc) — The Linux Kernel documentation

Drone Plugins – Drone Buildah

sh: write error: Invalid argument – Centos 7 · Issue #100 · multiarch/qemu-user-static · GitHub

Download QEMU – QEMU

Drone CI / CD | Drone