学习
实践
活动
工具
TVP
写文章

Docker挂了,数据如何找回

背景

docker 在实际使用中,让运维人员诟病的,除了安全问题外,大概就是数据的问题了

很多人在初用 docker 的时候,很多时候都忘记或不知道 docker 中需要保留的数据需要挂载到宿主机文件夹到容器内部对应目录(当然除了挂载宿主机目录,还有其他解决方案,我们后面会有文章介绍)

当容器因为某些原因挂掉、无法重新启动的时候,他们就认为数据丢失了,找不回了,这也是很多人对 docker 的一个认识误区,网上没有一篇文章说 docker 数据的问题,今天详细解释下,docker 数据在哪里

docker 文件系统

首先说一下这边以 docker-ce 19.03.4 版本举例,GraphDriver 是 overlay2

这里说明下,overlay 技术是一种虚拟网络技术,这里说的 overlay 是 overlayfs,是一种联合文件系统

我们通过 docker info 简单查看 Storage Driver

也可以通过 docker inspect 查看镜像或容器的详细信息

在 GraphDriver 部分可以看到使用的是什么文件系统,之前旧内核的系统中的 docker 的 GraphDriver 是使用 DeviceMapper,由于 overlay2 性能比 devicemapper 好,而且新版本 docker-ce 默认采用,所以这里只研究 overlay2

从上面的截图可以看到 GraphDriver 的 data 部分有四部分,分别是 LowerDir、MergerDir、UpperDir、WorkDir,解释这几个概念之前,先来回顾下 docker 的镜像的分层原理

docker 镜像是一种轻量可执行的独立软件包, 用来打包软件运行的环境和基于运行环境开发的软件 ,它包含运行某个软件所需要的所有内容,包括代码、运行时的库、环境变量和配置文件

docker 的镜像实际上由一层一层的文件系统组成,这种层级的文件系统称为联合文件系统(unionFS)

UnionFS(联合文件系统):union 文件系统(unionFS)是一种分层、轻量级并且高性能的文件系统,它支持 对文件系统的修改作为一次提交来一层层的叠加 ,同时可以将不同的目录挂载到同一个虚拟文件系统下。union 文件系统是 docker 镜像的基础。镜像可以通过分层来进行继承。基于基础镜像(没有父镜像),可以制作各种的应用镜像

这种分层最大的好处就是资源共享

很多时候,你 pull 镜像的时候,如果是来源于相同的 base 镜像,你可以看到,pull 的时候,底下的层是已经 pulled,而且 base64 加密 id 是一样的,这就是因为 base 镜像资源已经下载,可以共享使用,不需要重复下载

在容器中,伴随联合文件系统的一个技术就是写时复制(CoW),该技术是 linux 内核的一个技术,为了避免不必要的进程间复制操作,在父进程 fork 子进程后,父子进程共享同一副本,当子进程需要调用 exec 写入的时候,数据才会被复制,从而父子进程各自有自己的副本

结合容器技术来看,当处于镜像态的时候,所有的层级都是只读的,但是当 docker run 启动为容器态的时候,在基础镜像上添加了一层可读写层,这时肯定会在最上层,可读写层进行文件写入,就需要将要写入的文件从它存在的层复制到可读写层,然后进行读写,并隐藏只读层的旧文件,这个就是利用了写时复制技术

回顾了这些基础之后,接着看下 overlay2 的基础概念及原理

overlayfs 在 linux 主机上只有两层,一个目录在下层,用来保存镜像(docker),另外一个目录在上层,用来存储容器信息。在 overlayfs 中,底层的目录叫做 lowerdir,顶层的目录称之为 upperdir,对外提供统一的文件系统为 merged。当需要修改一个文件时,使用 CoW 将文件从只读的 Lower 复制到可写的 Upper 进行修改,这个复制出来的临时目录就是 Workdir,结果也保存在 Upper 层

以上就是 GraphDriver 的 data 部分的四部分,可以从前面的图中看到 lowerdir,包含多个层,因为它就是 rootfs,容器镜像,也就是我们 pull 镜像的时候看到的层级

实例解析

overlay2 存储在/var/lib/docker/overlay2 目录中,如果你只有少数镜像,比较好查看,多个镜像的时候,就只能通过 docker inspect 的方式查找对应镜像的层级 id,然后通过这个 id 去 overlay2 目录去找对应的 overlayfs 目录

在该目录下有个“l”目录,这个目录是存放所有 overlayfs 目录的短名称的,通过软连接的方式与 overlyafs 目录下的所有目录链接,这个是为了在 mount 挂载的时候避免参数太长,达到页面大小限制

接着我们分别看下 LowerDir、MergerDir、UpperDir、WorkDir

我这里通过个 redis 容器来说明,首先是 lowerdir,lowerdir 因为有多个,你可以一个一个进去看一下,就能够看明白它每个层级存储的东西,如下:

"LowerDir": "/var/lib/docker/overlay2/3d56c8ea55a05f092442c3cdf01c49556a71b8f089a5772413ef566ec68bca31-init/diff

:/var/lib/docker/overlay2/7e6a11d051174f5a71ce038268e6fa8a310d046032ed435b10ed7846f9eb2d92/diff

:/var/lib/docker/overlay2/bbcd520d1d0d62323bcac4a66328164b99fa1704974ceffc412d348c6f7b55e9/diff

