容器映象多架構支援介紹

2022-11-14 18:00:49

容器映象多架構支援介紹

簡介

出於開發需要,我們經常會需要瀏覽公共映象庫,以選取合適的基礎映象,在瀏覽過程中,不經意地會發現部分映象的一個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 "http://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