本文最后更新于:2024年7月24日 晚上
前言
随着容器、芯片技术的进一步发展,以及绿色、节能、信创等方面的要求,多 CPU 架构的场景越来越常见。典型的应用场景包括:
信创:x86 服务器 + 鲲鹏 ARM 等信创服务器;
个人电脑:苹果 Mac M1 + Windows 电脑(或旧的 Intel 芯片苹果电脑);
Edge:数据中心使用 x86 服务器,边缘 Edge 端使用低功耗的 arm 边缘设备(如树莓派等)。
容器云原生技术在这方面支持的是很好,但是实际使用中细节会有一些问题,举一个例子,就是:如何保存 / 同步多架构容器 Docker 镜像
本次先以将 Docker Hub 的镜像同步到本地镜像仓库为例说明。
词汇表
英文
中文
说明
multi-arch image
多架构镜像
variant
变体
不同变体指的如:redis 镜像的 arm/v5
和 arm/v7
两种变体
manifest
清单
manifest-list
清单(的)列表
layer
(镜像)层
image index
镜像索引
OCI 专有名词,含义和 manifest-list 相同
manifest digest
清单摘要
容器镜像如何支持多架构
一个多架构镜像(A multi-arch image)是一种容器镜像,它可以组合不同架构体系(如 amd64 和 arm)的变体(variants),有时还可以组合不同操作系统(如 windows 和 linux)的变体。运行支持多架构的镜像时,容器客户端会自动选择与你的 OS 和架构相匹配的镜像变体。
多架构镜像是基于镜像清单和清单列表实现的。
清单(Manifests)
每个容器镜像都由一个“清单”表示。清单是一个 JSON 文件,用于唯一标识镜像,并引用其层(layer)及其相应的大小。
hello-world
Linux 镜像的基本清单类似于以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "schemaVersion" : 2 , "mediaType" : "application/vnd.docker.distribution.manifest.v2+json" , "config" : { "mediaType" : "application/vnd.docker.container.image.v1+json" , "size" : 1510 , "digest" : "sha256:fbf289e99eb9bca977dae136fbe2a82b6b7d4c372474c9235adc1741675f587e" } , "layers" : [ { "mediaType" : "application/vnd.docker.image.rootfs.diff.tar.gzip" , "size" : 977 , "digest" : "sha256:2c930d010525941c1d56ec53b97bd057a67ae1865eebf042686d2a2d18271ced" } ] }
清单列表 (Manifest-lists)
多架构镜像的 清单列表 (通常称为 OCI 镜像 的镜像索引 )是镜像的集合(索引),您可以通过指定一个或多个镜像名称来创建一个。它包括有关每个镜像的详细信息,例如支持的操作系统和体系架构、大小和清单摘要 (manifest digest)。清单列表的使用方式与 docker pull
和 docker run
命令 中的镜像名称相同。
docker CLI 使用 docker manifest
命令管理清单和清单列表。
🐾 Warning :
目前,该命令 docker manifest
和子命令是实验性的。有关使用实验性命令的详细信息,请参阅 Docker 文档。
✍️笔者注:可能是因为 实验性 的原因,使用过程中有几个多架构镜像碰到了诡异的问题。
您可以使用该命令 docker manifest inspect
查看清单列表。以下是多架构镜像hello-world:latest
的输出,它有三个清单:两个用于 Linux 操作系统体系架构,一个用于 Windows 体系架构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 { "schemaVersion" : 2 , "mediaType" : "application/vnd.docker.distribution.manifest.list.v2+json" , "manifests" : [ { "mediaType" : "application/vnd.docker.distribution.manifest.v2+json" , "size" : 524 , "digest" : "sha256:83c7f9c92844bbbb5d0a101b22f7c2a7949e40f8ea90c8b3bc396879d95e899a" , "platform" : { "architecture" : "amd64" , "os" : "linux" } } , { "mediaType" : "application/vnd.docker.distribution.manifest.v2+json" , "size" : 525 , "digest" : "sha256:873612c5503f3f1674f315c67089dee577d8cc6afc18565e0b4183ae355fb343" , "platform" : { "architecture" : "arm64" , "os" : "linux" } } , { "mediaType" : "application/vnd.docker.distribution.manifest.v2+json" , "size" : 1124 , "digest" : "sha256:b791ad98d505abb8c9618868fc43c74aa94d08f1d7afe37d19647c0030905cae" , "platform" : { "architecture" : "amd64" , "os" : "windows" , "os.version" : "10.0.17763.1697" } } ] }
使用 docker manifest
保存多架构镜像
这里是将多架构的镜像推送到本地镜像仓库步骤:
标记每个特定于体系结构的镜像并将其推送到容器注册表。以下示例假定有两个 Linux 体系结构:arm64 和 amd64。
1 2 3 4 5 6 7 8 9 docker tag myimage:arm64 \ 192.168.2.23:5000/multi-arch-samples/myimage:arm64 docker push 192.168.2.23:5000/multi-arch-samples/myimage:arm64 docker tag myimage:amd64 \ 192.168.2.23:5000/multi-arch-samples/myimage:amd64 docker push 192.168.2.23:5000/multi-arch-samples/myimage:amd64
运行 docker manifest create
以创建清单列表,以将前面的镜像合并到多架构镜像中。
1 2 3 docker manifest create 192.168.2.23:5000/multi-arch-samples/myimage:multi \ 192.168.2.23:5000/multi-arch-samples/myimage:arm64 \ 192.168.2.23:5000/multi-arch-samples/myimage:amd64
使用以下命令 docker manifest push
将清单推送到镜像仓库:
1 docker manifest push 192.168.2.23:5000/multi-arch-samples/myimage:multi
使用命令 docker manifest inspect
查看清单列表。上一节显示了命令输出的示例。
将多架构清单推送到镜像仓库后,使用多架构镜像的方式与处理单架构镜像的方式相同。例如,使用 docker pull
拉取镜像。
保存 / 同步多架构镜像实用脚本一 - 基于 docker manifest
场景一
已有多架构压缩包 需要 load 压缩包并将多架构镜像上传到本地镜像仓库
以 K3s 为例,官方在 release 时已经发布了多架构的离线镜像压缩包,分别为:
k3s-airgap-images-amd64.tar.gz
k3s-airgap-images-arm.tar.gz
k3s-airgap-images-arm64.tar.gz
…
这些包已经下载好,并传到客户 / 用户的离线环境机器上。现在需要 load 压缩包并将多架构镜像上传到本地镜像仓库
大致步骤
docker load
压缩包
其中的镜像逐个打 tag, 改为 < 本地镜像仓库地址 >/.../...:<tag>-<arch>
push 镜像
以上步骤重复 3 遍,将 tag 带有 -amd64
-arm
-arm64
的镜像都 push 到本地镜像仓库
镜像逐个 docker manifest create
以创建清单列表
使用以下命令 docker manifest push
将清单逐个推送到镜像仓库
完整脚本如下:
🐾 Warning:
由于本人能力有限,在使用 k3s v1.21.7+k3s1 版本的 8*3 个离线镜像做测试的时候,总是 5 个成功,另外 3 个出现 manifest list 的 arch 和 manifest 对不上的情况。
不知道是我脚本问题还是 docker manifest
命令是实验性导致的。
有经验的还请帮忙看看。谢谢~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 #!/bin/bash amd64_images="k3s-airgap-images-amd64.tar.gz" arm64_images="k3s-airgap-images-arm64.tar.gz" arm_images="" list="k3s-images.txt" usage () { echo "USAGE: $0 [--amd64-images k3s-airgap-images-amd64.tar.gz] [--arm64-images k3s-airgap-images-arm64.tar.gz] [---arm-images k3s-airgap-images-arm.tar.gz] --registry my.registry.com:5000" echo " [-l|--image-list path] text file with list of images; one image per line." echo " [-x|--amd64-images path] amd64 arch tar.gz generated by docker save." echo " [-a|--arm64-images path] arm64 arch tar.gz generated by docker save." echo " [---arm-images path] arm arch tar.gz generated by docker save." echo " [-r|--registry registry:port] target private registry:port." echo " [-h|--help] Usage message" }push_manifest () { export DOCKER_CLI_EXPERIMENTAL=enabled manifest_list=() for i_arch in "${arch_list[@]} " ; do manifest_list+=("$1 -${i_arch} " ) done echo "Preparing manifest $1 , list[${arch_list[@]} ]" docker manifest create "$1 " "${manifest_list[@]} " --insecure docker manifest push "$1 " --purge --insecure }while [[$# -gt 0 ]]; do key="$1 " case $key in -r | --registry) reg="$2 " shift shift ;; -l | --image-list) list="$2 " shift shift ;; -x | --amd64-images) amd64_images="$2 " shift shift ;; -a | --arm64-images) arm64_images="$2 " shift shift ;; --arm-images) arm_images="$2 " shift shift ;; -h | --help ) help ="true" shift ;; *) usage exit 1 ;; esac done if [[-z $reg ]]; then usage exit 1fi if [[$help ]]; then usage exit 0fi arch_list=()if [[-n "${amd64_images} " ]]; then arch_list+=("amd64" )fi if [[-n "${arm64_images} " ]]; then arch_list+=("arm64" )fi if [[-n "${arm_images} " ]]; then arch_list+=("arm" )fi image_list=()while IFS= read -r i; do [-z "${i} " ] && continue image_list+=("${i} " )done <"${list} " for arch in "${arch_list[@]} " ; do [-z "${arch} " ] && continue case $arch in amd64) docker load --input ${amd64_images} ;; arm64) docker load --input ${arm64_images} ;; arm) docker load --input ${arm_images} ;; esac for i in "${image_list[@]} " ; do [-z "${i} " ] && continue case $i in */*) image_name="${reg} /${i} " ;; *) image_name="${reg} /library/${i} " ;; esac docker tag "${i} " "${image_name} -${arch} " docker rmi -f "${i} " docker push "${image_name} -${arch} " done done for i in "${image_list[@]} " ; do [-z "${i} " ] && continue case $i in */*) image_name="${reg} /${i} " ;; *) image_name="${reg} /library/${i} " ;; esac push_manifest "${image_name} " done
使用方法:
1 ./load-images-multi-arch.sh --registry 192.168.2.23:5000 --arm-images k3s-airgap-images-arm.tar.gz
日志输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 $ ./load-images-multi-arch.sh --registry 192.168 .2 .23 :5000 --arm_images k3s-airgap-images-arm.tar.gz# docker load 镜像第一轮,是 amd64 架构的 67f 770da229b: Loading layer [==================================================>] 1.45 MB/1.45 MB Loaded image: rancher/library-busybox:1.32 .1 ...# 打 tag 并 delete 原 tag 镜像,并 push Untagged: rancher/coredns-coredns:1.8 .3 The push refers to repository [192.168 .2 .23 :5000 /rancher/ coredns-coredns]85 c53e1bd74e: Pushed225 df95e717c: Pushed1.8 .3 -amd64: digest: sha256:db4f1c57978d7372b50f416d1058beb60cebff9a0d5b8bee02bfe70302e1cb2f size: 739 ...# docker load 镜像第二轮,是 arm64 架构的 ...32626 eb1fe89: Loading layer [==================================================>] 526.8 kB/526.8 kB Loaded image: rancher/pause:3.1 ...Untagged: rancher/pause:3.1 The push refers to repository [192.168 .2 .23 :5000 /rancher/ pause]32626 eb1fe89: Pushed3.1 -arm64: digest: sha256:2 aac966ece8906a535395f92bb25f0e8e21dac737df75b381e8f9bdd3ed56528 size: 527 # docker load 镜像第二轮,是 arm 架构的 8e322 dc9c333: Loading layer [==================================================>] 5.045 MB/5.045 MBefed3cfd1b26: Loading layer [==================================================>] 1.623 MB/1.623 MBa46153382f22: Loading layer [==================================================>] 3.584 kB/3.584 kB Loaded image: rancher/klipper-lb:v0.3 .4 ...Untagged: rancher/coredns-coredns:1.8 .3 The push refers to repository [192.168 .2 .23 :5000 /rancher/ coredns-coredns]9f 4a0b0fd8b2: Pushed225 df95e717c: Layer already exists1.8 .3 -arm: digest: sha256:dfc241eae22da74dd378535b69d7927f897acf48424cdcb90991b33f412cb7ae size: 739 # docker manifest create Preparing manifest 192.168 .2 .23 :5000 /rancher/ coredns-coredns:1.8 .3 , list[amd64 arm64 arm] Created manifest list 192.168 .2 .23 :5000 /rancher/ coredns-coredns:1.8 .3 sha256: dc76fece93e42f05e7013e159097a0d426734fd268467f242d5b155dd49b0221 Preparing manifest 192.168 .2 .23 :5000 /rancher/ klipper-helm:v0.6 .6 -build20211022, list[amd64 arm64 arm] Created manifest list 192.168 .2 .23 :5000 /rancher/ klipper-helm:v0.6 .6 -build20211022sha256: e1c6842554ea37e66443cfab9a2422231bf8390b4c69711a74eb4cccde9d3dba Preparing manifest 192.168 .2 .23 :5000 /rancher/ klipper-lb:v0.3 .4 , list[amd64 arm64 arm] Created manifest list 192.168 .2 .23 :5000 /rancher/ klipper-lb:v0.3 .4 sha256: 98842 bae8630a2aab1a94960185e152745ecf16ca69cf1eefdb53848cbc41063 Preparing manifest 192.168 .2 .23 :5000 /rancher/ library-busybox:1.32 .1 , list[amd64 arm64 arm] Created manifest list 192.168 .2 .23 :5000 /rancher/ library-busybox:1.32 .1 sha256: 0 b93c11bfd89ee5c971deaf9f312d115b2e1d797f79a7f68a266baecfb09a99f Preparing manifest 192.168 .2 .23 :5000 /rancher/ library-traefik:2.4 .8 , list[amd64 arm64 arm] Created manifest list 192.168 .2 .23 :5000 /rancher/ library-traefik:2.4 .8 sha256: 58464 dda10504d271a17855541ed8d31a787ea25eb751ecce90e14256f23eb24 Preparing manifest 192.168 .2 .23 :5000 /rancher/ local-path-provisioner:v0.0 .19 , list[amd64 arm64 arm] Created manifest list 192.168 .2 .23 :5000 /rancher/ local-path-provisioner:v0.0 .19 sha256: 0 c797ef85540a4934ea84a9471f4f5a10c93f749ee668d92527361c61bbe98c3 Preparing manifest 192.168 .2 .23 :5000 /rancher/ metrics-server:v0.3 .6 , list[amd64 arm64 arm] Created manifest list 192.168 .2 .23 :5000 /rancher/ metrics-server:v0.3 .6 sha256: 742595f 61320bcaead987c5aafc3eb64b9a9151edb02b9e4d27f8abcae26d92e Preparing manifest 192.168 .2 .23 :5000 /rancher/ pause:3.1 , list[amd64 arm64 arm] Created manifest list 192.168 .2 .23 :5000 /rancher/ pause:3.1 sha256: f3ef3cbaf2ea466a0c2a2cf3db0d9fbc30f4c24e57a79603aa0fa8999d4813b0
Skopeo 简介
最近 Skopeo 版本更新到了 v1.8 , 最近的版本增加了一些与 多架构 有关的 flags, 使得通过 skopeo
进行多架构镜像的保存 / 同步更为方便。
📝 Notes:
目前关于多架构,只有 3 个选项,3 个选项都没有选择源镜像多个架构的其中几个的能力,但正在开发中。
具体见这个 Issue: feature: Support list of archs for sync
command · Issue #1694 · containers/skopeo (github.com)
以下是一些相关 flags:
skopeo
--override-arch <arch>
: 使用 arch
代替机器的架构来选择镜像。
--override-os <os>
: 使用 os
代替机器的 OS 来选择镜像。
--override-variant <variant>
: 使用 variant
运行的架构的变体来选择镜像。(不同变体指的如:redis 镜像的 arm/v5
和 arm/v7
两种变体)
skopeo copy
--all, -a
: 如果 source-image 引用的是一个镜像列表,那么 不要 只复制与当前操作系统和体系架构匹配的镜像(取决于全局的 --override-os
、--override-arch
和--override-variant
选项的使用),而是尝试复制列表中的所有镜像,以及列表本身。
--multi-arch
: 如果源镜像引用多架构镜像,则控制要复制的内容。默认设置是system
。
system
: 仅复制与系统架构匹配的镜像
all
: 复制完整的多架构镜像
index-only
: 仅复制镜像索引 (image index).(index-only
选项通常会失败,除非目标中已经存在每个架构所引用的镜像,或者目标注册中心支持稀疏索引。)
skopeo sync
📝 Notes:
根据 skopeo copy --multi-arch index-only
的描述,场景一 还有一种实现就是:
docker manifest
之前的步骤,维持原状
将 docker manifest create
和 docker manifest push
替换为 skopeo copy --multi-arch index-only
保存 / 同步多架构镜像实用脚本二 - 基于 skopeo copy
场景二
直接从 docker.io 同步镜像到本地镜像仓库
以 K3s 某一版本为例,镜像列表为:
rancher/coredns-coredns:1.8.3
rancher/klipper-helm:v0.6.6-build20211022
rancher/klipper-lb:v0.3.4
rancher/library-busybox:1.32.1
rancher/library-traefik:2.4.8
rancher/local-path-provisioner:v0.0.19
rancher/metrics-server:v0.3.6
rancher/pause:3.1
这里直接基于 镜像搬运工 skopeo 提供的脚本做修改,修改后如下:
📝 Notes:
因为较新版本的 skopeo
才有上面说的一系列 flags, 我的 Ubuntu apt 安装的 skopeo
还停留在 v1.5
版本,没有上述功能。所以直接通过 docker run
方式运行
除此之外还添加了 --multi-arch all
选项。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #!/bin/bash GREEN_COL="\\033[32;1m" RED_COL="\\033[1;31m" NORMAL_COL="\\033[0;39m" SOURCE_REGISTRY=$1 TARGET_REGISTRY=$2 IMAGES_LIST_FILE=$3 : ${IMAGES_LIST_FILE:="k3s-images.txt"} : ${TARGET_REGISTRY:="192.168.2.23:5000"} : ${SOURCE_REGISTRY:="docker.io"} set -eo pipefail CURRENT_NUM=0 ALL_IMAGES="$(sed -n '/#/d;s/:/:/p' ${IMAGES_LIST_FILE} | sort -u) " TOTAL_NUMS=$(echo "${ALL_IMAGES} " | wc -l)skopeo_copy () { if docker run -it quay.io/skopeo/stable:latest copy --insecure-policy --src-tls-verify=false --dest-tls-verify=false \ --src-creds caseycui:xxxxxxxxxxxxxxxxxxxxx --multi-arch all --override-os linux -q docker://$1 docker://$2 ; then echo -e "$GREEN_COL Progress: ${CURRENT_NUM} /${TOTAL_NUMS} sync $1 to $2 successful $NORMAL_COL " else echo -e "$RED_COL Progress: ${CURRENT_NUM} /${TOTAL_NUMS} sync $1 to $2 failed $NORMAL_COL " exit 2 fi }for image in ${ALL_IMAGES} ; do let CURRENT_N192.168.2.23:5000UM=${CURRENT_NUM} +1 skopeo_copy ${SOURCE_REGISTRY} /${image} ${TARGET_REGISTRY} /${image} done
运行效果如下:
1 2 3 4 5 6 7 8 9 $ bash sync.sh Progress: 1 /8 sync docker.io/rancher/ coredns-coredns:1.8 .3 to 192.168 .2 .23 :5000 /rancher/ coredns-coredns:1.8 .3 successful Progress: 2 /8 sync docker.io/rancher/ klipper-helm:v0.6 .6 -build20211022 to 192.168 .2 .23 :5000 /rancher/ klipper-helm:v0.6 .6 -build20211022 successful Progress: 3 /8 sync docker.io/rancher/ klipper-lb:v0.3 .4 to 192.168 .2 .23 :5000 /rancher/ klipper-lb:v0.3 .4 successful Progress: 4 /8 sync docker.io/rancher/ library-busybox:1.32 .1 to 192.168 .2 .23 :5000 /rancher/ library-busybox:1.32 .1 successful Progress: 5 /8 sync docker.io/rancher/ library-traefik:2.4 .8 to 192.168 .2 .23 :5000 /rancher/ library-traefik:2.4 .8 successful Progress: 6 /8 sync docker.io/rancher/ local-path-provisioner:v0.0 .19 to 192.168 .2 .23 :5000 /rancher/ local-path-provisioner:v0.0 .19 successful Progress: 7 /8 sync docker.io/rancher/ metrics-server:v0.3 .6 to 192.168 .2 .23 :5000 /rancher/ metrics-server:v0.3 .6 successful Progress: 8 /8 sync docker.io/rancher/ pause:3.1 to 192.168 .2 .23 :5000 /rancher/ pause:3.1 successful
最终效果
最终本地的镜像效果如下:
🎉🎉🎉
📚️ Reference