/var/lib/docker/overlay2/dee456318494848b5ea4182ddb8f67f25969b121580afa9ff8aa73cf9c0d256e/diff

:/var/lib/docker/overlay2/a45bb32d7cd7d2386d12826e4d8e524b410616d06cf1ec29f2eab03ba3eb80b2/diff

:/var/lib/docker/overlay2/085f1022334a3727171e3d038ed59f69cb0c6199b93a84afeb1e0b1b8674abf1/diff"

从上面可以看出来,diff 就是存储该层 rootfs 的目录,而每个层级里面的 link 文件里面存储的就是 l 文件夹下的短名称的超链接,除了最底层,上层都有一个 lower 文件,该文件里面就存储了它上层的短名称

而 work 目录,用来联合挂载指定的工作目录

接着看 MergedDir、UpperDir、WorkDir 其实是指向一个层级 id,只是目录不同,是不是觉得这个 id 这么熟悉,其实在 LowerDir 的最上层的 id 就是这个,只不过它最后面有个 init,这个 init 稍后介绍,先来看下这三个目录

mergedir 对外提供统一的视图,这里可以看到整合了所有 lowerdir 层级的文件

因为是新启动的容器,upperdir 目录没有内容,workdir 目录因为写时复制很快,所以通常也无法看到,后面进入容器写入文件进行会在 upperdir 目录看到内容

关于 init 层

init 层是以一个 uuid+-init 结尾表示,夹在只读层和读写层之间,作用是专门存放/etc/hosts、/etc/resolv.conf 等信息,需要这一层的原因是当容器启动时候,这些本该属于 image 层的文件或目录,比如 hostname,用户需要修改,但是 image 层又不允许修改,所以启动时候通过单独挂载一层 init 层,通过修改 init 层中的文件达到修改这些文件目的。而这些修改往往只在当前容器生效,而在 docker commit 提交为镜像时候,并不会将 init 层提交。该层文件存放的目录为/var/lib/docker/overlay2/<init_id>/diff

从上面这部分可以看到,所有容器或者镜像的层级目录都存在 overlay2 目录下,那么一个容器或者镜像是怎么把这些整合起来的?答案是元数据关联,元数据分为 image 元数据和 layer 元数据

镜像元数据存储在了/var/lib/docker/image/<storage_driver>/imagedb/content/sha256/目录下,名称是以镜像 ID 命名的文件,镜像 ID 可通过 docker images 查看,这些文件以 json 的形式保存了该镜像的 rootfs 信息、镜像创建时间、构建历史信息、所用容器、包括启动的 Entrypoint 和 CMD 等等,比如刚才的 redis 镜像

打开该文件,是一个 json 格式文件,但是没有 vim 默认没有格式化,通过:%!python -m json.tool 工具转换成 json 格式查看

其中 rootfs 部分

可以看到对应前面的 6 层 overlayfs,其排列也是有顺序的,从上到下依次表示镜像层的最低层到最顶层

diff_id 如何关联进行层?具体说来,docker 利用 rootfs 中的每个 diff_id 和历史信息计算出与之对应的内容寻址的索引(chainID) ,而 chaiID 则关联了 layer 层,进而关联到每一个镜像层的镜像文件

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>/ 文件夹下

每个 chainID 目录下会存在三个文件 cache-id、diff、zize:

cache-id 文件:

docker 随机生成的 uuid,内容是保存镜像层的目录索引,也就是/var/lib/docker/overlay2/中的目录,这就是为什么通过 chainID 能找到对应的 layer 目录

如图对应的 overlay 目录为/var/lib/docker/overlay2/e701317468246c6188f1bff4f9b9c159648d86108bb02e0ef5f224fd49efd1f0

diff 文件:

保存了镜像元数据中的 diff_id(与元数据中的 diff_ids 中的 uuid 对应)

size 文件:

保存了镜像层的大小

在 layer 的所有属性中,diffID 采用 SHA256 算法,基于镜像层文件包的内容计算得到。而 chainID 是基于内容存储的索引,它是根据当前层与所有祖先镜像层 diffID 计算出来的,具体算如下:

  • 如果该镜像层是最底层(没有父镜像层),该层的 diffID 便是 chainID。
  • 该镜像层的 chainID 计算公式为 chainID(n)=SHA256(chain(n-1) diffID(n)),也就是根据父镜像层的 chainID 加上一个空格和当前层的 diffID,再计算 SHA256 校验码。 

综合上述一个完整的容器层如下图:

回到开头,启动后的容器数据存在哪里?

可以肯定的是在可读写层,结合 overlayfs 原理看,就是在 upperdir,也就是可读写层中的 diff 目录,比如我们进入容器,在 home 目录下写入个测试文件,然后查看 diff 目录

在 mergedir 目录下同样也有该文件

从实际中看,并不是所有数据都在这个目录,当启动容器的时候通过挂载本地目录的形式映射容器内部目录的时候,数据不再存储在 overlayfs,而是直接存储在本地映射的目录

另外一种情况是,当使用 dockerfile 指定 workdir 的情况下,启动容器会自动挂载一个 volume 目录到 workdir 目录

那么这个时候,存在 workdir 目录下的数据会存在自动映射的 Source 目录下

总结如下

只要不删除容器,数据完全可以找回

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/1b8f07a44c66f90c61d23438c
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码关注腾讯云开发者

领取腾讯云代金券