前言
我们学习docker技术也许都是从执行命令docker pull ubuntu然后docker run -it ubuntu开始的,经过一段时间使用会对docker image有了基本认识,也大概了解了image和container之间的关系,docker image是一个层次结构,通过union filesystem技术将若干个只读layer合并成一个文件系统,并在最上层叠加一个可读写layer为container提供统一的根文件系统视图,通过copy on write和white out技术,提供container内文件新增,删除和修改功能。随着学习深入还是有几个问题待解:首先,image是静态只读的,而container是动态读写的,启动container时,比如简单的一条命令docker run -it ubuntu,并没有其他参数,docker daemon是如何获取相应image的位置,又是如何利用层次结构信息组装出一个可读写的rootfs提供给container使用;其次,随着docker技术的普及,docker image是将来应用交付,测试和部署的唯一媒介。开发人员除了完成源代码开发和构建应用,也要学会如何制作image,不能只会使用现成的类似ubuntu这样的image,image是使用docker build命令依据Dockfile文件生成的,学会从零开始构建一个简单但是可运行的image将非常有助于理解构建原理,对未来构建任何复杂的image是很好的起点。
准备工作
安装docker 版本1.11.0,环境配置(通过docker info获取)
注意这里docker使用的存储驱动是aufs,aufs是docker官方最早支持的存储驱动,稳定可用于生产环境。如果你的docker使用的存储驱动不是aufs,下文结果有所不同。
深入理解Docker image
以ubuntu:latest为例,首先docker pull ubuntu,然后docker history ubuntu看它的层次结构。可以看到ubuntu:latest有4层image,这里上层依赖下层,最下层是base image。如果感觉不够直观,可以用dockviz(这个是第三方工具,可从github下载)看。
每层image有个sha256的hash id(图示中只截取前面几位),创建时间,创建命令,image大小。创建命令,其实对应生成该层的Dockerfile命令,我们看到还有image的大小为0B,说明该image应该是空的。
现在开始分析docker内部到底如何管理image的层次结构。从docker info知道docker的rootdir是/var/lib/docker,它把所有的image layers存储在/var/lib/docker/aufs目录下,aufs有3个子目录,分别是diff,layers,mnt。
l diff目录存储所有image layer内容,除1个目录没有内容外,其他都有若干个子目录。
l layers目录存储所有image layer的层次信息。
从命令显示结果可以看到,每个image layer对应一个文件,文件内容是下层layer的名称。
l mnt目录存储容器的每个image layer的对应挂载点的内容,因为现在还没创建任何容器,所以目录里面暂时都是空的。
其实,aufs目录只是存储image layers的内容,所有image layers的元数据都存储在另一个重要的目录/var/lib/docker/graph中,正是这个目录提供了全部layers的层次结构信息 。
每个layer对应一个目录,每个目录下有2个文件(暂时不理_tmp目录),json和layersize。layersize文件内容是一个数字,表示该layer的大小。json文件有很多信息,为了输出更有可读性,用python的json模块来显示,命令是python -m json.tool json,在一堆冗长的输出找到2个信息:id和parent 。id是本image layer的名称,而parent正是下层image layer的名称,通过这个关系依次查看其他几个image layer,可以得到整个ubuntu:latest的4个image layers的层次结构。
执行docker run -it ubuntu命令时docker daemon如何按照上面的层次信息组装出一个可读写的rootfs?熟悉Git的话都会想到肯定有一个地方,保存着一个指针,指向整个层次结构的“头”,在/var/lib/docker目录有一个文件就有这个作用 。看到ubuntu:latest指向正是顶层image layer所在目录。
执行docker run -it ubuntu,可以看到容器中rootfs已经组装起来了。
此时在/var/lib/docker/aufs的几个子目录,也发生了一些变化。diff中新增了2个名字几乎一样的目录,其中一个目录还有2个子目录。
layers中新增了2个文件,文件名称与新增目录是对应的,这里的8296fab70eb4c7853701b28b6c5951918bf745e6c1befc5606cbfaed2156de20-init是8296fab70eb4c7853701b28b6c5951918bf745e6c1befc5606cbfaed2156de20的下层。
可以猜测新增的目录跟容器的可读写layer有关,试着验证下。在容器中新建一个文件testfile。果然在8296fab70eb4c7853701b28b6c5951918bf745e6c1befc5606cbfaed2156de20目录中出现testfile文件,内容也是一致的。
新增还相对容易理解,在容器中删除文件呢?比如删除/var/log/dpkg.log,这个文件是位于8aa2fc7185e20bacda32d815eaae32cbc1c0457dc160ed5b3995ab79a8c7fd98的image里面的,在容器中删除文件,如图14。8aa2fc7185e20bacda32d815eaae32cbc1c0457dc160ed5b3995ab79a8c7fd98的image里文件依然存在,如图15。同时在8296fab70eb4c7853701b28b6c5951918bf745e6c1befc5606cbfaed2156de20/var/log多了一个文件.wh.dpkg.log,大小是0。这就是传说中的whiteout技术,效果就是删除文件不必修改下层image,只要在顶层增加一个.wh文件,对应被删除的文件
最后看下如何修改文件,还是以倒霉的dpkg.log为例(谁叫你是日志文件呢,改就改了)。修改文件。在8aa2fc7185e20bacda32d815eaae32cbc1c0457dc160ed5b3995ab79a8c7fd98的image里文件依然没有变化,如图18,8296fab70eb4c7853701b28b6c5951918bf745e6c1befc5606cbfaed2156de20/var/log多了一个文件dpkg.log。从文件大小可以看出这个dpkg.log是完整的修改后的文件,可不是增量的修改内容。这其实就是copy on write技术,修改后的文件覆盖原文件,而且不必修改下层image。
构建最小image
从上节我们知道运行容器的必要条件就是要提供一个rootfs,之前使用的ubuntu就是一个rootfs,是从docker hub上pull下载的。理论上只要具备组成rootfs的要素,就可以自己构建出一个image,使用它运行容器。构建docker image是用dockerfile完成的。我们来试着构建一个世界上最小的image。
先写一个最简单的程序,hello,功能就是输出hello,docker!。
编译,gcc -o hello hello.c得到hello。然后写一个Dockerfile。
Dockerfile的语法规则简单解释下,FROM命令后面的参数是base image,一切Dockerfile都是从这句开始的。在现成的image上做二次开发,比如ubuntu就写FROM ubuntu,这里我们是从零开始,所以是FROM scratch,scratch是空的base image。COPY就是复制本地文件到image中,类似cp命令。CMD是容器启动时执行的命令。
有了Dockerfile,就可以开始构建。命令是docker build -t hello .,-t的参数是image的名称,成功完成。
可以看到新的image已经生成,大小约8.5kB。
有了image,那就启动容器试试,结果报错了,没有输出期待的hello,docker! 。
想到hello会依赖一些系统库文件,而scratch image里连/lib目录都没有 。
看下hello到底依赖哪些系统库 。
其中/lib/x86_64-linux-gnu/libc.so.6和/lib64/ld-linux-x86-64.so.2是符号链接,分别指向/lib/x86_64-linux-gnu/libc-2.19.so和/lib/x86_64-linux-gnu/ld-2.19.so,修改Dockerfile,把它们全部放进去,修改后的Dockerfile。
顺便说下这里分几次COPY而不是一次完成,是为了更好利用docker的build cache机制。再次build。
新构建的image已经生成,大小接近4MB,当然大部分空间是系统库文件占用了。再次启动容器,输出正常。
这样就制作出了一个不能再精简的image,其实以后要移植一些大型的应用,比如bash,mysql,tomcat等,道理都是一样的,只要把应用的可执行文件和所有依赖库复制进来就可实现,还有一些技巧,确保image的构建时间较短,层次结构合理,这对大型应用的image构建有重要意义。