Docker镜像存储格式分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Docker镜像存储格式分析相关的知识,希望对你有一定的参考价值。

参考技术A 新版本的docker镜像存储其实是很绕的,各种ID和目录定义较多,不是很直观,本文较详细的分析一下镜像本地存储和在registry存储的格式。测试用的docker版本是20.10.9,存储引擎overlay2。

为了方便分析镜像层级结构,我们基于ubuntu加两个文件,使用 docker build -t myubuntu . 创建一个新镜像myubuntu。

镜像元数据存储在 /var/lib/docker/image/overlay2 :

imagedb目录存储的是镜像元数据。

镜像ID是从Image Json文件得到的,即 sha256sum(ImageJson)。

Layer DiffID是没有压缩的对应层的tar文件的sha256sum值。当然,打包和截包文件的适合得保证是可以可重复操作的,不然会导致Layer的DiffID出错(可以使用tar-split保存tar headers)。比如上面例子中ubuntu镜像只有一层,diff_ids列表只有 (history里面的CMD不占磁盘空间) "sha256:350f36b271dee3d47478fbcd72b98fed5bbcc369632f2d115c3cb62d784edaec"。每一层的tar文件我们可以通过 docker save ubuntu -o ubuntu.tar 得到,解压ubuntu.tar后可以得到对应的层级的打包后的layer.tar文件,如下可以验证DiffID的值计算原理。

ChainID用于标识镜像层级栈,它的值由DiffID计算得来。ChainID对应的layer目录是 /var/lib/docker/image/overlay2/layerdb/sha256 ,这下面的目录就是ChainID,其中内容存储了镜像的层级栈关系。比如这一层的parent是什么,以及对应的镜像数据存储目录的cache_id。本身layerdb只是存储layer的元数据信息,并不存储实际镜像数据。

即最底层的ChainID跟DiffID一样,而其他层的ChainID则是通过计算从最底层到这层的Digest得到。

查看Image Json文件可以看到myubuntu相比ubuntu的diff_ids加了两层,imagedb目录下面也多了两个镜像元数据信息文件,对应新增加的两层镜像。

除了之前的350f...对应ubuntu,其中7c7e是one.txt那层,而a58f则是two.txt那层。

