Docker 存储引擎
# 一、前言
本文主要探究docker存储引擎使用,并会对镜像及容器在宿主机底层文件系统中的存储进行细说。默认情况下,当我们安装好docker服务器之后,会在主机创建/var/lib/docker
目录,该目录下保存容器相关信息: 网络、容器数据、卷、构建文件等。
- Docker存储引擎
- Docker根目录结构
# 二、Docker 镜像的存储结构
注意
docker镜像采用分层设计,每一层都成为"layer",这些layer被放在了/var/lib/docker/<storage-driver>/
下,这里的storage-driver可以有很多种如:AUFS、OverlayFS、VFS、Brtfs等。本文镜像存储位置 : /var/lib/docker/overlay2
# 2.1 镜像存储内容
我们以nginx
镜像为例,对镜像进行探究
# 可以看到该镜像共有6层
[root@localhost overlay2]# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
bb263680fed1: Pull complete
258f176fd226: Pull complete
a0bc35e70773: Pull complete
077b9569ff86: Pull complete
3082a16f3b61: Pull complete
7e9b29976cce: Pull complete
Digest: sha256:6650513efd1d27c1f8a5351cbd33edf85cc7e0d9d0fcb4ffb23d8fa89b601ba8
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
2
3
4
5
6
7
8
9
10
11
12
13
对于一个镜像,pull到本地之后,镜像的每一层都会存储在/var/lib/docker/overlay2/目录下,文件名为随机生成,其实就是下面将要介绍的cache-id
[root@localhost ~]# ll /var/lib/docker/overlay2/
total 0
drwx--x---. 4 root root 72 Feb 17 17:53 17574149b77113b94a939436f238cb76f8a3e1f9f2ca54e74049191991c89672
drwx--x---. 4 root root 72 Feb 17 17:53 74b2cdf01468ffa270f73c6e1678f8b263f61c0e64f0dad349ca503a9bc91de4
drwx--x---. 4 root root 55 Feb 17 17:53 ab1bd2cb0dadb413e726246c0d87737db3c68cfd563ff221512decbd9e102632
drwx--x---. 4 root root 72 Feb 17 17:53 b3ae534bd7e47e38cdc36ea977e292d5a85a6dd243941b4beb94b9477e6e558d
brw-------. 1 root root 253, 0 Feb 14 10:18 backingFsBlockDev
drwx--x---. 4 root root 72 Feb 17 17:53 d54a73b229b6f509b26aee4bad3f39a8ea88cf96237bbafdc00f9911ea0265e2
drwx--x---. 3 root root 47 Feb 17 17:53 fbd6c79cba6c92bab616888278f0aca3b910b9f5b5ef18b8f11c7f90ea40e84f
drwx------. 2 root root 210 Feb 17 18:03 l
2
3
4
5
6
7
8
9
10
- l : 所有层的软连接,短链接使用短名称,避免mount时候参数达到页面大小限制。
- 该目录下的目录是镜像的每一层镜像信息,而不是某一个镜像的。
探究镜像每一层
说明
- 处于底层的镜像目录包含了一个diff和一个link文件,diff目录存放了当前层的镜像内容,而link文件则是与之对应的短名称
- 其它层的镜像还多了work目录和lower文件,lower文件用于记录父层的短名称,work目录用于联合挂载指定的工作目录。
这些目录和镜像的关系是怎么组织在的一起呢?答案是通过元数据关联。元数据分为image元数据和layer元数据。
# 2.2 image元数据
# Docker image信息存储目录
[root@localhost ~]# tree /var/lib/docker/image/overlay2/ -L 1
/var/lib/docker/image/overlay2/
├── distribution # Image的所有Layer diffid和digest映射关系
├── imagedb # 本地所有Image的元数据信息
├── layerdb # 本地所有Image的所有Layer的索引结构
└── repositories.json # 本地所有repo的所有版本Image的信息
2
3
4
5
6
7
说明
镜像元数据存储在了/var/lib/docker/image/overlay2/imagedb/content/sha256/
,名称是以镜像ID命名的文件,镜像ID可通过docker images查看,这些文件以json的形式保存了该镜像的rootfs信息、镜像创建时间、构建历史信息、所用容器、包括启动的Entrypoint和CMD等等,通过格式化查看文件(vim :%!python -m json.tool
),可以看到最下面diff_ids字段,对应的的是一个镜像层,其排列也是有顺序的,从上到下依次表示镜像层的最低层到最顶层。
其实镜像ID就是通过该image的元数据配置文件信息通过sha256的结果命名的,例如:
[root@localhost ~]# sha256sum /var/lib/docker/image/overlay2/imagedb/content/sha256/3f8a00f137a0d2c8a2163a09901e28e2471999fde4efc2f9570b91f1c30acf94
3f8a00f137a0d2c8a2163a09901e28e2471999fde4efc2f9570b91f1c30acf94 /var/lib/docker/image/overlay2/imagedb/content/sha256/3f8a00f137a0d2c8a2163a09901e28e2471999fde4efc2f9570b91f1c30acf94
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 3f8a00f137a0 10 days ago 142MB
2
3
4
5
6
# 2.3 Image Layer元数据
说明
layer 对应镜像层的概念,在 docker 1.10 版本以前,镜像通过一个 graph 结构管理,每一个镜像层都拥有元数据,记录了该层的构建信息以及父镜像层 ID,而最上面的镜像层会多记录一些信息作为整个镜像的元数据。graph 则根据镜像 ID(即最上层的镜像层 ID) 和每个镜像层记录的父镜像层 ID 维护了一个树状的镜像层结构。
在 docker 1.10 版本后,镜像元数据管理巨大的改变之一就是简化了镜像层的元数据,镜像层只包含一个具体的镜像层文件包。用户在 docker 宿主机上下载了某个镜像层之后,docker 会在宿主机上基于镜像层文件包和 image 元数据构建本地的 layer 元数据,包括 diff、parent、size 等。而当 docker 将在宿主机上产生的新的镜像层上传到 registry 时,与新镜像层相关的宿主机上的元数据也不会与镜像层一块打包上传。
Docker 中定义了 Layer
只读层和 RWLayer
读写层 两种接口,分别用来定义只读层和可读写层的一些操作,又定义了 roLayer 和 mountedLayer,分别实现了上述两种接口。其中,roLayer 用于描述不可改变的镜像层,mountedLayer 用于描述可读写的容器层。具体来说,roLayer 存储的内容主要有索引该镜像层的 chainID、该镜像层的校验码 diffID、父镜像层 parent、storage_driver 存储当前镜像层文件的 cacheID、该镜像层的 size 等内容。这些元数据被保存在/var/lib/docker/image/<storage_driver>/layerdb/sha256/<chainID>/
文件夹下。
# Docker image 层级管理信息目录结构
[root@localhost ~]# tree /var/lib/docker/image/overlay2/layerdb/
/var/lib/docker/image/overlay2/layerdb/
├── mounts
├── sha256
└── tmp
# 元数据 每个chainID目录下都会有三个文件 cache-id、diff、zize
[root@localhost ~]# ls -l /var/lib/docker/image/overlay2/layerdb/sha256/
total 0
drwx------. 2 root root 85 Feb 17 17:53 1486739bc51436dd10d2bc1d45e130771c73d3aee35e49971905aa767d195342
drwx------. 2 root root 85 Feb 17 17:53 452008e5f3c114989bfc978a2829cf061f0868463f3553b4e20c964a41eda749
drwx------. 2 root root 71 Feb 17 17:53 4695cdfb426a05673a100e69d2fe9810d9ab2b3dd88ead97c6a3627246d83815
drwx------. 2 root root 85 Feb 17 17:53 ccfe545858415bccd69b8edff4da7344d782985f22ad4398bdaa7358d3388d15
drwx------. 2 root root 85 Feb 17 17:53 cf7515030d4de4fb66994e0d9fccbaf19fcfbf46f7dad8cf895051750b840128
drwx------. 2 root root 85 Feb 17 17:53 e34f63c02e162795cc8a2b43d1a3ff0ccd6d3456ce12aebb74452e252d1ecb8a
#
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cache-id:
docker随机生成的uuid,内容是保存镜像层的目录索引,也就是/var/lib/docker/overlay2/中的目录,这就是为什么通过chainID能找到对应的layer目录。以chainID为
1486739bc51436dd10d2bc1d45e130771c73d3aee35e49971905aa767d195342
对应的cache-id
文件内容为d54a73b229b6f509b26aee4bad3f39a8ea88cf96237bbafdc00f9911ea0265e2
,也就保存在/var/lib/docker/overlay2/d54a73b229b6f509b26aee4bad3f39a8ea88cf96237bbafdc00f9911ea0265e2
注意
cache-id是宿主机创建或者拉取Imag的时候随机生成的,同一个Image被Push然后Pull后,该值是会变的。
- diff:
保存了镜像元数据中的diff_id
(与元数据中的diff_ids中的uuid对应),该值是在Image创建后就固定不变的。
通过/var/lib/docker/image/overlay2/imagedb/content/sha256/3f8a00f137a0d2c8a2163a09901e28e2471999fde4efc2f9570b91f1c30acf94
文件,vim :%!python -m json.tool
查看。
- ChainID:
关于ChainID计算,这里分为两种情况:
- 如果该Layer是最底层Layer,那么该层:
ChainID=DiffID
。
- 否则
ChianID=SHA256(parent Layer ChainID + " " + DiffID)
总结
ImageID是通过Image的元数据配置信息文件经过SHA26得到的Hash值;image元数据,文件名以镜像ID命名,文件内容中roofs.diff_ids中的内容与Image Layer的元数据目录下diff文件内容一一对应,用于生成ChainID;Image Layer的元数据,文件以ChainID命名,目录下cache-id内容与宿主机/var/lib/docker/overlay2目录一一对应,通过该cache-id找到本层映射到宿主机上的存储目录。
# 三、Docker 容器的存储结构
上面我们针对一个镜像pull或者本地构建后,各层文件存储相关的探究,接下来我们对Docker基于镜像运行后,容器的文件存储结构进行探究。
# 3.1 运行镜像
[root@localhost ~]# docker run -d --name nginx nginx:latest
23fd9b4987a85160d44ca7ee4dc9d2e89139ad52723f68bba15c68cc7e0ffb7f
2
# 3.2 查看目录
当我们运行上面Nginx镜像之后,可以再次查看layerdb/mounts
目录下会多出以ContainerID
为目录的layer元数据管理层目录。
# 容器层元数据目录
[root@localhost ~]# ls -l /var/lib/docker/image/overlay2/layerdb/mounts/23fd9b4987a85160d44ca7ee4dc9d2e89139ad52723f68bba15c68cc7e0ffb7f/
total 12
-rw-r--r--. 1 root root 69 Feb 20 16:37 init-id
-rw-r--r--. 1 root root 64 Feb 20 16:37 mount-id
-rw-r--r--. 1 root root 71 Feb 20 16:37 parent
# 容器层数据存储目录
[root@localhost ~]# ls /var/lib/docker/overlay2/bdbd8a7cb2ab6cf250c22d7abf8d57508cbc7f6f66e842f27a5b30fa63afc78e -l
total 8
drwxr-xr-x. 7 root root 62 Feb 20 16:37 diff
-rw-r--r--. 1 root root 26 Feb 20 16:37 link
-rw-r--r--. 1 root root 202 Feb 20 16:37 lower
drwx------. 2 root root 6 Feb 20 16:37 merged
drwx------. 3 root root 18 Feb 20 16:37 work
2
3
4
5
6
7
8
9
10
11
12
13
14
diff: 目录为容器的读写层,容器内修改的文件都会在 diff 中出现。
merged:目录为分层文件联合挂载后的结果,也是容器内的工作目录。
init-id: 是由
mountID+"-init"
拼接而成,同样会索引到Container的init Layer的内容;该init Layer本身也是mountedLayer,最终Container的读写层是基于该init Layer+Image roLayer堆叠而成的。mount-id: 随机生成,记录该容器层的cache-id,用来索引Container的读写层Layer内容存储目录。
parent: 指向Image最顶层的roLayer的ChainID。
init-id、mount-id,其实就是索引
overlay2目录下该Layer存储目录中的两个文件
。
# 3.3 容器层读写文件
这里我们运行容器之后,在容器中创建一个文件,然后观察容器层存储变化。
[root@localhost ~]# docker exec -it nginx /bin/bash
root@23fd9b4987a8:/# echo 123 >/tmp/1.txt
root@23fd9b4987a8:/# cat /tmp/1.txt
123
2
3
4
# 3.4 总结
通过以上分析,对于一个完整的容器,我们可以分为如下的read-only,init 和readable/writeable三种:
- read-only: 指的就是容器的rootfs镜像层。
- init : Docker 项目单独生成的一个内部层,专门用来存放
/etc/hosts、/etc/resolv.conf
等信息,作用是专门存放/etc/hosts、/etc/resolv.conf等信息,需要这一层的原因是当容器启动时候,这些本该属于image层的文件或目录,比如hostname,用户需要修改,但是image层又不允许修改,所以启动时候通过单独挂载一层init层,通过修改init层中的文件达到修改这些文件目的。而这些修改往往只读当前容器生效,而在docker commit提交为镜像时候,并不会将init层提交。该层文件存放的目录为/var/lib/docker/overlay2/<init_id>/diff 。 - readable/writeable: 容器层,在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。
# 四、Docker容器分层文件系统
上面我们了解了镜像及容器的存储结果,我们知道,对于一台宿主机往往不仅运行一个容器,随着运行的容器越来越多,/var/lib/docker/overlay2
目录下的层也就会越多,如果我们想要定位运行容器的占用宿主机磁盘情况,那么就需要我们找到对应的关系。
# 4.1 查看目前容器运行
目前运行
artalk
、rabbitmq
两个容器
# 4.2 容器存储目录
可以看到仅仅是运行2个容器,就有几十个层目录
# 4.3 查找关联容器
我们随便以一个层id,查找关联的容器
855b9e0cac98ce02b8bce8908126186a73c596bcd2f6ddc2031e795b13adfbb0
docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Name}}, {{.GraphDriver.Data}}' | grep "855b9e0cac98ce02b8bce8908126186a73c596bcd2f6ddc2031e795b13adfbb0"
说明
docker inspect --format
命令可以指定格式获取Docker容器的详细信息:
.State.Pid
:提取了容器的进程标识符(PID)。.Name
:获取容器名称,Docker 容器的名称可能包括斜杠(/),例如/rabbitmq
。.GraphDriver.Data
: 获取容器使用的存储驱动的相关数据,对于该参数,我们还可以更加详细的存储数据:.GraphDriver.Data.WorkDir
:这是容器内部进程的默认工作目录,它决定了容器内部进程在启动时所在的目录位置。.GraphDriver.Data.LowerDir
: 这是底层的只读文件系统层,通常包含基础操作系统的文件和目录。多个容器可以共享相同的lowerdir
。.GraphDriver.Data.UpperDir
:这是容器的可写文件系统层,包含了容器内部的变化。每个容器都会有自己的唯一的upperdir
。.GraphDriver.Data.MergedDir
: 这是OverlayFS
的联合视图,它将UpperDir
和LowerDir
层叠加在一起,创建了一个统一的文件系统视图,供容器内的进程使用。
有时候我们在/var/lib/docker/overlay2
目录下找到了子目录,但实际容器已经不存在,这可能是因为:
容器已经被删除: 如果你删除了一个容器,Docker 可能会在一段时间后清理该容器的存储层(overlay2 子目录)。这是为了释放磁盘空间并维护系统的整洁性。所以,在子目录存在但容器不存在的情况下,很可能是该容器已被删除。
容器没有正常停止: 如果一个容器没有正常停止(比如通过
docker stop
命令),而是强制终止或崩溃,Docker 可能会保留其存储层,以便在需要时进行调查和分析。这可能会导致在/var/lib/docker/overlay2
目录下看到未运行的容器的子目录。Docker 存储层问题: 有时,Docker 存储层的问题可能导致容器的子目录留下。这可能是由于未处理的 Docker 异常或存储层的不一致性。
文件系统损坏: 在某些情况下,文件系统损坏可能会导致残留的容器子目录。这通常是一个不太常见的情况,但也需要考虑。
# 4.4 使用场景
我们通常会使用此方法,查找占用宿主机磁盘较大的容器:
# 统计存储目录大小
cd /var/lib/docker/overlay2
du -h --max-depth=1
# 查找关联容器
docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Name}}, {{.GraphDriver.Data.WorkDir}}' | grep "${上一步统计的子目录文件夹名称}"
2
3
4
5