
1. 先搞清楚 kolla 镜像的“一整套”到底有多少个、叫什么名把 OpenStack 的 kolla 镜像搬到自家云厂商的托管镜像仓库第一件要干的事不是敲docker pull而是把镜像清单弄清楚。我见过太多人上来就凭记忆拉nova-api、keystone、glance-api这几个“看起来最重要”的结果kolla-ansible一跑 prechecks 就报一堆manifest unknown回头补镜像补到怀疑人生。kolla 的镜像不是零散的一堆独立服务它是一个版本强耦合的集合所有镜像的 tag 必须完全一致缺一个都可能让部署卡在中间。这一章先把镜像的命名规则、数量和 registry 前缀的生效位置讲清楚后面所有操作都建立在这三个认知上。1.1 镜像全名的四段拼装规则kolla 生成镜像全名的逻辑其实很简单就是四段拼接registry/namespace/container_name:tag。四段里最容易出错的是后两段之外的 registry 和 namespace因为很多人只盯着 tag。container_name是 kolla 内部定义的容器名比如nova_api、nova_libvirt、keystone、haproxy、mariadb、rabbitmq注意 kolla 内部用的是下划线风格虽然仓库里实际的镜像路径往往是中划线风格nova-api这个转换由 kolla 的 filter 完成你手工拉取时需要按仓库里的真实路径来。tag 则是release-distro-arch三段式典型值如2024.1-ubuntu-22.04也有2023.2-rocky-9、2024.1-debian-bookworm这类组合。这里有个非常关键的细节arch 段在 x86_64 机器上通常省略不写只在 ARM 等平台才需要显式带-aarch64。所以你在 x86 环境下看到2024.1-ubuntu-22.04是对的不要自己脑补补一个-x86_64上去那样一定拉不到。用一张表把四段的取值来源和常见错误对照一下比翻文档快得多段位典型取值常见错误registry上游公共仓库地址混用了旧版 docker.io 与新版 quay 前缀导致一半镜像拉不到namespacekolla 的命名空间写成kolla但实际应为openstack.kolla路径 404container_namenova-api、keystone用了 kolla 内部的下划线名仓库里不存在该路径tag2024.1-ubuntu-22.04自行为 x86 补-x86_64或版本号与本次部署不一致记住一句口诀tag 从头到尾一个字符都不许改能改的只有 registry 和 namespace。1.2 一个最小可部署集合的镜像数量盘点“到底要拉多少个”这个问题答案取决于你要部署哪些服务。一个只开核心组件keystone、glance、nova、neutron、placement、horizon、mariadb、rabbitmq、memcached、haproxy、keepalived的全能型单节点镜像数量大概在55 到 70 个之间如果打开 cinder、swift、heat、octavia、barbican、manila、设计相关的 telemetry很容易冲到120 个以上。全量拉一遍 kolla 的镜像清单规模在两百多个。这些镜像的体积分布很不均匀nova-libvirt、neutron-server、cinder-volume、horizon这类包含大量 Python 依赖和系统包的镜像单个解压后两三个 GB 很常见kolla-toolbox、cron、fluentd这类小镜像可能只有几百 MB。以 2024.1 的实际经验值来说一个中等规模部署的全套镜像压缩体积合计在 15GB 到 25GB 区间落到本地磁盘解压后大约 40GB 到 60GB。这个数字一定要提前算因为它直接决定你的/var/lib/docker所在分区会不会被撑爆。我在第一次干这件事的时候就吃了亏临时找的跳板机根分区只有 40G拉到第七十个镜像时 dockerd 直接无响应docker images卡住不动最后只能清空了重来。我一般会按下面的顺序确定清单而不是凭感觉先定openstack_release和distro这两个决定了 tag 的唯一取值从kolla-ansible的ansible/group_vars/all.yml里找到本次要部署的服务集合或者干脆用kolla-ansible的enable_*开关反推出跑得起来的组件集拿一份官方镜像清单文件做基线按实际启用的服务做裁剪宁可多留两三个用不上的也不要漏把清单写成一个纯文本文件一行一个镜像路径后面所有脚本都读这个文件。清单写成文件的好处是拉取、改 tag、推送、校验四个环节全部复用同一份数据源不会出现“拉的时候有、推的时候忘了”的情况。1.3 registry 前缀在 kolla-ansible 里的生效位置弄清了镜像名还要知道部署端是怎么用这个前缀的。kolla-ansible拼镜像全名的模板大致是{{ docker_registry }}/{{ docker_namespace }}/{{ container_name }}:{{ tag }}也就是说registry 和 namespace 是两个独立变量这一点非常容易被忽略。很多人习惯性地把docker_registry填成一个完整路径比如my-registry.example.com/kolla然后把docker_namespace留空或者保持默认。这种写法在部分版本上能跑但一旦docker_namespace默认值不为空拼出来的路径就会多出一段变成my-registry.example.com/kolla/openstack.kolla/nova-api:...直接 404。我的做法是docker_registry只填域名加端口docker_namespace填你要用的仓库命名空间。这样两个变量各司其职拼出来的路径和你手工 push 时用的路径能完全对上# /etc/kolla/globals.yml 中的相关片段 docker_registry: registry.internal.example.com:5000 docker_namespace: kolla对应到手工操作你推上去的镜像路径就应该是registry.internal.example.com:5000/kolla/nova-api:2024.1-ubuntu-22.04。两边一致部署时才能精准命中。还有一个隐藏点kolla-ansible里有个kolla_docker_registry变量注意多了个前缀在某些版本里用于指定 kolla 工具容器自己的来源和docker_registry不是一回事。改配置前先grep -rn docker_registry /path/to/kolla-ansible/ansible/group_vars/看一眼当前版本的定义能省掉一次排查。2. 拉取前必须算清楚的三笔账容量、时间、失败率老老实实说这件事的难点根本不在“会不会用 docker 命令”而在资源规划。几十上百个 GB 的镜像搬运本质是一次长时间、高并发、易中断的数据迁移。这一章讲的三笔账是我每次动手前必算的算完再决定用哪套方案、开多少并发、分几批。2.1 磁盘容量估算与 /var/lib/docker 的位置问题容量估算不能只看“镜像总大小”要按三层来算压缩层上游仓库里存储的体积也就是你在网页上看到的数字全套 15~25GB解压层docker 拉下来之后本地存储的体积通常是压缩层的 2~2.5 倍40~60GB中间层如果你用docker tagdocker push的方式中间还会产生一份同样大小的临时对象虽然是硬链接为主但 docker 的存储驱动会额外占用一部分元数据和层索引实际可能再多出 10% 左右。所以最低准备 100GB 可用空间是底线想稳一点直接给 150GB。这个数字我在多个环境里验证过不是拍脑袋。第二个问题更隐蔽/var/lib/docker默认在根分区。哪怕你的数据盘挂了 500G只要没把 docker 的数据目录挪过去一样会被根分区卡死。迁移数据目录的步骤如下注意每一步都不要跳# 1. 确认当前 docker 数据目录位置 docker info | grep -i docker root dir # 2. 停止 docker 和容器 systemctl stop docker.socket docker # 确认没有残留进程 pgrep -a dockerd # 3. 迁移数据用 rsync 保留属性便于回滚 rsync -aHAX --progress /var/lib/docker/ /data/docker/ # 4. 修改 daemon 配置 mkdir -p /etc/docker cat /etc/docker/daemon.json EOF { data-root: /data/docker, max-concurrent-downloads: 6, max-concurrent-uploads: 4, storage-driver: overlay2 } EOF # 5. 启动并验证 systemctl start docker docker info | grep -i docker root dirmax-concurrent-downloads默认是 3调高确实能提升总吞吐但要注意别和max-concurrent-uploads一起无脑拉满。上传并发过高时某些自建 registry 会被打满连接数反而出现大量 500 错误。我一般下载开 6、上传开 4是比较稳的组合。注意改完># 用 time 和 docker 的进度输出做一次小批量试拉 time docker pull 上游 registry/namespace/nova-api:tag # 观察拉完后 docker 自己打印的总大小和耗时拉 3 到 5 个有代表性的大镜像把“大小÷耗时”平均一下得到的就是有效吞吐。用这个数字去算全量误差能控制在 20% 以内。这里还要提一个容易忽略的成本时间窗口。如果你是在办公网里干这件事白天带宽被同事分走晚上会快很多。我一般把大批量拉取安排在夜里跑用nohup挂后台第二天早上看日志。别在周五下午开始拉两百个镜像那基本等于给自己找麻烦。2.3 拉取失败率与重试策略几十上百个镜像串行拉取中途出现几个失败是大概率事件不是意外。原因五花八门某一层传输超时、上游偶发 429 限流、本地磁盘瞬时写满、dockerd 内存不足……指望“一次拉完”是不现实的正确的心态是“设计一套能失败重试的流程”。我的做法是把拉取包装成一个带重试的脚本核心逻辑是先检查本地是否已有同名同 tag 的镜像有就跳过没有就拉失败后 sleep 若干秒重试最多重试 3 次单个镜像 3 次都失败就记进失败清单继续下一个。整批跑完之后再针对失败清单单独跑一轮。#!/usr/bin/env bash # pull_kolla.sh - 按清单批量拉取带幂等与重试 set -uo pipefail UPSTREAM${UPSTREAM:-上游 registry/上游 namespace} TAG${TAG:?请设置 TAG例如 2024.1-ubuntu-22.04} LIST${LIST:-images.txt} FAILEDfailed.txt : $FAILED while read -r name; do [ -z $name ] continue ref${UPSTREAM}/${name}:${TAG} if docker image inspect $ref /dev/null 21; then echo [skip] $ref 已存在 continue fi ok0 for i in 1 2 3; do echo [pull] 第 ${i} 次尝试 $ref if docker pull $ref; then ok1 break fi sleep $((i * 15)) done if [ $ok -eq 0 ]; then echo [fail] $ref | tee -a $FAILED fi done $LIST echo ---- 失败清单 ---- cat $FAILED这个脚本有几个刻意的设计值得说明理由幂等检查放在最前批量任务中断后重跑是常态没有幂等检查就要从头再来一遍浪费的是几个小时set -uo pipefail但不加-e因为单个镜像失败不应该让整个脚本退出这就是“预期内失败”的处理方式重试间隔递增15 秒、30 秒、45 秒给上游一点缓冲时间比固定 5 秒硬刚的成功率高不少失败清单落盘这是整批任务的“断点续传”依据比记在脑子里靠谱一万倍。3. 两条落地路线本地中转 与 skopeo 直连复制把镜像从上游搬到自家仓库技术上就两条路本地中转pull 到本地再 push 上去和工具直连复制registry 到 registry不落地。两条路我都用过多轮各有明确适用场景不是“新工具一定更好”。3.1 路线Adocker pull tag push 的完整脚本这条路最朴素也最好理解本地拿一份改个名字推上去。优点是所有步骤都可见、可中断、可单独排查缺点是本地要占一份完整空间网络流量走两遍下游一遍、上游一遍。执行顺序上有个小优化不要“全拉完再全推”而是按批拉、按批推每批 10 到 15 个。这样做的好处是磁盘峰值占用只有一批的量而且中途失败影响面小。下面这个脚本做了拉取和推送一体的处理#!/usr/bin/env bash # migrate_kolla.sh - pull retag push 一体化 set -uo pipefail UPSTREAM上游 registry/上游 namespace TARGETregistry.internal.example.com:5000/kolla TAG${TAG:?请设置 TAG} LIST${LIST:-images.txt} BATCH${BATCH:-10} LOGDIR./logs mkdir -p $LOGDIR : $LOGDIR/failed_pull.txt : $LOGDIR/failed_push.txt pull_one() { local name$1 local src${UPSTREAM}/${name}:${TAG} local dst${TARGET}/${name}:${TAG} if ! docker image inspect $src /dev/null 21; then docker pull $src || { echo $name $LOGDIR/failed_pull.txt; return 1; } fi docker tag $src $dst || { echo $name $LOGDIR/failed_push.txt; return 1; } docker push $dst || { echo $name $LOGDIR/failed_push.txt; return 1; } echo [ok] $name return 0 } # 分批处理控制磁盘峰值 split -l $BATCH $LIST $LOGDIR/batch_ for f in $LOGDIR/batch_*; do echo 处理批次 $f while read -r name; do [ -z $name ] continue pull_one $name done $f # 每批结束后清理本地镜像释放空间 while read -r name; do [ -z $name ] continue docker rmi ${UPSTREAM}/${name}:${TAG} ${TARGET}/${name}:${TAG} /dev/null 21 done $f done echo ---- pull 失败 ----; cat $LOGDIR/failed_pull.txt echo ---- push 失败 ----; cat $LOGDIR/failed_push.txt关键点在于每批结束后docker rmi清掉本地镜像。这一步很多人不愿意做觉得“万一要重推呢”但不清的话磁盘峰值就是全量前面算的容量账全部白费。清理之后如果推失败了要重来代价只是重新 pull 一次比重开一台机器划算。docker tag这一步的意义在于给镜像打上目标仓库的完整路径。docker 的 push 行为完全由镜像名里的 registry 段决定你不能在 push 时临时指定地址所以 tag 是必须的中间步骤不能省。3.2 路线Bskopeo copy 免落盘的取舍skopeo这类工具能直接从一个 registry 复制到另一个 registry本地不落地。它的核心命令形态是skopeo copy \ --all \ --src-creds src_user:src_pass \ --dest-creds dst_user:dst_pass \ docker://上游 registry/上游 namespace/name:tag \ docker://registry.internal.example.com:5000/kolla/name:tag--all的含义是把所有架构的 manifest 一起复制。这个参数对于 kolla 这种多架构镜像来说很关键不加--all只复制当前平台那一个 manifest如果后续有人在 ARM 机器上从你的仓库拉就会报no matching manifest。这条路的最大优势是省磁盘。本地不落地峰值占用接近零一台小规格的机器就能跑不用担心/var/lib/docker撑爆。第二个优势是可以做校验skopeo inspect --raw能直接读到远端 manifest复制完立刻比对 digest比docker pull之后再看 ID 更干净。麻烦的地方在于需要给工具单独配认证而且要确认目标仓库支持 manifest 直接上传。极少数私有 registry 在 API 兼容性上有瑕疵表现为大层上传到 99% 时断掉而同样的层用 docker push 就没问题。我遇到过两次最后都是退回路线 A 解决的。3.3 两条路线的对比与选型建议把两条路线的关键差异列成表选型时对着看维度路线A本地中转路线Bskopeo 直连复制本地磁盘峰值全量或一批的量40G接近 0网络流量走两遍下上走一遍直连失败可观测性强docker 日志直观中需要读工具输出多架构支持需额外处理--all一把过对机器规格要求高磁盘、内存低出问题时的兜底直接手动docker pull排查需要熟悉工具命令适合场景镜像数少、要逐个排查、已有大机器镜像上百、磁盘紧张、追求效率我目前的习惯是首次迁移用路线 B 打底跑完之后用路线 A 做补漏。因为首批总有几个镜像会因为各种原因复制失败补漏的时候量很小用 docker 命令逐个处理最快排查也最清楚。反过来一上来就用路线 A 拉两百个镜像中间随便来个磁盘告警就能让你忙一下午。提示不管走哪条路线先把目标仓库里的两个测试镜像推上去跑通再开始全量。认证、网络策略、仓库命名空间权限这些问题用一个小镜像验证只要五分钟用十个 GB 的镜像验证要半小时。4. 推上去之后的命名约定仓库路径与 kolla 期望的前缀怎么对齐很多人以为“推上去就完事了”其实推上去的路径和 tag才是决定部署能不能跑起来的关键。这一章讲三个必须守住的约定全是踩过坑之后总结的。4.1 不要把 tag 改掉我见过一种“聪明”的做法把2024.1-ubuntu-22.04这种长 tag 简化成2024.1觉得省事好看。这个操作在 kolla 里是致命的。原因在于kolla-ansible是按变量拼出完整 tag 再去拉的它拼出来的 tag 一定带 distro 段。你把它改成短 tag部署时就找不到镜像报manifest unknown或者pull access denied而且报错信息里不会告诉你“其实是 tag 不对”只会说拉不到。更麻烦的是如果部署端配置的是openstack_release: 2024.1、kolla_base_distro: ubuntu这类分散变量你手工改 tag 之后两边就永远对不上。所以规矩很简单上游 tag 是什么你推上去就是什么一个字符都不动。想区分环境改 registry 或 namespace不要动 tag。4.2 namespace 用一级还是两级namespace 的层级直接影响你后面维护的复杂度。kolla 上游用的是两级形式域名 命名空间如果你在自建仓库里也用两级路径结构就是registry.internal.example.com:5000/kolla/nova-api:2024.1-ubuntu-22.04如果你用了带项目层级的两级 namespace比如registry.internal.example.com:5000/infra/kolla/nova-api那就变成三级路径了。这在技术上完全可行但要注意kolla-ansible的docker_namespace变量能不能接受带斜杠的值。有些版本的变量处理会做转义带斜杠可能出问题。我的建议是保持一级 namespace也就是docker_registry填域名端口docker_namespace填kolla。想区分环境就用不同的 registry 地址或者不同的 namespace 名kolla-prod、kolla-test不要往路径里塞层级。4.3 私有仓库的认证与 insecure 配置自家仓库基本都要认证。这里有两个配置层次都要做第一层是操作端也就是你做 push 的那台机器要先登录docker login registry.internal.example.com:5000 \ -u 用户名 -p 密码 # 凭据写入 ~/.docker/config.json如果要批量在多台机器上操作可以用--password-stdin从文件读避免密码出现在 shell history 里cat /path/to/pass.txt | docker login registry.internal.example.com:5000 \ -u 用户名 --password-stdin第二层是部署端也就是跑kolla-ansible的所有节点。这里需要在globals.yml里给出凭据kolla 会在拉镜像时使用docker_registry: registry.internal.example.com:5000 docker_namespace: kolla docker_registry_username: 用户名 docker_registry_password: 密码 docker_registry_insecure: no如果仓库走的是自签证书docker_registry_insecure需要设成yes同时每个节点的 dockerd 都要把这个地址加进insecure-registries否则 docker 层就已经拒绝了kolla 配置再对也没用{ insecure-registries: [registry.internal.example.com:5000] }改完daemon.json记得systemctl daemon-reload systemctl restart docker。这一步在每个节点都要做包括控制节点、计算节点、存储节点。我吃过一次亏只改了控制节点kolla-ansible deploy跑到 nova_libvirt 就卡住日志里是一句含糊的error pulling image翻了半小时才想起来计算节点没配。5. kolla-ansible 侧对接让部署真的从你的仓库拉前面所有工作都是为了这一步。配置改动量其实很小但每一项都不能错。这一章按“必改项 → 验证动作 → 报错对照”的顺序说。5.1 globals.yml 五个必改项/etc/kolla/globals.yml里和镜像来源相关的主要是下面这几项。我一般用grep先确认现有值再统一改grep -nE docker_registry|docker_namespace|openstack_release|kolla_base_distro|network_interface /etc/kolla/globals.yml需要关注的五项配置项作用常见取值docker_registry镜像仓库地址域名端口registry.internal.example.com:5000docker_namespace仓库命名空间kollaopenstack_release决定 tag 的前半段2024.1kolla_base_distro决定 tag 的中段ubuntudocker_registry_username/password仓库认证按实际填改完之后强烈建议做一次配置验证让 kolla 自己把镜像全名拼出来比你口算靠谱kolla-ansible -i ./inventory/all-in-one \ -e /etc/kolla/globals.yml \ prechecks --tags 某个服务名 21 | tee /tmp/prechecks.logprechecks阶段会做镜像存在性检查如果前缀或 tag 不对这里就会暴露出来比跑到deploy一半再失败要省时间得多。这是我每次必做的一步哪怕前面已经验证过。5.2 验证镜像来源的三个动作配置改完了怎么确认部署时真的从你的仓库拉而不是悄悄回退到上游三个动作从粗到细动作一看部署日志里的镜像全名。kolla-ansible deploy的输出里会打印TASK [service : Pulling image xxx]把这一行抓出来看一眼前缀对不对。这是最直接的方式。动作二在目标节点上docker image inspect看 RepoDigests。部署完成后找一个容器对应的镜像docker image inspect image --format {{.RepoDigests}}Digest 里的域名应该指向你的仓库。如果是上游地址说明某处配置没生效。动作三临时断掉对上游的访问做一次反向验证。这个动作最狠也最准把节点上对上游仓库的出站访问暂时阻断然后删掉一个非关键镜像重新执行该服务的部署。如果还能拉下来说明确实走的是你的仓库如果报错说拉不到说明有地方还在指向上游。第三个动作我不建议在正式环境常做但在测试环境验证首次迁移时非常值得。它能把“配置看起来对但其实没生效”这种隐蔽问题一次性揪出来。5.3 报错对照表把迁移过程中实际遇到过的报错整理成表遇到问题时先对号入座报错关键字大概率原因处理方向manifest unknowntag 不对或该镜像没推上去到仓库里查这个路径和 tag 是否存在pull access denied认证未配置或凭据错了检查docker_registry_username/passwordhttp: server gave HTTP response to HTTPS client仓库是 HTTP 但被当 HTTPS 处理加insecure-registries并重启 dockerx509: certificate signed by unknown authority自签证书未被信任分发 CA 证书到/etc/docker/certs.d/registry/no space left on device磁盘满清本地镜像、扩盘、迁># 逐个校验目标仓库里是否存在对应镜像 while read -r name; do [ -z $name ] continue refregistry.internal.example.com:5000/kolla/${name}:${TAG} if skopeo inspect --raw docker://${ref} /dev/null 21; then echo [has] $ref else echo [miss] $ref | tee -a missing.txt fi done images.txt这一步的价值在于它绕开了 docker 的本地状态直接问仓库。本地有镜像不代表远端有远端有也不代表 manifest 完整。只有skopeo inspect --raw能拿到完整 manifest 的才是部署时一定能拉到的。我现在的固定流程是拉取 → 推送 →闭环校验→ 只对缺失项补推 → 再校验一次。补推的量通常很小五六个以内手工处理比跑全量脚本快。最后分享一个我个人觉得最有用的习惯把每次迁移的镜像清单、仓库地址、tag、执行时间记成一个文本文件放在项目目录里。过两周再回来看或者换个同事接手这份记录的价值远超你想象。镜像迁移这种事做一次就忘一次唯一靠得住的是写下来的东西。