Docker镜像与容器存储结构分析
Docker是一个开源的应用容器引擎,主要利用linux内核namespace实现沙盒隔离,用cgroup实现资源限制。
Docker 支持三种不同的镜像层次存储的drivers: aufs、devicemapper、btrfs ;
Aufs:
AUFS (AnotherUnionFS) 是一种 Union FS, 简单来说就是支持将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)的文件系统。 Aufs driver是docker 最早支持的driver,但是aufs只是linux内核的一个补丁集而且不太可以会被合并加入到linux内核中。但是由于aufs是唯一一个 storage driver可以实现容器间共享可执行及可共享的运行库, 所以当你跑成千上百个拥有相同程序代码或者运行库时时候,aufs是个相当不错的选择。
Device Mapper:
Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机制下,用户可以很方便的根据自己的需要制定实现存储资源的管理策略(详见:http://www.ibm.com/developerworks/cn/linux/l-devmapper/index.html) 。
Device mapper driver 会创建一个100G的简单文件包含你的镜像和容器。每一个容器被限制在10G大小的卷内。(如果想要调整,参考:http://jpetazzo.github.io/2014/01/29/docker-device-mapper-resize/ 。中文译文: http://zhumeng8337797.blog.163.com/blog/static/100768914201452405120107/ )
你可以在启动docker daemon时用参数-s 指定driver: docker -d -s devicemapper ;
Btrfs:
Btufs driver 在docker build 可以很高效。但是跟 devicemapper 一样不支持设备间共享存储(文档里是does not share executable memory between devices)。
下面笔者就已有的条件去分析下docker的镜像与容器的存储结构。
环境:
opensuse 13.10 + Docker version 1.2.0, build fa7b24f
Ubuntu 14.10 + Docker version 1.0.1, build 990021a
在没有aufs支持的linux发行版本上(CentOS,opensuse等)安装docker可能就使用了devicemapper driver。
查看你的linux发行版有没有aufs支持:lsmod | grep aufs
笔者opensuse 13.10里是没有加载这个模块的:
而虚拟机里的ubuntu 14.10 是加载了这个模块的:
而我们列出/var/lib/docker 这个目录的内容也可以看出你那个docker是使用了哪个storage driver:
opensuse 13.10 上的/var/lib/docker
这里应该看出是使用了device mapper这个driver ;
然后再来看看虚拟机ubuntu 14.10上/var/lib/docker 目录:
这里也可以看出笔者ubuntu里docker 是使用了aufs 这个driver : 下文就这两个不同的driver作对比。
请注意分析的是哪一个。
那么镜像文件是本地存放在哪里呢?
笔者在opensuse和ubuntu里把docker彻底重新安装了一遍删除了所有镜像,并只Pull下来一个ubuntu:14.10的镜像,这样分析起来会比较简单明了: 现在两个系统都只有一个ubuntu:14.10的镜像:
opensuse:
Ubuntu :
好了。首先现在我们来看看/var/lib/docker里都是什么文件。
1、首先用Python 的json.tool工具查看下repositories-* 里的内容。
opensuse:
里面的json数据记录的正是本地上存放的镜像的名称及其64位长度的ID.这个ID可以有其12位的简短模式。 Ubuntu上也是一样的:
而且我们可以发现这两个ID是一样。这时我们其实可以猜想到:这个ID是全局性的,就是说你这个镜像在镜像仓库上的ID也是这个。被其它机器上ID也是这个。这样的好处无疑是方便管理镜像。
2、/var/lib/docker/graph 目录里的内容:
opensuse:
Ubuntu:
Graph目录里有7个长ID命名的目录,其中第二个长ID是我们所pull下来的ubuntu14.10镜像的对应的长ID..那么其它6个是怎么来的呢?
这里我们用docker images -tree列出镜像树形结构:
可以看到最下层的镜像是我们的ubuntu14.10。那么上面对应的是6个layer。就是说在这个树中第n+1个层是基于第n个层上改动的。而第个层在graph目录里都对应着一个长ID目录。
我们来看看虚拟机里ubuntu14.10 里的docker images -tree:
大小数量一致。但是到了最后一个层的大小不一样(这里原因可能会是系统问题,也可能是docker版本问题。具体原因需要另外考察)
再分析一下各个层的大小,第一个为0B, 第二个层就应该为198.9MB,第三个层大小为0.2MB(199.1-198.9)…如此类推下去。
上层的image依赖下层的image(注:这里的逻辑上层是上图树形结构的下层),因此docker中把下层的image称作父image,没有父image的image称作base image ;
例如我要用这里的ubuntu:14.10为模板启动一个容器时,docker会加载树形结构中的最下层( 2185fd5…),然后加载其父层(f180ea…),这样一直加载到第一层(511136…)才算加载这个rootfs。那么一个层在哪里保存它的父 层信息呢?在下面长ID目录里的json文件其实也可以看到这个信息。
graph长ID目录内容:(对于ubuntu里是一样的,这里以opensuse为例)
我们进入长ID目录里看看里面的内容:
opensuse :
我们进入最后一个层长ID目录里。里面有一个json文件及一个名为layersize的文件。 用cat查看layersize里的内容,里面记录的数字是指这个层的大小。这里(绿色前头)是0。而我们从上面的目录树可以算出最后一个层确实是0。如 果还不相信。我们再算算倒数第二个层的大小(opensuse里的树形图里短id为f180ea115597的层)应该为37.8M。现在进入对应长ID 目录:
可以看到是是37816084(B),约37.8M,与我们计算的刚刚吻合。
而另一个文件json又是什么呢?用python工具看看:(内容有点多,没有截完)
可以看到json这个文件保存的是这个镜像的元数据。
拉到底部可以看到有个parent:的值:
这个就是保存了其父层长ID的值。对照树形结构看f180ea115597 的父层是不是0f154c52e965 。
但是注意在graph这个目录里并没有找到我们想找到的镜像内容存放地。只是一些镜像相关的信息数据。
镜像里的内容存放在哪里
opensuse :
在opensuse下的/var/lib/docker/devicemapper/devicemapper/这个目录下找到两个文件,并列出其大小。
其中一个data的文件大小为100G(非真实占用)。真实占用的情况如下:
100G的只占用了590M。
上面我们讲到:Device mapper driver 会创建一个100G的简单文件包含你的镜像和容器。每一个容器被限制在10G大小的卷内。那么看来这个100G的简单文件正是这个名为data 的文件,那么镜像和容器下是存放在这里的。
好了。这时我在opensuse上再pull下一个ubuntu:12.10 镜像看看这个文件大小有什么变化: 这次一下子截了三个命令的信息:
Pull下来的ubuntu是172.1M。树形结构可以看到各个层的关系。而data的大小变成了787M. 没pull ubuntu:12.10之前是590M.增加了197M,跟pull下来的172.1M有点差距。这里可认为是存储了额外的某些信息。
那么容器是不是也存放在这里呢?
我们用ubuntu14.10启动一个模板看看情况如何:
这次我也是一下子截了几个命令:
可以看到了一个基于ubuntu:14.10镜像的容器在运行中,简短ID是a9b35d72fcd4,
第二个命令du列出了data的大小为789M,增加了2M。
第三个命令列出了container目录内出现一个长ID的目录,ID就是运行的容器的ID。但是里面的文件应该都是些配置文件。并没有我们想要的内容目录。
这样的话我们进一步做测试:在运行的容器内使用dd if=/dev/zero of=test.txt bs=1M count=8000 创建一个8G大小的文件后:
这里data变成了8.6G,增长了接近8G,这样也证实了容器里的内容是保存在data这个简单文件内的。
这样的话证实了devicemapper driver是把镜像和容器的文件都存储在data这个文件内。
Ubuntu 的aufs driver 又如何呢:
Ubuntu上由于是aufs driver 所以/var/lib/docker 目录下有aufs目录而不是devicemapper 目录:
这里的aufs 目录有三个目录,diff 、layers 、mnt 三个目录。
这里layers目录是保存了layers层次信息,并不是layers里面的内容。
而diff 目录时有数个长ID目录:
列出这几个目录的大小可以看出基本与上面树形结构的所能计算的大小相对应(相关部分可能是由于压缩或者其它原因造成,这里纯属猜测)。
那我们进入f180ea115597这个ID对应的目录看看里面是什么:
里面是一些文件夹,但是只有几个,并不像我们平时常规linux发行版里的那么齐全。
这里的话其实我们可以想到了因为一个层是基于另一个层之上的。Aufs文件系统可以做到增量修改,所以这里的几个文件夹是基于上一个层做的修改内容增量地保存在这里,因为上一个层对于这个层来说不可写:
在这里我需要先引用一张网上的图片:
这里我们可以看到一个我们想象中的运行中的container是包含了若干个readonly的image层,然后最上面的writable层才是我们可写的层。第一个readonly的层会加载其父层。直到最下面的base image层。
我们所做的改动会被保存在最上面的那个writable层里。当我们用commit 把容器固化成镜像时那个层就会变成我们上面看到的“目录不齐全的”长ID目录。
为了证实这一点,我们在运行一个基于ubuntu:14.10镜像的容器:
可以看到运行的容器简短ID为7b3c13323d8c 。
这时再列出diff目录的内容:
多了两个长ID目录,正是我们运行的容器的ID,列出内容:
然后我们在运行的容器中创建一个/test 目录,并在里面用dd命令创建一个8G的test.txt文件:完成这些后再列出这两个目录内容:
可以看到其中一个目录(没有init后缀)变成了7.5G,而另一个目录还是24K。
在长ID目录里还多了一个test文件夹,正是我们在容器里创建的,这样的话里面无疑问就是test.txt文件了。容器通过这种方法在writable层里记录了修改过的内容(增量记录) (这里有个小问题笔者也还不清楚:怎么记录删除了东西呢?这个问题以后再考察)
从上面我们可以知道容器的writable 层是保存在以容器ID为名的长ID目录里的,而ID+init后缀目录是保存容器的初始信息的。
好了,现在我们进行最后一个实验:把容器固化成镜像。
(这里要做个小小调整。把上面8G的文件删除了再建一个3G大小的文件test_3G.txt代替)
Commit 后把容器固化成了test_image的镜像。得到那个镜像的长ID。
现在看看变化:
那个窗口目录还在,原因是我们还没用rm 命令删除那个容器。而多出来的镜像目录正是我们固化所得到的,其大小与上面容器writable层大小一致为3GB。现在看看里面是什么内容:
里面有一个test目录,目录下对应我们创建的3GB大小的test_3G.txt文件。
这就是我们改动过的内容保存了在这个目录内。
现在我们用rm命令删除容器看看结果:
容器被删除了,其对应的长目录ID也被删除了。而那个固化的得到的镜像( c7560af30 )被保存了下来。