镜像(或制品)拉取过程具体细节在不同客户端会有差异,但整体而言都会遵循v2接口的规范分为下面几个步骤
1. 去获取bearer token,这个token在后续的中都需要使用
2. 获取制品的manifest,
3. 根据获取到的manifest信息获取制品元数据config
4. 根据获取到的manifest信息并行拉取各个层
对于镜像的客户端而言,无论他怎么变,都要经过上面几个过程去完成镜像拉取。而其他制品的客户端,差异在第3,第4点。
不同的客户端,或者同一个类型,不同版本的客户端,在怎么样获取到bearer token的URL,也会有差异,因此这里就用docker作为例子,介绍拉取一个镜像,会调用什么接口,各个接口的作用是什么。
第一个接口
当通过docker客户端拉取一个镜像 docker pull registry-crs-huadong1.ctyun.cn/hg-ns1/redis:cl-throttle。dockerd首先会往镜像服务发送一个请求
registry-crs-huadong1.ctyun.cn/v2/
这个请求都会得到有一个固定的响应码 401,同时响应头里面会包含有一个
Www-Authenticate: Bearer realm="{协议}://registry-crs-huadong1.ctyun.cn/service/token",service="ctyun-container-registry"
第二个接口——service token
通过这个响应头,就能构造下一个请求,用来获取bearer token
{协议}://registry-crs-huadong1.ctyun.cn/service/token?service=ctyun-container-registry&scope=repository%3Ahg-ns1%2Fredis%3Apull
这个请求的URL是从v2的响应头Www-Authenticate获取到service/token接口的URL,对于queryString部分service=ctyun-container-registry也是在这个头里面获取,scope则是docker自己构造,这里做了URL编码,解开则是
repository:hg-ns1\redis:pull
这三段参数中,repository是固定的,中间hg-ns1\redis是拉取命令中的命名空间和镜像仓库,最后一个行为,这里是拉取,是pull,假设是要推送镜像时,这里就是push。
特别地,如果遇到是往私有仓库中拉取时,获取service token需要带上拉取凭证。这是请求头会带上-H"Authorization: Basic xxxxxx",否则得到的响应会说不能匿名拉取。而至于这个Basic Token,就要通过下面命令求出
echo -n "userName:password"|base64
这个得出的basic token就是docker login后,保存到~/.docker/config.json里面的凭证。
拿到的响应体是
{"token":"eyJhbGciOiJSUxxxxxxxxor8bRZjSOW8qRkg","access_token":"","expires_in":43200,"issued_at":"2025-05-11T08:35:17Z"}
其中token这个就是后续请求都要用到的bearer token
第三个接口——manifest
接着按照拿到service token接口拿到的bearer token,会请求manifest接口
registry-crs-huadong1.ctyun.cn/v2/hg-ns1/redis/manifests/cl-throttle
这个接口中/v2/一段是固定的,后面紧跟hg-ns1/redis,是镜像的命名空间和仓库名,manifests一段是固定的,最后一段cl-throttle,就是所拉镜像的tag
请求这个接口时需要带上bearer token请求头 -H"Authorization: Bearer xxxx"
然而第一次请求这个接口用的HTTP 方法不是GET,而是HEAD。通过这个HEAD请求的响应头 Docker-Content-Digest: sha256:ec784d649462697aabf712826fc7b237e0237728d66499adf636520f31aa61ba,dockerd会与本地的仓库中现存的manifest进行比对,如果已经存在了,就证明这个镜像已经早就拉取下来,那拉取过程就会结束。当然一般情况下本地没有的,就会再次请求上面的接口,这次就是用GET请求,而不是用HEAD了,获取到的响应就是manifest的内容了
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 8189,
"digest": "sha256:8022c4c620a219fcc91298d084b57cde2b4e37ddd2275e4d94cded74ea51397a"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 27093851,
"digest": "sha256:b2f14fd03aa1067f4854fa4c791fd5f2550e57085e2b75935bdb538168ff6038"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 1739,
"digest": "sha256:ee57bca4d6329839cb0c1192b79ff660513c043b0848d35f14ccb8520816a296"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 1357426,
"digest": "sha256:62a26204ddc975218a0fbe59d8e7531c5f9e585382d902e6739fdcaf25a6ef70"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 7334485,
"digest": "sha256:e5853dc9148600cfe92484c1a335eac3a472cef69440d27447c98a43f27b3adf"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 97,
"digest": "sha256:2b69cfbcbf2775f250ef208c2739c2e6ad64392885f89fbde8d67fc4f911dd31"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 409,
"digest": "sha256:af42f1f6d0f18564ff4c740b11c093482e028a3e2a983fe2a33e674145113610"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 142,
"digest": "sha256:25a104246c957df5633e68ba9b3b2bb0204e75d9d0de5f1c4abb917f80abe31e"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 18268809,
"digest": "sha256:dfcf0c5bfc766cc80f6ef5ff3876df4969831c3be122933f8b218300680db6a6"
}
]
}
在这里能得到镜像config各个层的的信息,这些信息包括mediaType,大小和digest。后面拉取层和config,都是用同一个接口拉取
话说回头,对于不同客户端。获取并构造service/token URL的方式会有不同,像本例子中的docker,就是从/v2/接口开始。而有些客户端,就是从这个manifest接口开始请求,用这个接口不带Bearer token请求时,也是得到一个401的响应,而响应头Www-Authenticate会多了一个scope,此时的service token的scope参数就是按照响应头生成
Www-Authenticate: Bearer realm="registry-crs-huadong1.ctyun.cn/service/token",service="ctyun-container-registry",scope="repository:hg-ns1/redis:pull"
第四个接口——blob
最后就是获取各个层和config了,请求的接口都是一个,因为都是把他们当作一个块进行处理,例如其中一个层的接口如下
registry-crs-huadong1.ctyun.cn/v2/hg-ns1/redis/blobs/sha256:dfcf0c5bfc766cc80f6ef5ff3876df4969831c3be122933f8b218300680db6a6
接口前面几段与manifest的相似,blobs代表这个是blob接口,最后一段是块的digest。
与上面的manifest接口请求的时候相似,他先会调用一个HEAD请求,然后才调用GET获取
这时如果是开启了对象存储重定向的,GET请求会收到一个307的响应,响应头里面会有个Location,这个是一个对象存储的URL,拉取块直接从对象存储拉取,会给拉取过程提速
当然最普通情况下没开启对象存储重定向时,获取的则是这个块的内容。有可能是文本,有可能是压缩的二进制流
各个层就是被这样一个一个拉下来,直到拉完位置,这个镜像拉取过程就此结束