[TOC]
在深入学习镜像之前我们需要知道镜像是如何(炼制/搓)
成的(等同于构建镜像),当然是通过我们DockerFile一条条指令为镜像生成每一层,按照执行顺序镜像文件系统复写封装从下到上;
关于容器镜像的OCI标准协议,那什么又是OCI标准协议?
答: Open Container Initiative(打开集装箱倡议)旨在围绕容器格式和运行时制定一个开放的工业化标准; 参考地址:容器开放接口规范(CRI OCI)
Docker 公司与 CoreOS 和 Google 共同创建了 OCI (Open Container Initial),并提供了三种规范
不常见
)关于 OCI 规范的作用说明:
不限于某种特定操作系统、硬件、CPU架构、公有云
等; 概括来说就是不受上层结构的绑定,如特定的客户端、编排栈
等OCI runtime filesytem bundle
的标准格式连接在一起,OCI 镜像可以通过工具转换成bundle然后OCI容器引擎能够识别这个 bundle 来运行容器, 其优点如下;参考来源:
1.OCI 镜像规范的主要由以下几个 markdown 文件组成:
├── annotations.md # 注解规范
├── config.md # image config 文件规范
├── considerations.md # 注意事项
├── conversion.md # 转换为 OCI 运行时
├── descriptor.md # OCI Content Descriptors 内容描述
├── image-index.md # manifest list 文件
├── image-layout.md # 镜像的布局
├── implementations.md # 使用 OCI 规范的项目
├── layer.md # 镜像层 layer 规范
├── manifest.md # manifest 规范
├── media-types.md # 文件类型
├── README.md # README 文档
├── spec.md # OCI 镜像规范的概览
描述:它决定了我们镜像按照什么标准来构建
,以及构建完镜像之后如何存放
,接着下文提到的 Dockerfile 则决定了镜像的 layer 内容以及镜像的一些元数据信息。
直白的说一个镜像规范 image-spec 和一个 Dockerfile 就指导着我们构建一个镜像;
总结以上几个 markdown 文件 OCI 容器镜像规范主要包括以下几块内容:
Layer : Docker 以 layer (镜像层) 保存的文件系统以及每个Layer保存与上层之间变化部分,以及对保存哪些文件,怎么表示增加、修改和删除的文件等进行描述;
Image-Config : 保存了文件系统的层级信息(每个层级的 hash 值,以及历史信息
)以及容器运行时需要的一些信息(比如环境变量、工作目录、命令参数、mount 列表),指定了镜像在某个特定平台和系统的配置:
# 比较接近我们使用 docker inspect <image|id> 看到的内容
{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
"Cmd": ["bash"],
"Image": "sha256:ba8f577813c7bdf6b737f638dffbc688aa1df2ff28a826a6c46bae722977b549",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"container": "38501d5aa48c080884f4dc6fd4b1b6590ff1607d9e7a12e1cef1d86a3fdc32df",
"container_config": {
"Hostname": "38501d5aa48c",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"bash\"]"
],
"Image": "sha256:ba8f577813c7bdf6b737f638dffbc688aa1df2ff28a826a6c46bae722977b549",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"created": "2020-06-07T01:59:47.348924716Z",
"docker_version": "19.03.5",
"history": [{
"created": "2020-06-07T01:59:46.877600299Z",
"created_by": "/bin/sh -c #(nop) ADD file:a82014afc29e7b364ac95223b22ebafad46cc9318951a85027a49f9ce1a99461 in / "
},{
"created": "2020-06-07T01:59:47.348924716Z",
"created_by": "/bin/sh -c #(nop) CMD [\"bash\"]",
"empty_layer": true
}],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": ["sha256:d1b85e6186f67d9925c622a7a6e66faa447e767f90f65ae47cdc817c629fa956"]
}
}
manifest : 镜像的config文件索引包括了Layer/Annotation
其文件中保存了很多和当前平台有关信息存放于在 registry 中
。
您可以在镜像仓库中通过Registry API请求获取镜像Manifest中的信息, 当我们拉取镜像的时候会根据该文件拉取相应的 layer,比如后面实现的不解压镜像拷贝;
镜像的 manifest 文件(图像清单规范)主要有以下三个目标:
第一个目标是内容可寻址的图像,通过支持的图像模型,其中所述图像的配置可被散列以生成图像和它的组件的唯一ID。
第二个目标是让多架构的图像,通过“mainfest”,这对于图像的特定于平台的版本参考图像清单。在OCI,这是在图像索引编入。
第三个目标是要翻译到OCI运行规范。
版本: 目前主流的版本是 Manifest Version 2, Schema 2 官方参考说明
注意: manifest 中的 layer 和 config 中的 layer 表达的虽然都是镜像的 layer ,但二者代表的意义不太一样;
注意: registry 中会有个 Manifest List 文件
,该文件是为不同处理器体系架构而设计的,通过该文件指向与该处理器体系架构相对应的 Image Manifest;
总结: 容器镜像的 Config,和 Layers 中的每一层,都是以 Blob 的方式存储在镜像仓库中的,它们的 digest 作为 Key 存在。因此在请求到镜像的 Manifest 后,Docker 会利用 digest 并行下载所有的 Blobs
,其中就包括 Config 和所有的 Layers。
//Manifest List
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 7143,
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
"platform": {
"architecture": "ppc64le",
"os": "linux",
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 7682,
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
"platform": {
"architecture": "amd64",
"os": "linux",
"features": [
"sse4"
]
}
}
]
}
// Image Manifest : 可通过 Registry API 请求查看到;
{
// Manifest Version 2, Schema 2
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
// 其定义包括两个部分,分别是 Config 和 Layers 且都包含三个字段分别是 digest、mediaType 和 size
// Config 是一个 JSON 对象
"config": {
// 内容类型
// 镜像的元数据: 关于容器镜像的配置,通常它会被镜像仓库用来在 UI 中展示信息,以及区分不同操作系统的构建等。
"mediaType": "application/vnd.docker.container.image.v1+json",
// 内容的大小
"size": 1509,
// 对象的 ID
"digest": "sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e"
},
// Layers 是一个由 JSON 对象组成的数组
"layers": [
{
// 众所周知,容器镜像是分层构建的,每一层就对应着 Layers 中的一个对象。
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", // 常见形式
"size": 5844992,
"digest": "sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a"
}
]
}
Image manifest index - OCI图像索引规范
描述:在 docker 的 distribution 中称之为 Manifest List 在 OCI 中就叫OCI Image Index Specification
,实际上两者是指的同一个文件,甚至两者 GitHub 上文档给的 example 都一一模样🤣,应该是 OCI 复制粘贴 Docker 的文档;
index 文件是个可选的文件,包含着一个列表为同一个镜像不同的处理器 arch 指向不同平台的 manifest 文件,它保证一个镜像可以跨平台使用
,即每个处理器 arch 平台拥有不同的 manifest 文件,使用 index 作为索引。
当我们使用 arm 架构的处理器时要额外注意,在拉取镜像的时候要拉取 arm 架构的镜像,一般处理器的架构都接在镜像的 tag 后面,默认 latest tag 的镜像是 x86 的,在 arm 处理器的机器这些镜像上是跑不起来的。
在深入学习Docker原理过程中发现Docker info中的Runtimes字段,由于个人阅历有限不得不Google一下所以有了以下内容;
$docker info | grep -i "runtime"
# Docker、Google等公司开源了用于运行容器的工具和库 runc,在此之后,各种运行时工具和库也慢慢出现,例如 rkt、containerd、cri-o 等,然而这些工具所拥有的功能却不尽相同,有的只有运行容器(runc、lxc),而有的除此之外也可以对镜像进行管理(containerd、cri-o)。
Runtimes: runc
Default Runtime: runc
Q: 什么是runtimes?
答:从字面理解Runtimes我们知道其为运行时实际它是Docker容器在运行时的一个周期等; 简单的说就是容器运行时,传统意义上来说就是代表容器从拉取镜像到启动运行再到中止的整个生命周期
容器在运行时分为两类:
其两者的区别是:
Q: 从上面的表达中我们知道runc是一个low-level 运行时,那它与Docker有何关系?
答: runC是一个根据OCI标准创建并运行容器的命令行工具(CLI tool), runC是docker中最为核心的部分,容器的创建,运行,销毁等等操作最终都将通过调用runc完成。 而runC也有自己的客户端,后来被提取出来作为一个单独的工具和库。其实现了 OCI 规范,包含config.json文件和容器的根文件系统。
描述:该规范定义着容器镜像如何存储在远端的Registry上,使用规范的好处是可以帮助我们把这些镜像按照约定俗成的格式来存放即方便第三方工具的存取;
目前实现该规范的 registry 就 docker 家的 registry 使用的多一些。其他的 registry 比如 harbor 、 quay.io 使用的也比较多。
描述:相信读者如果对docker有个基础了解以及看了我前面的文章,想必对它不感到陌生吧;众所周知 docker 镜像需要一个 Dockerfile 来构建而成; 当我们对 OCI 镜像规范有了个大致解之后,接下来就拿着 Dockerfile 这个 “图纸” 去一步步构建镜像。
此处不再详细DockerFile的详细书写以及最佳优化技巧,感兴趣的朋友可以参看前面的文章;
下面我们基于docker build test-image
进一步理解目录结构;
FROM alpine
LABEL name="test-image"
RUN apk -v add --no-cache bash
RUN apk -v add --no-cache curl
COPY ./startService.sh /
CMD ["/bin/bash", "/startService.sh"]
构建过程输出如下:
$docker build -t test-image .
Sending build context to Docker daemon 3.072kB
Step 1/6 : FROM alpine
---> 3f53bb00af94
Step 2/6 : LABEL name="test-image"
---> Running in 3bd6320fc291
Removing intermediate container 3bd6320fc291
---> bb97dd1fb1a1
Step 3/6 : RUN apk -v add --no-cache bash
---> Running in f9987ff57ad7
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz
(1/5) Installing ncurses-terminfo-base (6.1_p20180818-r1)
(2/5) Installing ncurses-terminfo (6.1_p20180818-r1)
(3/5) Installing ncurses-libs (6.1_p20180818-r1)
(4/5) Installing readline (7.0.003-r0)
(5/5) Installing bash (4.4.19-r1)
Executing bash-4.4.19-r1.post-install
Executing busybox-1.28.4-r2.trigger
OK: 18 packages, 136 dirs, 2877 files, 13 MiB
Removing intermediate container f9987ff57ad7
---> a5635f1b1d00
Step 4/6 : RUN apk -v add --no-cache curl
---> Running in c49fb2e4b311
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz
(1/5) Installing ca-certificates (20171114-r3)
(2/5) Installing nghttp2-libs (1.32.0-r0)
(3/5) Installing libssh2 (1.8.0-r3)
(4/5) Installing libcurl (7.61.1-r1)
(5/5) Installing curl (7.61.1-r1)
Executing busybox-1.28.4-r2.trigger
Executing ca-certificates-20171114-r3.trigger
OK: 23 packages, 141 dirs, 3040 files, 15 MiB
Removing intermediate container c49fb2e4b311
---> 9156d1521a2f
Step 5/6 : COPY ./startService.sh /
---> 704626646baf
Step 6/6 : CMD ["/bin/bash", "/startService.sh"]
---> Running in 1c5e6e861264
Removing intermediate container 1c5e6e861264
---> 6cd0a66e83f1
Successfully built 6cd0a66e83f1
Successfully tagged test-image:latest
构建过程如图所示:
WeiyiGeek.docker
描述:通过前面的基础学习我们知道Docker 是一个典型的 C/S 架构的应用,分为 Docker 客户端(即平时敲的 docker 命令)
和 Docker 服务端(dockerd 守护进程)
。
主要特征:
(/var/run/docker.sock)
和服务端通信;(tcp://xxx.xx.xx.xx:2375)
进行与服务端通信。如果说炼制镜像也需要个工厂的话,那么 dockerd 守护进程就是个生产镜像的工厂。
Tips: 镜像工厂不止Docker一家还有Redhat家的Buildah也能生产镜像,两者区别如下:
构建镜像docker build
的流程:
RUN: chroot . /bin/bash -c "apt get update……"
;\Tips: 我们可以采用docker history <imageName:Tag>
命令来逆向推算出 docker build 的过程。
现在有个问题来了基础镜像是如何生成?总不会是凭空捏造的吧!
答: 实际上它也是通过Dockerfile中的指令来构建的, 比如以
debian:buster
该基础镜像为例看一下基础镜像是如何炼成的;
Base Image Dockerfile
# 自从 docker 1.5 版本开始在 Dockerfile 中 FROM scratch 指令并不进行任何操作也就是不会创建一个镜像层;
FROM scratch
# ADD指令把 rootfs.tar.xz 解压到 / 目录下,由此产生的一层镜像就是最终构建的镜像真实的 layer 内容
ADD rootfs.tar.xz /
# 指定这镜像在启动容器的时候执行的应用程序,一般基础镜像的 CMD 默认为 bash 或者 sh 。
CMD ["bash"]
注意事项:
1) 上述中的scratch镜像并不是真实存在的,当您使用docker pull命令下载它时候会提示Error response from daemon: 'scratch' is a reserved name
;
2) 上述中的rootfs.tar.xz
是发行版源码编译的可以在docker-debian-artifacts中找到它,它是一个搓出来的根文件系统;
如果对于其源码构建感兴趣可以参考debian 基础镜像的 Jenkins 流水线任务debuerreotype
意外发现 Debian 官方是将所有 arch 和所有版本的 rootfs.tar.xz
都放在这个 repo 里的,以至于这个 repo 的大小接近 2.88 GiB;
# rootfs.tar.xz Repo 下载
$ git clone https://github.com/debuerreotype/docker-debian-artifacts
Receiving objects: 100% (660/660), 2.88 GiB | 16.63 MiB/s, done.
# rootfs.tar.xz 解压后查看我所说的他是一个Linux根文件系统
# 分支切换
git checkout dist-amd64
cd buster && tar -xvf rootfs.tar.xz -C !$
# 不同于我们安装CentOS系统的那个根文件系统
ls rootfs/
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
# 该根文件系统是经过一系列裁剪去掉容器中必要的文件,使之更加轻量适用于容器运行场景其体积大小大概为125MB
125M rootfs
# 如果使用slim的rootfs.tar.xz会更小一些;
du -sh slim/rootfs
76M slim/rootfs
3) 此时在上述环境中我们非常方便的自己构建debian:buster 基础镜像
# 在 buster 目录中进行执行镜像构建命令
docker build -t debian:buster .
# 下面就是构建 Debian 基础镜像的过程,正如 Dockerfile 中的那样,最终只产生了一层镜像。
Sending build context to Docker daemon 30.12MB
# Step 1/3 : FROM scratch
# Step 2/3 : ADD rootfs.tar.xz /
# 1756d6a585ae
# Step 3/3 : CMD ["bash"]
# Running in c86a8b6deb3d
# Removing intermediate container c86a8b6deb3d
# 04948daa3c2e
Successfully built 04948daa3c2e
描述: Docker 在构建完镜像后会将其存储在Docker 本地存储家目录中,默认情况即/var/lib/docker
, 如果修改了Docker ROOT Dir 目录的可以通过docker info | grep "Docker Root Dir"
命令查看;
对于镜像存储路径最重要的 image 和 overlay2 这个两个目录,容器的元数据存放在 image 目录下,容器的 layer 数据则存放在 overlay2 目录下。
# 镜像元数据目录
/var/lib/docker/image
# overlay2 表示本地使用的存储引擎
/var/lib/docker/image/overlay2/
# 该文件存储镜像元数据信息即由image name 和 digest 进行组合, 而Digest又与Image ID 对应
# 当我们 docker run 一个容器的时候也用到这个文件去索引本地是否存在该镜像,没有镜像的话就自动去 pull 这个镜像。
/var/lib/docker/image/overlay2/repositories.json
# {
# "Repositories": {
# "debian": {
# "debian:v1": "sha256:cfba37fd24f80f59e5d7c1f7735cae7a383e887d8cff7e2762fdd78c0d73568d",
# "debian:v2": "sha256:e6e782a57a51d01168907938beb5cd5af24fcb7ebed8f0b32c203137ace6d3df"
# },
# }
# }
# 镜像 layer 的存储目录
/var/lib/docker/overlay2
# diff 的上级目录就是以镜像 layer 的 digest 为名的目录
# overlay2
# ├── 259cf6934509a674b1158f0a6c90c60c133fd11189f98945c7c3a524784509ff
# │ └── diff
# │ ├── bin
# │ ├── dev
# │ ├── etc
# │ ├── home
# │ ├── lib
# │ ├── media
# │ ├── mnt
# │ ├── opt
# │ ├── proc
# │ ├── root
# │ ├── run
# │ ├── sbin
# │ ├── srv
# │ ├── sys
# │ ├── tmp
# │ ├── usr
# │ └── var
# 查看镜像Layer绑定的Links名称
$more /var/lib/docker/overlay2/30a121491fc6ba331b30f25c411fbe62f9e8e4c24fbe372cd046f4ab601f94bd/link
IEBGN75T7KVLDOEHY5COAGEWQ2
# 其中还有个 l 文件夹(Links)下面有一坨坨的硬链接文件指向上级目录的 layer 目录(便于mount由于digest为256长度太长)
tree /var/lib/docker/overlay2/l/
# └─ l
# ├── PCIS4FYUJP4X2D4RWB7ETFL6K2 -> ../259cf6934509a674b1158f0a6c90c60c133fd11189f98945c7c3a524784509ff/diff
# └── XK5IA4BWQ2CIS667J3SXPXGQK5 -> ../e8f6e78aa1afeb96039c56f652bb6cd4bbd3daad172324c2172bad9b6c0a968d/diff
$ls -lha l/ | grep "IEBGN75T7KVLDOEHY5COAGEWQ2"
lrwxrwxrwx. 1 root root 72 6月 29 14:41 IEBGN75T7KVLDOEHY5COAGEWQ2 -> ../30a121491fc6ba331b30f25c411fbe62f9e8e4c24fbe372cd046f4ab601f94bd/diff
WeiyiGeek.镜像在本地的存储
描述:镜像仓库主要是用来存储和分发镜像的,并对外提供一套 HTTP API V2。
镜像仓库中的所有镜像都是以数据块 (Blob) 的方式存储
在文件系统中。 支持多种文件系统,主要包括filesystem,S3,Swift,OSS
等。
下面详细介绍一下,镜像的所有数据,是如何存储在镜像仓库的文件系统中的?
为了分析镜像如何存放在Registry上,我们可以在本地docker环境中启动registry容器来实验即可,所以在这种场景下不太合适用Harbor这种重量级的Registry镜像仓库实践;
Step1.Registry 镜像容器启动
# 拉取并运行 registry 容器,注意此处为了方便演示没对Registry设置安全认证设置在实际环境中必须进行设置
$docker run -d --name registry -p 9188:5000 -v /var/lib/registry:/var/lib/registry registry
$docker run -d --name registry -p 5000:5000 -v /var/lib/registry:/var/lib/registry registry
335ea763a2fa4508ebf3ec6f8b11f3b620a11bdcaa0ab43176b781427e0beee6
Step2.上传本地镜像到 Registry 仓库
# 1) 列出本地镜像
$docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-hello scratch cb05b87d0012 5 days ago 2.07MB
go-hello stage 5934753f8f4f 5 days ago 73.9MB
# 2) 将镜像标签重新命名为registry地址+镜像名称+版本
$docker tag go-hello:scratch 127.0.0.1:5000/go-hello:scratch
$docker push 127.0.0.1:5000/go-hello:scratch
# The push refers to repository [127.0.0.1:5000/go-hello]
# a3586d882361: Pushed
# scratch: digest: sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e size: 528
$docker tag go-hello:stage 127.0.0.1:5000/go-hello:stage
$docker push 127.0.0.1:5000/go-hello:stage
# The push refers to repository [127.0.0.1:5000/go-hello]
# f5c12d3201a7: Pushed
# 095624243293: Pushed
# a37e74863e72: Pushed
# 8eeb4a14bcb4: Pushed
# ce3011290956: Pushed
# stage: digest: sha256:b96f0ada640f7d628a4f22766f5432cbb1a4dfa208b03dc6444f8c699da9533c size: 1360
Step3.当我们在本地启动一个 registry 容器之后,容器内默认的存储位置为 /var/lib/registry
$ tree -d /var/lib/registry/docker/registry
registry
└── v2
├── blobs
│ └── sha256
│ ├── 0e
│ │ └── 0e83886eae4fa0f68c7212e4667e030f8c95aa3669f68257a0fa45bf233492b6
│ ├── 32
│ │ └── 323d0d660b6a7da8df08a01dbc7250f38cfa2161db00c7c27c0b20be07a8236a
│ ├── 3f
│ │ └── 3ff22d22a8554f746f90a78b501da38d56a46f2ddba0dfec8b260aebaa61b3ba
│ ├── 53
│ │ └── 5395625ce01dee311e2f7c879b0b148ac7525de7aad5080a518d7f7e5a99d368
│ │ └── layer
│ ├── 59
│ │ └── 5934753f8f4f23efdff6e5108997385f17c3b0b25426fe54eb373fb2f1112d87
│ ├── 8d
│ │ └── 8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e
│ ├── b7
│ │ └── b7f616834fd07522cbfd33f0dfa848903599320b5c7191b59fe9aa7562f956a1
│ ├── b9
│ │ └── b96f0ada640f7d628a4f22766f5432cbb1a4dfa208b03dc6444f8c699da9533c
│ ├── cb
│ │ └── cb05b87d001253772ae9a212200de5eb8304ab9691c61589332a2f57e7059209
│ └── e7
│ └── e7cb79d19722c46b9c0829811d7a5a0ae82c8771ab7f2f68e7d3a3ed6bd5d5d0
└── repositories
└── go-hello
├── _layers
│ └── sha256
│ ├── 0e83886eae4fa0f68c7212e4667e030f8c95aa3669f68257a0fa45bf233492b6
│ ├── 323d0d660b6a7da8df08a01dbc7250f38cfa2161db00c7c27c0b20be07a8236a
│ ├── 3ff22d22a8554f746f90a78b501da38d56a46f2ddba0dfec8b260aebaa61b3ba
│ ├── 5395625ce01dee311e2f7c879b0b148ac7525de7aad5080a518d7f7e5a99d368
│ ├── 5934753f8f4f23efdff6e5108997385f17c3b0b25426fe54eb373fb2f1112d87
│ ├── b7f616834fd07522cbfd33f0dfa848903599320b5c7191b59fe9aa7562f956a1
│ ├── cb05b87d001253772ae9a212200de5eb8304ab9691c61589332a2f57e7059209
│ └── e7cb79d19722c46b9c0829811d7a5a0ae82c8771ab7f2f68e7d3a3ed6bd5d5d0
├── _manifests
│ ├── revisions
│ │ └── sha256
│ │ ├── 8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e
│ │ └── b96f0ada640f7d628a4f22766f5432cbb1a4dfa208b03dc6444f8c699da9533c
│ └── tags
│ ├── scratch
│ │ ├── current
│ │ └── index
│ │ └── sha256
│ │ └── 8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e
│ └── stage
│ ├── current
│ └── index
│ └── sha256
│ └── b96f0ada640f7d628a4f22766f5432cbb1a4dfa208b03dc6444f8c699da9533c
└── _uploads
# 目录说明
blobs 目录: 存储镜像的Layer 数据以及 Manifest 和 Image Config,这些文件都是以 data 为名的文件存放在于该层Layer sha256 相对应的目录下;cd..
- 文件最大的 data 文件实际是一个压缩文件,在Docker拉取镜像时候会下载该data文件,等待完成后docker-untar进程将会把data文件解压到`/var/lib/docker/overlay2/${digest}/diff`目录下
- 文件最小的 data 文件实际是Manifest,它记录了一个镜像所包含的 layer 信息,当我们 pull 镜像的时候会使用到这个文件;
- 文件介于最大和最小 的data文件实际是 Image Config 存储镜像相关信息(架构、容器配置、创建实际) 即 `docker inspect image_id` 命令所看到的一些信息:
repositories 目录: 存储镜像在仓库相关的信息,它们为了使用以内容寻址的 sha256 散列存储方便索引文件;
- _uploads: 它是临时文件夹主要用来存放 push 镜像过程中的文件数据,并且当镜像 layer 上传完成之后会清空该文件夹; 其中的 data 文件上传完毕后会移动到 blobs 目录下,并根据该文件的 sha256 值来进行散列存储到相应的目录下。
- _manifests: 该文件夹是镜像上传完成之后由Registry来生成的,在该目录下的文件都是一个名为link的文本文件其值指向blobs目录下与之对应的目录;在该目录中包含镜像的tags 和 revisions 信息,每一个镜像的每一个Tag对应着于tag名称相同的目录;
# - _revisions 目录里存放了该 repository 历史上上传版本的所有 sha256 编码信息。
# - tags/current 目录下的 link 文件保存了该 tag 目前的 manifest 文件的 sha256 编码对应在 blobs 中的 sha256 目录下的 data 文件
# - tags/index 目录则列出了该 tag 历史上传的所有版本的 sha256 编码信息
Step4.当我们 pull 镜像的时候如果不指定镜像的 tag名默认就是 latest, registry 会从 HTTP 请求中解析到这个tag名,然后根据tag名目录下的 link 文件找到该镜像的 manifest的位置返回给客户端
,客户端接着去请求这个manifest 文件,客户端根据这个 manifest 文件来 pull 相应的镜像 layer 。
# 找到Manifest位置
$cat /var/lib/registry/docker/registry/v2/repositories/go-hello/_manifests/tags/scratch/current/link
sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e
# 进入查看Manifest内容
$ls -lah /var/lib/registry/docker/registry/v2/blobs/sha256/8d/8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e/
总用量 4.0K
drwxr-xr-x. 2 root root 18 8月 2 09:58 .
drwxr-xr-x. 3 root root 78 8月 2 09:58 ..
-rw-r--r--. 1 root root 528 8月 2 09:58 data
$cat /var/lib/registry/docker/registry/v2/blobs/sha256/8d/8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e/data
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
# 镜像拉取时镜像Image Config文件地址
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1472,
"digest": "sha256:cb05b87d001253772ae9a212200de5eb8304ab9691c61589332a2f57e7059209"
},
# 镜像拉取时下载并利用docker-untar线程解压
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 1106793,
"digest": "sha256:5395625ce01dee311e2f7c879b0b148ac7525de7aad5080a518d7f7e5a99d368"
}
]
}
总结思想:
描述: 当我们在本地构建完成一个镜像之后,如何传递给他人呢?此时下面的文章或者技巧能更好帮助您;
这里需要利用GitHub(git)进行做类比,实际上搬运镜像的流程就像在Github上搬运代码一样;
Github 代码仓库 == Docker registry镜像仓库(Harbor-公共/私有的镜像)
# Registry: https://index.docker.io/v1/
git clone|pull == docker pull # 拉取
git push == docker push # 推送
描述:为了方便理解docker full拉取镜像流程可以参看OCI Registry规范中的文档地址,下面是结合大佬的博客简单梳理一下 pull 一个镜像的大致流程:
WeiyiGeek.向大佬借的图
结合上图大致的流程,我将远程的镜像仓库拉取到本地来给容器运行时使用具体流程如下:
Step1.配置Registry镜像仓库的auth认证信息
# Auth 认证信息配置在Docker pull时候会向registry进行鉴权随后会取得的token;
# 在随后的HTTP请求中都要包含该Token才能有权限进行操作;
cat ~/.docker/config.json
{
"auths": { "https://registry.k8s.li/v2/": {"auth": "d2VicH855828WM7bSVsslJFpmQE43Sw=="}},
"HttpHeaders": {
"User-Agent": "Docker-Client/19.03.5 (linux)"
},
"experimental": "enabled"
}
Step2.dockerd 守护进程解析 docker 客户端参数由镜像名 + tag
构成向 registry 请求Manifest 文件,
# registry 中一个镜像有多个 tag 或者多个处理器体系架构的镜像,则根据这个 tag 来返回给客户端与之对应的 manifest 文件;
# HTTP 请求为GET /v2/<name>/manifests/<reference> 下面参数的解释与上面的Manifest解释对应
GET /v2/<name>/manifests/<reference>
{
"annotations": {
"com.example.key1": "value1",
"com.example.key2": "value2"
},
# 验证已下载的层使用
"config": {
"digest": "sha256:6f4e69a5ff18d92e7315e3ee31c62165ebf25bfa05cad05c0d09d8f412dae401",
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 452
},
# 下载拉取使用
"layers": [{
"digest": "sha256:6f4e69a5ff18d92e7315e3ee31c62165ebf25bfa05cad05c0d09d8f412dae401",
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 78343
}
],
"schemaVersion": 2
}
Step3.docker 守护进程解析这个 Manifest 文件获取镜像的 layer 的信息;然后dockerd守护进程并行下载各 layer ,HTTP 请求为GET /v2/<name>/blobs/<digest>
。
Step4.dockerd 起一个单独的进程 docker-untar 来 gzip 解压缩已经下载完成的 layer 文件;注意对于有些比较大的镜像(比如几十 GB 的镜像),往往镜像的 layer 已经下载完成了,但还没有解压完;
$ls /var/lib/docker/overlay2/30a121491fc6ba331b30f25c411fbe62f9e8e4c24fbe372cd046f4ab601f94bd/diff/
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
# 默认该命令不存在的
$docker-untar /var/lib/docker/overlay2/30a121491fc6ba331b30f25c411fbe62f9e8e4c24fbe372cd046f4ab601f94bd/diff/
Step5.验证 image config 中的 RootFS.DiffIDs
是否与下载(解压后)hash 相同;
Step6.解析 Manifest 获取镜像 Configuration,验证镜像是否正确。
描述: 采用 docker push 推送一个镜像到远程的Registry流程恰好与docker pull拉取镜像到本地流程相反; 它先获取要上传镜像的Layer信息推送到Registry,当镜像的所有layer上传完毕后再push Image Manifest 到Registry;
具体流程:
Step1.push 镜像到Registry仓库中需要进行鉴权同时返回一个Token(后续利用它作为身份验证);
Step2.向Registry发起请求POST /v2/<name>/blobs/uploads/
,registry 返回一个上传镜像 layer 时要应到的 URL;
Step3.客户端通过HEAD /v2/<name>/blobs/<digest>
请求检查 registry 中是否已经存在镜像的 layer。
Step4.客户端通过URL 使用 POST 方法来实时上传 layer 数据即上传镜像,但是上传镜像 layer 分为 Monolithic Upload (整体上传)
和Chunked Upload(分块上传)
两种方式。
# 1.Monolithic Upload
PUT /v2/<name>/blobs/uploads/<session_id>?digest=<digest>
Content-Length: <size of layer>
Content-Type: application/octet-stream
<Layer Binary Data>
# 2.Chunked Upload
PATCH /v2/<name>/blobs/uploads/<session_id>
Content-Length: <size of chunk>
Content-Range: <start of range>-<end of range>
Content-Type: application/octet-stream
<Layer Chunk Binary Data>
Step5.镜像的 layer 上传完成之后,客户端需要向 registry 发送一个 PUT HTTP 请求告知该 layer 已经上传完毕。
PUT /v2/<name>/blobs/uploads/<session_id>?digest=<digest>
Content-Length: <size of chunk>
Content-Range: <start of range>-<end of range>
Content-Type: application/octet-stream
<Last Layer Chunk Binary Data>
Step6.最后当所有的 layer 上传完之后客户端再 PUT 请求将 manifest 推送上去就完事儿了。
PUT /v2/<name>/manifests/<reference>
Content-Type: <manifest media type>
{
"annotations": {
"com.example.key1": "value1",
"com.example.key2": "value2"
},
"config": {
"digest": "sha256:6f4e69a5ff18d92e7315e3ee31c62165ebf25bfa05cad05c0d09d8f412dae401",
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 452
},
"layers": [
{
"digest": "sha256:6f4e69a5ff18d92e7315e3ee31c62165ebf25bfa05cad05c0d09d8f412dae401",
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"size": 78343
}
],
"schemaVersion": 2
}
描述: 它是一个Python脚本它使用Request库请求Registry API来从镜像仓库中拉去镜像并保存为tar包, 拉取完成后使用Docker load加载到本地docker镜像中;
它与docker pull 以及 Skopeo copy有相似原理,相同的是它们都会去调用Registry API;
Install && Usage:
# Download docker-dray
wget https://raw.githubusercontent.com/NotGlop/docker-drag/master/docker_pull.py
# Syntax
python3 docker_pull.py [image name]
# Usage
$python3 docker_pull.py nginx
Creating image structure in: tmp_nginx_latest
afb6ec6fdc1c: Pull complete [27098756]
dd3ac8106a0b: Pull complete [26210578] ]
8de28bdda69b: Pull complete [538]
a2c431ac2669: Pull complete [900]
e070d03fd1b5: Pull complete [669]
Docker image pulled: library_nginx.tar
$docker load -i library_nginx.tar
ffc9b21953f4: Loading layer [==================================================>] 72.49MB/72.49MB
d9c0b16c8d5b: Loading layer [==================================================>] 63.81MB/63.81MB
8c7fd6263c1f: Loading layer [==================================================>] 3.072kB/3.072kB
077ae58ac205: Loading layer [==================================================>] 4.096kB/4.096kB
787328500ad5: Loading layer [==================================================>] 3.584kB/3.584kB
Loaded image: nginx:latest
描述: Skopeo由Redhat 红帽开发的,它与Podman和Buildah 简称(PSB)下一代容器架构中的一员; 红帽家的Podman推出是想要取代Docker和Containerd容器运行时,虽然它符合OCI规范但是对现在企业来讲基本采用Docker并且也已经应用在生产环境之中了,替换的成本并不值得他们去换到 PSB 上去;
Q: 但是丝毫不影响我们使用Skopeo镜像搬运工具,为什么说它是一个神器呢?
答: 因为它可以针对不同存在方式的镜像Layer同步或者复制到指定镜像仓库中,这样做的好处是免去了像
docker pull –> docker tag –> docker push
那样 pull 镜像对镜像进行解压缩,push 镜像进行压缩。尤其是在搬运一些较大的镜像(几GB 或者几十 GB的镜像,比如 nvidia/cuda ),使用 skopeo copy 的加速效果十分明显。 比如在CI/CD流水线中搬运两个镜像仓库而不必打包然后再分别push到镜像之中;
例如,用 skopeo inspect 命令可以很方方便地通过 registry 的 API 来查看镜像的 manifest 文件,以前我都是用 curl 命令的,要 token 还要加一堆参数,所以比较麻烦,所以后来就用上了 skopeo inspect
root@deploy:/root # skopeo inspect docker://index.docker.io/webpsh/webps:latest --raw
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 2534,
"digest": "sha256:30d9679b0b1ca7e56096eca0cdb7a6eedc29b63968f25156ef60dec27bc7d206"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2813316,
"digest": "sha256:cbdbe7a5bc2a134ca8ec91be58565ec07d037386d1f1d8385412d224deafca08"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 8088920,
"digest": "sha256:54335262c2ed2d4155e62b45b187a1394fbb6f39e0a4a171ab8ce0c93789e6b0"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 262,
"digest": "sha256:31555b34852eddc7c01f26fa9c0e5e577e36b4e7ccf1b10bec977eb4593a376b"
}
]
}
Q:当我们拿到一个镜像之后,如果用它来启动一个容器呢?
答:容器运行时通过一个叫 OCI runtime filesytem bundle 的标准格式将 OCI 镜像通过工具转换为 bundle 然后
OCI 容器引擎能够识别这个 bundle 来运行容器
。 此处涉及到了 OCI 规范中的另一个规范即运行时规范 runtime-spec;
filesystem bundle 是个目录,用于给 runtime 提供启动容器必备的配置文件和文件系统; 标准的容器 bundle 包含以下内容:
OCI Image
|
download unpack
|
OCI Runtime Filesystem Bundle <---run---> OCI Runtime
流程说明:
Step1.当我们启动一个容器之后我们使用Tree命令来分析Overlay2会发现,容器docker run 启动后较之前的overlay2目录下多了一个 merged 的文件夹并且该文件夹在容器中可见;
╭─root@sg-02 /var/lib/docker
╰─# tree overlay2 -d -L 3
overlay2
├── 259cf6934509a674b1158f0a6c90c60c133fd11189f98945c7c3a524784509ff
│ └── diff
│ ├── bin
|
│ └── var
├── 27f9e9b74a88a269121b4e77330a665d6cca4719cb9a58bfc96a2b88a07af805
│ ├── diff
│ └── work
├── 5f85c914c55220ec2635bce0080d2ad677f739dcfac4fd266b773625e3051844
│ ├── diff
│ │ └── var
│ ├── merged
│ │ ├── bin
│ │ ├── dev
│ │ ├── etc
│ │ ├── home
│ │ ├── lib
│ │ ├── media
│ │ ├── mnt
│ │ ├── proc
│ │ ├── root
│ │ ├── run
│ │ ├── sbin
│ │ ├── srv
│ │ ├── sys
│ │ ├── tmp
│ │ ├── usr
│ │ └── var
│ └── work
│ └── work
├── 5f85c914c55220ec2635bce0080d2ad677f739dcfac4fd266b773625e3051844-init
│ ├── diff
│ │ ├── dev
│ │ └── etc
│ └── work
│ └── work
Step2.Docker 通过 overlayfs 联合挂载的技术将镜像的多层 layer 挂载为一层,这层的内容就是容器里所看到的也就是 merged 文件夹。
$mount -l
overlay on / type overlay (rw,relatime,lowerdir=/opt/docker/overlay2/l/4EPD2X5VF62FH5PZOZHZDKAKGL:/opt/docker/overlay2/l/MYRYBGZRI4I76MJWQHN7VLZXLW:/opt/docker/overlay2/l/5RZOXYR35NSGAWTI36CVUIRW7U:/opt/docker/overlay2/l/LBWRL4ZXGBWOTN5JDCDZVNOY7H:/opt/docker/overlay2/l/526XCHXRJMZXRIHN4YWJH2QLPY:/opt/docker/overlay2/l/XK5IA4BWQ2CIS667J3SXPXGQK5,upperdir=/opt/docker/overlay2/f913d81219134e23eb0827a1c27668494dfaea2f1b5d1d0c70382366eabed629/diff,workdir=/opt/docker/overlay2/f913d81219134e23eb0827a1c27668494dfaea2f1b5d1d0c70382366eabed629/work)
从 docker 官方文档 Use the OverlayFS storage driver
里偷来的一张图片分别介绍各 Dir 的作用
# 这些是覆盖文件系统的只读层。对于docker这些是按顺序组装的图像层。
LowerDir: these are the read-only layers of an overlay filesystem. For docker, these are the image layers assembled in order.
# 这是覆盖文件系统的读写层。对于docker,这相当于容器特定层,该层包含容器所做的更改。
UpperDir: this is the read-write layer of an overlay filesystem. For docker, that is the equivalent of the container specific layer that contains changes made by that container.
# 这是一个需要的目录覆盖,它需要一个空的目录内部使用。
WorkDir: this is a required directory for overlay, it needs an empty directory for internal use.
# Docker在运行容器时有效地将根目录chroot到这个目录中。
MergedDir: this is the result of the overlay filesystem. Docker effectively chroot’s into this directory when running the container.
WeiyiGeek.
参考来源: 木子(https://blog.k8s.li/Exploring-container-image.html)