容器镜像多架构支持介绍

容器镜像多架构支持介绍

简介

出于开发需要,我们经常会需要浏览公共镜像库,以选取合适的基础镜像,在浏览过程中,不经意地会发现部分镜像的一个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