[TOC]
0x00 镜像如何炼成 在深入学习镜像之前我们需要知道镜像是如何(炼制/搓)
成的(等同于构建镜像),当然是通过我们DockerFile一条条指令为镜像生成每一层,按照执行顺序镜像文件系统复写封装从下到上;
1.OCI 标准协议 关于容器镜像的OCI标准协议,那什么又是OCI标准协议?
答: Open Container Initiative(打开集装箱倡议)旨在围绕容器格式和运行时制定一个开放的工业化标准;
参考地址:容器开放接口规范(CRI OCI)
Docker 公司与 CoreOS 和 Google 共同创建了 OCI (Open Container Initial),并提供了三种规范
关于 OCI 规范的作用说明:
1.制定容器格式标准的宗旨就提高镜像通用性以便不限于某种特定操作系统、硬件、CPU架构、公有云
等; 概括来说就是不受上层结构的绑定,如特定的客户端、编排栈
等
2.两个协议通过 OCI runtime filesytem bundle
的标准格式连接在一起,OCI 镜像可以通过工具转换成bundle然后OCI容器引擎能够识别这个 bundle 来运行容器, 其优点如下;
操作标准化:容器的标准化操作包括使用标准容器创建、启动、停止容器,使用标准文件系统工具复制和创建容器快照,使用标准化网络工具进行下载和上传。
内容无关:内容无关指不管针对的具体容器内容是什么,容器标准操作执行后都能产生同样的效果。如容器可以用同样的方式上传、启动,不管是PHP应用还是MySQL数据库服务。
基础设施无关:无论是个人的笔记本电脑还是AWS S3,亦或是OpenStack,或者其它基础设施,都应该对支持容器的各项操作。
为自动化量身定制:制定容器统一标准,是的操作内容无关化、平台无关化的根本目的之一,就是为了可以使容器操作全平台自动化。
工业级交付:制定容器标准一大目标,就是使软件分发可以达到工业级交付成为现实
参考来源:
1.OCI 镜像规范的主要由以下几个 markdown 文件组成:
[TOC]
0x00 镜像如何炼成 在深入学习镜像之前我们需要知道镜像是如何(炼制/搓)
成的(等同于构建镜像),当然是通过我们DockerFile一条条指令为镜像生成每一层,按照执行顺序镜像文件系统复写封装从下到上;
1.OCI 标准协议 关于容器镜像的OCI标准协议,那什么又是OCI标准协议?
答: Open Container Initiative(打开集装箱倡议)旨在围绕容器格式和运行时制定一个开放的工业化标准;
参考地址:容器开放接口规范(CRI OCI)
Docker 公司与 CoreOS 和 Google 共同创建了 OCI (Open Container Initial),并提供了三种规范
关于 OCI 规范的作用说明:
1.制定容器格式标准的宗旨就提高镜像通用性以便不限于某种特定操作系统、硬件、CPU架构、公有云
等; 概括来说就是不受上层结构的绑定,如特定的客户端、编排栈
等
2.两个协议通过 OCI runtime filesytem bundle
的标准格式连接在一起,OCI 镜像可以通过工具转换成bundle然后OCI容器引擎能够识别这个 bundle 来运行容器, 其优点如下;
操作标准化:容器的标准化操作包括使用标准容器创建、启动、停止容器,使用标准文件系统工具复制和创建容器快照,使用标准化网络工具进行下载和上传。
内容无关:内容无关指不管针对的具体容器内容是什么,容器标准操作执行后都能产生同样的效果。如容器可以用同样的方式上传、启动,不管是PHP应用还是MySQL数据库服务。
基础设施无关:无论是个人的笔记本电脑还是AWS S3,亦或是OpenStack,或者其它基础设施,都应该对支持容器的各项操作。
为自动化量身定制:制定容器统一标准,是的操作内容无关化、平台无关化的根本目的之一,就是为了可以使容器操作全平台自动化。
工业级交付:制定容器标准一大目标,就是使软件分发可以达到工业级交付成为现实
参考来源:
1.OCI 镜像规范的主要由以下几个 markdown 文件组成:
1 2 3 4 5 6 7 8 9 10 11 12 13 ├── annotations.md ├── config.md ├── considerations.md ├── conversion.md ├── descriptor.md ├── image-index.md ├── image-layout.md ├── implementations.md ├── layer.md ├── manifest.md ├── media-types.md ├── README.md ├── spec.md
Image Manifest - a document describing the components that make up a container image | 描述了构成容器的图像的部件的文档
Image Index - an annotated index of image manifests | 图像清单的带注释索引
Image Layout - a filesystem layout representing the contents of an image | 表示图像的内容的文件系统布局
Filesystem Layer - a changeset that describes a container’s filesystem | 描述一个容器的文件系统中的变更
Image Configuration - a document determining layer ordering and configuration of the image suitable for translation into a runtime bundle | 文档确定层的排序和适合翻译的图像的配置到运行时的束
Conversion - a document describing how this translation should occur | 描述应该是如何发生这种转换的文档
Descriptor - a reference that describes the type, metadata and content address of referenced content | 描述的类型,元数据和引用的内容的内容地址的引用
2.OCI 规范是免费的哦,不像大多数 ISO 规范还要交钱才能看(︶^︶)哼。
image-spec - 镜像规范 描述:它决定了我们镜像按照什么标准来构建
,以及构建完镜像之后如何存放
,接着下文提到的 Dockerfile 则决定了镜像的 layer 内容以及镜像的一些元数据信息。
直白的说一个镜像规范 image-spec 和一个 Dockerfile 就指导着我们构建一个镜像;
总结以上几个 markdown 文件 OCI 容器镜像规范主要包括以下几块内容:
Layer : Docker 以 layer (镜像层) 保存的文件系统以及每个Layer保存与上层之间变化部分,以及对保存哪些文件,怎么表示增加、修改和删除的文件等进行描述;
Image-Config : 保存了文件系统的层级信息(每个层级的 hash 值,以及历史信息
)以及容器运行时需要的一些信息(比如环境变量、工作目录、命令参数、mount 列表),指定了镜像在某个特定平台和系统的配置:
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 { "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 文件(图像清单规范)主要有以下三个目标:1 2 3 第一个目标是内容可寻址的图像,通过支持的图像模型,其中所述图像的配置可被散列以生成图像和它的组件的唯一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。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 //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 处理器的机器这些镜像上是跑不起来的。
runtime-spec 运行时规范 在深入学习Docker原理过程中发现Docker info中的Runtimes字段,由于个人阅历有限不得不Google一下所以有了以下内容;1 2 3 4 $docker info | grep -i "runtime" Runtimes: runc Default Runtime: runc
Q: 什么是runtimes?
答:从字面理解Runtimes我们知道其为运行时实际它是Docker容器在运行时的一个周期等; 简单的说就是容器运行时,传统意义上来说就是代表容器从拉取镜像到启动运行再到中止的整个生命周期
容器在运行时分为两类:
1) low-level runtime : 关注如何与操作系统交互,创建并运行容器,使用 namespace 和 cgroup 实现资源隔离和限制,目前常见的 low-level runtime有:
lmctfy – 是Google的一个项目,它是Borg使用的容器运行时
runc – 目前使用最广泛的容器运行时。
rkt – CoreOS开发的Docker/runc的一个流行替代方案,提供了其他 low-level runtimes (如runc)所提供的所有特性。
2) high-level runtime : 指包含了更多上层功能,例如 grpc调用,镜像存储管理等,目前主流的 high-level runtime 有:
其两者的区别是:
High-level runtimes相较于low-level runtimes位于堆栈的上层
low-level runtimes负责实际运行容器,而High-level runtimes负责传输和管理容器镜像、解压镜像,并传递给low-level runtimes来运行容器。
Q: 从上面的表达中我们知道runc是一个low-level 运行时,那它与Docker有何关系?
答: runC是一个根据OCI标准创建并运行容器的命令行工具(CLI tool), runC是docker中最为核心的部分,容器的创建,运行,销毁等等操作最终都将通过调用runc完成。 而runC也有自己的客户端,后来被提取出来作为一个单独的工具和库。其实现了 OCI 规范,包含config.json文件和容器的根文件系统。
distribution-spec 镜像仓库规范 描述:该规范定义着容器镜像如何存储在远端的Registry上,使用规范的好处是可以帮助我们把这些镜像按照约定俗成的格式来存放即方便第三方工具的存取;
目前实现该规范的 registry 就 docker 家的 registry 使用的多一些。其他的 registry 比如 harbor 、 quay.io 使用的也比较多。
2.Dockerfile 描述:相信读者如果对docker有个基础了解以及看了我前面的文章,想必对它不感到陌生吧;众所周知 docker 镜像需要一个 Dockerfile 来构建而成; 当我们对 OCI 镜像规范有了个大致解之后,接下来就拿着 Dockerfile 这个 “图纸” 去一步步构建镜像。
此处不再详细DockerFile的详细书写以及最佳优化技巧,感兴趣的朋友可以参看前面的文章;
下面我们基于docker build test-image
进一步理解目录结构;1 2 3 4 5 6 7 FROM alpineLABEL 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" ]
构建过程输出如下: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 $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.top-docker
3.基础镜像 描述:通过前面的基础学习我们知道Docker 是一个典型的 C/S 架构的应用,分为 Docker 客户端(即平时敲的 docker 命令)
和 Docker 服务端(dockerd 守护进程)
。
主要特征:
1) Docker 客户端通过 REST API 和服务端进行交互,即客户端每发送一条指令,底层都会转化成 REST API 调用的形式发送给服务端,服务端处理客户端发送的请求并给出响应。
2) Docker 镜像的构建、容器创建、容器运行等工作都是 Docker 服务端来完成的,Docker 客户端只是承担发送指令的角色。
3) Docker 客户端和服务端可以在同一个宿主机,也可以在不同的宿主机;
如果在同一个宿主机的话,Docker 客户端默认通过 UNIX 套接字(/var/run/docker.sock)
和服务端通信;
如果不在同一个宿主机的化,Docker客户端则通过TCP通道(tcp://xxx.xx.xx.xx:2375)
进行与服务端通信。
如果说炼制镜像也需要个工厂的话,那么 dockerd 守护进程就是个生产镜像的工厂。
Tips: 镜像工厂不止Docker一家还有Redhat家的Buildah也能生产镜像,两者区别如下:
1.Docker 构建镜像时候需要ROOT权限,而Buildah则可以采用非ROOT权限构建镜像
2.Docker 镜像兼容性相对于Buildah较好;
3.Docker 构建镜像使用占比比Buildah多;
构建镜像docker build
的流程:
1) 在Docker Cli 执行镜像构建命令并且使用-f参数来指定Dockerfile文件,-t 指定构建出的镜像标签信息;
2) Docker Cli 会将构建命令后面指定的路径(.)上下文环境所有文件打包成一个 tar 包,并发送给 Docker 服务端;
3) Docker Deamon 收到客户端发送的 tar 包并解压,根据 Dockerfile 里面的指令进行镜像的分层构建;
4) Docker 下载 FROM 语句中指定的基础镜像,然后将基础镜像的 layer 联合挂载为一层并在上面创建一个空目录;
5) 此时会启动一个临时的容器并在 chroot 中启动一个 bash, 运行 RUN 语句中的命令:RUN: chroot . /bin/bash -c "apt get update……"
;\
6) 在RUN命令执行完毕后,会将当前层目录进行压缩从而形成新镜像中的新的一层, 同时为下一层提供基础镜像;
7) 如果 Dockerfile 中包含其它命令,就以之前构建的层次为基础,从第二步开始重复创建新层,直到完成所有语句后退出;
8) 在 Dockerfile 中包含的所有指令命令执行完毕后镜像构建完成,并为该镜像打上Tag;
Tips: 我们可以采用docker history <imageName:Tag>
命令来逆向推算出 docker build 的过程。
现在有个问题来了基础镜像是如何生成?总不会是凭空捏造的吧!
答: 实际上它也是通过Dockerfile中的指令来构建的, 比如以debian:buster
该基础镜像为例看一下基础镜像是如何炼成的;
Base Image Dockerfile1 2 3 4 5 6 FROM scratchADD rootfs.tar.xz / 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;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ git clone https://github.com/debuerreotype/docker-debian-artifacts Receiving objects: 100% (660/660), 2.88 GiB | 16.63 MiB/s, done . git checkout dist-amd64 cd buster && tar -xvf rootfs.tar.xz -C !$ls rootfs/ bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var 125M rootfs du -sh slim/rootfs 76M slim/rootfs
3) 此时在上述环境中我们非常方便的自己构建debian:buster 基础镜像1 2 3 4 5 6 7 8 9 10 11 12 13 docker build -t debian:buster . Sending build context to Docker daemon 30.12MB Successfully built 04948daa3c2e
0x01 镜像存储原理 本地存储 - Local 描述: Docker 在构建完镜像后会将其存储在Docker 本地存储家目录中,默认情况即/var/lib/docker
, 如果修改了Docker ROOT Dir 目录的可以通过docker info | grep "Docker Root Dir"
命令查看;
对于镜像存储路径最重要的 image 和 overlay2 这个两个目录,容器的元数据存放在 image 目录下,容器的 layer 数据则存放在 overlay2 目录下。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 /var/lib/docker/image /var/lib/docker/image/overlay2/ /var/lib/docker/image/overlay2/repositories.json /var/lib/docker/overlay2 $more /var/lib/docker/overlay2/30a121491fc6ba331b30f25c411fbe62f9e8e4c24fbe372cd046f4ab601f94bd/linkIEBGN75T7KVLDOEHY5COAGEWQ2 tree /var/lib/docker/overlay2/l/ $ls -lha l/ | grep "IEBGN75T7KVLDOEHY5COAGEWQ2" lrwxrwxrwx. 1 root root 72 6月 29 14:41 IEBGN75T7KVLDOEHY5COAGEWQ2 -> ../30a121491fc6ba331b30f25c411fbe62f9e8e4c24fbe372cd046f4ab601f94bd/diff
weiyigeek.top-镜像在本地的存储
镜像仓库 - Registry 描述:镜像仓库主要是用来存储和分发镜像的,并对外提供一套 HTTP API V2。 镜像仓库中的所有镜像都是以数据块 (Blob) 的方式存储
在文件系统中。 支持多种文件系统,主要包括filesystem,S3,Swift,OSS
等。
下面详细介绍一下,镜像的所有数据,是如何存储在镜像仓库的文件系统中的?
为了分析镜像如何存放在Registry上,我们可以在本地docker环境中启动registry容器来实验即可,所以在这种场景下不太合适用Harbor这种重量级的Registry镜像仓库实践;
Step1.Registry 镜像容器启动
1 2 3 4 $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 registry335ea763a2fa4508ebf3ec6f8b11f3b620a11bdcaa0ab43176b781427e0beee6
Step2.上传本地镜像到 Registry 仓库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $docker imagesREPOSITORY TAG IMAGE ID CREATED SIZE go-hello scratch cb05b87d0012 5 days ago 2.07MB go-hello stage 5934753f8f4f 5 days ago 73.9MB $docker tag go-hello:scratch 127.0.0.1:5000/go-hello:scratch$docker push 127.0.0.1:5000/go-hello:scratch$docker tag go-hello:stage 127.0.0.1:5000/go-hello:stage$docker push 127.0.0.1:5000/go-hello:stage
Step3.当我们在本地启动一个 registry 容器之后,容器内默认的存储位置为 /var/lib/registry
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 $ 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名称相同的目录;
Step4.当我们 pull 镜像的时候如果不指定镜像的 tag名默认就是 latest, registry 会从 HTTP 请求中解析到这个tag名,然后根据tag名目录下的 link 文件找到该镜像的 manifest的位置返回给客户端
,客户端接着去请求这个manifest 文件,客户端根据这个 manifest 文件来 pull 相应的镜像 layer 。
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 $cat /var/lib/registry/docker/registry/v2/repositories/go-hello/_manifests/tags/scratch/current/linksha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e $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" , "config" : { "mediaType" : "application/vnd.docker.container.image.v1+json" , "size" : 1472, "digest" : "sha256:cb05b87d001253772ae9a212200de5eb8304ab9691c61589332a2f57e7059209" }, "layers" : [ { "mediaType" : "application/vnd.docker.image.rootfs.diff.tar.gzip" , "size" : 1106793, "digest" : "sha256:5395625ce01dee311e2f7c879b0b148ac7525de7aad5080a518d7f7e5a99d368" } ] }
总结思想:
1.同一镜像在不同Registry镜像仓库中,存储的方式、位置和内容完全一样因为它们的Layer digest在仓库中唯一。
通过 Registry API 获得的两个镜像仓库中相同镜像的 manifest 信息完全相同。
两个镜像仓库中相同镜像的 manifest 信息的存储路径和内容完全相同。
两个镜像仓库中相同镜像的 blob 信息的存储路径和内容完全相同。
2.registry 存储目录里并不会存储与该 registry 相关的信息,比如我们push镜像时给镜像加上 127.0.0.1:5000 前缀; 所以我们在迁移一个大的Registry镜像仓库时候最快捷的方法就是打包(tar -cvf - 不需要加z参数浪费 CPU 时间得不偿失)该registry存储然后将其tar包rsync到其它机器即可;
0x02 镜像搬运 描述: 当我们在本地构建完成一个镜像之后,如何传递给他人呢?此时下面的文章或者技巧能更好帮助您;
这里需要利用GitHub(git)进行做类比,实际上搬运镜像的流程就像在Github上搬运代码一样;1 2 3 4 Github 代码仓库 == Docker registry镜像仓库(Harbor-公共/私有的镜像) git clone |pull == docker pull git push == docker push
pull - 镜像拉取 描述:为了方便理解docker full拉取镜像流程可以参看OCI Registry规范中的文档地址 ,下面是结合大佬的博客简单梳理一下 pull 一个镜像的大致流程:
weiyigeek.top-向大佬借的图
结合上图大致的流程,我将远程的镜像仓库拉取到本地来给容器运行时使用具体流程如下:
Step1.配置Registry镜像仓库的auth认证信息
1 2 3 4 5 6 7 8 9 10 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 文件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 已经下载完成了,但还没有解压完;
1 2 3 4 5 $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,验证镜像是否正确。
push - 推送镜像 描述: 采用 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PUT /v2/<name>/blobs/uploads/<session_id>?digest=<digest> Content-Length: <size of layer> Content-Type: application/octet-stream <Layer Binary Data> 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 已经上传完毕。
1 2 3 4 5 6 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 推送上去就完事儿了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 Docker-dray 描述: 它是一个Python脚本它使用Request库请求Registry API来从镜像仓库中拉去镜像并保存为tar包, 拉取完成后使用Docker load加载到本地docker镜像中;
它与docker pull 以及 Skopeo copy有相似原理,相同的是它们都会去调用Registry API;
Install && Usage:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 wget https://raw.githubusercontent.com/NotGlop/docker-drag/master/docker_pull.py python3 docker_pull.py [image name] $python3 docker_pull.py nginxCreating 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.tarffc9b21953f4: 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 - 镜像搬运神器 描述: 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 inspect1 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 root@deploy:/root { "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" } ] }
0x03 镜像使用 Q:当我们拿到一个镜像之后,如果用它来启动一个容器呢?
答:容器运行时通过一个叫 OCI runtime filesytem bundle 的标准格式将 OCI 镜像通过工具转换为 bundle 然后OCI 容器引擎能够识别这个 bundle 来运行容器
。 此处涉及到了 OCI 规范中的另一个规范即运行时规范 runtime-spec;
filesystem bundle 是个目录,用于给 runtime 提供启动容器必备的配置文件和文件系统; 标准的容器 bundle 包含以下内容:
config.json: 该文件包含了容器运行的配置信息,该文件必须存在 bundle 的根目录,且名字必须为 config.json
容器的根目录可以由 config.json 中的 root.path 指定
1 2 3 4 5 OCI Image | download unpack | OCI Runtime Filesystem Bundle <---run---> OCI Runtime
流程说明:
Step 3.如果想对 overlayfs 文件系统有详细的了解,可以参考 Linux 内核官网上的这篇文档 overlayfs.txt 。
参考来源: 木子(https://blog.k8s.li/Exploring-container-image.html )