我们可以看下ChainID目录下的内容,可以看到除了ubuntu的基础层,其他层都有一个文件parent,值就是父层的diff_id,diff是diff_id值,cache-id则是镜像实际存储目录,位于 /var/lib/docker/overlay2/cache-id ,size是这一层实际增加文件的大小,tar-split.json.gz是打包这层镜像的配置(参考 https://github.com/vbatts/tar-split )。

CacheID是一个uuid值,每次都不一样。它对应的目录 /var/lib/docker/overlay2/$CacheID ,该目录存储了镜像每层文件和下一层的链接等。验证一下:

其中diff目录下面便是这一层的文件。link是对应的diff目录的短链接,lower则是下一层diff目录的短链接。

在layerdb目录的size文件可以看到每一层的镜像大小,但是加起来跟 docker images 显示的大小会有点差距,比如myubuntu镜像每一层计算加起来是 4 + 65593591 + 4 = 65593599 / 1024 / 1024 = 62.55MiB ,实际显示是 65.6MB,这是因为 docker images 里面计算是按 65593599/1000/1000=65.59 计算的。

另外,因为镜像每层记录的是相对前一层的文件变化,即便删除了文件和软件包,新镜像大小也不会变小。除非使用 docker export 重新导出一个新镜像。

这个目录存储的是layer diffid和digest的关系,其中digest是镜像仓库里面的目录ID。

其中 v2metadata-by-diff存储了layer diffid对应在镜像仓库的信息,包括digest,sourcerepository等。

diffid-by-digest则是反过来的,文件名是仓库里面的layer digest,内容是layer diffid。因为我们新加的镜像myubuntu并没有push到仓库,所以这个目录下面没有信息。

我们创建一个本地的registry,然后对myubuntu另外打个tag, docker tag myubuntu 127.0.0.1:5000/myubuntu ,则respositories.json会多一条新的记录。此时,distribution目录还没有变化。

当我们执行 docker push 127.0.0.1:5000/myubuntu ,则会发现distribution的两个目录分别多了两条记录,对应的是myubuntu的 7c7e... 和 a58f... 两个diffid。此时repository.json里面 127.0.0.1:5000/myubuntu 会多一条带digest的记录,这就是镜像仓库里面对应的digest。其中push时显示的 digest:sha256:bb3c... 对应的是registry的manifest文件的digest,下一节分析。

registry中存储的镜像文件是经过gzip压缩,比如myubuntu本地大小是65.6MB,推到registry压缩后大小约27MB,压缩比还是不错的。registry存储目录如下,主要分为blobs和repositories两个目录。

repositories的一级子目录是镜像名,这里是myubuntu。下面对应三个子目录 _manifests,_layers, _uploads。

blobs存储的内容除了各镜像layer的压缩后的文件,还包括Manifest Json和Image Json文件(未压缩)。

选一个layer的压缩文件data解压看一下内容:

另外需要注意的是, docker pull 时前面显示的值是对应层在registry的压缩文件的digest值,并不是layer diffid,size也是registry存储的压缩文件大小。

03-Docker存储引擎

参考技术A

目前docker的默认存储引擎为overlay2,不同的存储引擎需要相应的文件系统支持,如需要磁盘分区的时候传递d-type稳健分层功能,即需要传递内核参数并开启格式化磁盘的时候指定的功能

Docker 存储引擎的核心思想是“层”的概念,理解了这个层,就基本可以理解它的设计思路。当我们拉取一个 Docker 镜像的时候,可以看到如下:

一个镜像被分成许多的“层”,每“层”包含了若干的文件,而一层层堆叠起来就组成了我们的一个完整的镜像。我们镜像中的文件就是所有“层”文件的并集。 我们构建 Docker 镜像一般采用 Dockerfile 的方式,而 Dockerfile 的每行命令,其实就会生成一个“层”,即使什么文件都没有添加。

文件的创建是在读写层增加文件,那修改和删除呢?

这就要提一下 Docker 设计的 copy-on-write (CoW) 策略。

当我们试图读取一个文件时,Docker 会从上到下一层层去找这个文件,找到的第一个就是我们的文件。所以下面层相同的文件就被“覆盖”了。而修改就是当我们找到这个文件时,将它“复制”到读写层并修改,这样读写层的文件就是我们修改后的文件,并且“覆盖”了镜像中的文件了。而删除就是创建了一个特殊的 whiteout 文件,这个 whiteout 文件覆盖的文件即表示删除了。

这样的设计有什么好处吗?

第一个好处是减少了存储空间,由于镜像被分成了多个层,而各个层是静态只读的,是可以共享的。当你从一个镜像构建另一个镜像时,只需要添加新的层,原有的层不会被复制。

我们可以用 docker history 命令查看我们创建的镜像,相同的层将共享且只保存一份。

我们可以在系统的 /var/lib/docker/<存储驱动>/ 下看到我们所有的层。
第二个好处是启动容器就变得非常轻量和快速。因为我们的容器只是添加了一个“空”的读写层,其他的都是复用的只读层,需要用时才会去搜索。

Docker 的存储引擎针对不同的文件系统,是由不同的存储驱动。

Docker 主要有一下几类存储驱动:

有条件的情况下,我们还是建议选择 overlay2 的存储驱动。

Linux 系统正常运行, 通常需要两个文件系统:

OverlayFS 是从 aufs 之上改进和简化而来的,比 aufs 和 devicemapper 有更好的性能,大部分情况下也比 btrfs 好。
OverlayFS 结构分为三个层: LowerDir 、 Upperdir 、 MergedDir

LowerDir、Upperdir、MergedDir 关系图:

特性:

获取镜像存储路径

Lower层
  LowerDir 层的存储是不允许创建文件, 此时的LowerDir实际上是其他的镜像的UpperDir层,也就是说在构建镜像的时候, 如果发现构建的内容相同, 那么不会重复的构建目录,而是使用其他镜像的Upper 层来作为本镜像的Lower
Merged层
  属于对外展示层,只能在运行中的容器查看,镜像是查看不了的

1)查看init层地址指向
  容器在启动的过程中, Lower 会自动挂载init的一些文件

2) init层主要内容是什么?
  init层是以一个uuid+-init结尾表示,放在只读层(Lowerdir)和读写层(Upperdir)之间,
  作用只是存放/etc/hosts、/etc/resolv.conf 等文件。
3) 为什么需要init层?
  (1) 容器在启动以后, 默认情况下lower层是不能够修改内容的, 但是用户有需求需要修改主机名与域名地址, 那么就需要添加init层中的文件(hostname, resolv.conf), 用于解决此类问题.
  (2) 修改的内容只对当前的容器生效, 而在docker commit提交为镜像时候,并不会将init层提交。
  (3) init 文件存放的目录为/var/lib/docker/overlay2/<init_id>/diff
4) 查看init层文件
  hostname与resolv.conf 全部为空文件, 在系统启动以后由系统写入。

配置 Docker 存储驱动非常简单,只需要修改配置文件即可。

方法1

方法2

以上是关于Docker镜像存储格式分析的主要内容,如果未能解决你的问题,请参考以下文章

好奇宝宝看 Docker 底层原理(中)

CentOS7修改Docker容器和镜像默认存储位置

03-Docker存储引擎

Docker镜像管理

Linux修改docker镜像和容器的默认存储位置

使用Nexus3构建Docker私有镜像仓库