Docker 镜像是什么?
- 镜像是一种轻量级、可执行的独立软件包,它包含运行某个软件所需要的所有内容,我们将应用程序和配置打包好形成一个可交付的运行环境(包括代码、运行时所需要的库、环境变量和配置文件等),这个打包好的运行环境就是 image 镜像文件。
- 只有通过镜像文件才能生成 Docker 容器实例。
1.镜像分层
- 以拉取 tomcat 镜像为例,我们可以看到 Docker 的镜像好像是一层层的下载。
docker pull tomcat
也可以通过如下的命令来查看镜像构建(build)历史信息:
docker history tomcat
2.overlay2 镜像存储结构
上文的tomcat 镜像总共拉取了 8层镜像,被存储在了 / var/lib/docker/overlay2 / 目录下,这里面多了一个 l 目录包含了所有层的软连接,软链接使用短名称,避免 mount 时候参数达到页面大小限制。
ls /var/lib/docker/overlay2/l/ -l
处于底层的镜像目录包含了一个 diff 和一个 link 文件,diff 目录存放了当前层的镜像内容,而 link 文件则是与之对应的短名称:
在这之上的镜像还多了 work 目录和 lower 文件,lower 文件用于记录父层的短名称,work 目录用于联合挂载指定的工作目录。而这些目录和镜像的关系是怎么组织在的一起呢?答案是通过元数据关联。元数据分为 image 元数据和 layer 元数据。
image 元数据
镜像元数据存储在了 /var/lib/docker/image/overlay2/imagedb/content/sha256/ 目录下,名称是以镜像 ID 命名的文件,镜像 ID 可通过 docker images 查看,这些文件以 json 的形式保存了该镜像的 rootfs (只读层)信息、镜像创建时间、构建历史信息、所用容器、包括启动的 Entrypoint 和 CMD 等等。例如 ubuntu 镜像的 id 为 edbfe74c41f8:
查看其对应的元数据 (使用 vim :%!python -m json.tool 格式化成 json) 截取了其 rootfs 的构成:
Layer是可以多层的,我们换个tomcat镜像看:
docker inspect tomcat | grep -A 10 "Layer"
/var/lib/docker/image/overlay2/imagedb/content/sha256.下:
上面的 diff_id 对应的的是一个镜像层(镜像包含多个镜像层),其排列也是有顺序的,从上到下依次表示镜像层的最低层到最顶层
diff_id 如何关联镜像层内容?具体说来,docker 利用 rootfs 中的每个 diff_id 和历史信息计算出与之对应的内容寻址的索引 (chainID) ,而 chaiID 则关联了 layer 层,进而关联到每一个镜像层的镜像文件。
layer 元数据
用户在 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 目录下会存在四或五个文件 cache-id、diff、size、parent(最底层的chainID不存在此文件)、tar-split.json.gz:
ls /var/lib/docker/image/overlay2/layerdb/sha256/1064b760561be2a46c302bbb9bcfa5156727e84ce0002fb32a7e99a231540f83/
cache-id diff parent size tar-split.json.gzpwd
cache-id 文件:
docker 随机生成的 uuid,内容是保存镜像层的目录索引,也就是 / var/lib/docker/overlay2 / 中的目录,这就是为什么通过 chainID 能找到对应的 layer 目录。
以 chainID 为 1064b760561be2a46c302bbb9bcfa5156727e84ce0002fb32a7e99a231540f83对应的目录为9404dfddab86b3479c2219cac644429c6f15d623d8486bdc5971e06efee91690,也就保存在 /var/lib/docker/overlay2/9404dfddab86b3479c2219cac644429c6f15d623d8486bdc5971e06efee91690
cat /var/lib/docker/image/overlay2/layerdb/sha256/1064b760561be2a46c302bbb9bcfa5156727e84ce0002fb32a7e99a231540f83/cache-id
9404dfddab86b3479c2219cac644429c6f15d623d8486bdc5971e06efee91690
diff 文件:
保存了镜像元数据中的 diff_id(与元数据中的 diff_ids 中的 uuid 对应)
cat 1064b760561be2a46c302bbb9bcfa5156727e84ce0002fb32a7e99a231540f83/diff
sha256:95a49d723ac3b7b685a3cf90e02e8480c2adfe07a3814c6a4f1ddd17618dc2d8
size 文件:
保存了镜像层的大小,单位是字节。
cat 94a819b97c531e343e4cf174587f6d91bf843ff57c305bb48475766f7687abfc/size
0
parent文件:
parent文件存放当前layer的父layer的chainID.
注意:对于最底层的layer来说,由于没有父layer,所以没有这个文件。
tar-split.json.gz文件:
layer压缩包的是split文件,通过这个文件可以还原layer的tar包。在docker save导出image的时候会用到。
在 layer 的所有属性中,diffID 采用 SHA256 算法,基于镜像层文件包的内容计算得到。而 chainID
是基于内容存储的索引,它是根据当前层与所有祖先镜像层 diffID 计算出来的,具体算如下: 如果该镜像层是最底层 (没有父镜像层),该层的
diffID 便是 chainID。 该镜像层的 chainID 计算公式为 chainID(n)=SHA256(chain(n-1)
diffID(n)),也就是根据父镜像层的 chainID 加上一个空格和当前层的 diffID,再计算 SHA256 校验码。
计算chainID的具体方法:$echo -n “sha256:babe7ce…(自己补全)
sha256:283fb…(自己补全)”|sha256sum //其余chainID以此类推
mountedLayer 信息存储的可读 init 层以及容器挂载点信息包括:容器 init 层 ID(init-id)、联合挂载使用的 ID(mount-id)以及容器层的父层镜像的 chainID(parent)。相关文件位于 /var/lib/docker/image/<storage_driver>/layerdb/mounts/<container_id>/ 目录下。
关于 init 层
init 层是以一个 uuid±init 结尾表示,夹在只读层和读写层之间,作用是专门存放 / etc/hosts、/etc/resolv.conf 等信息,需要这一层的原因是当容器启动时候,这些本该属于 image 层的文件或目录,比如 hostname,用户需要修改,但是 image 层又不允许修改,所以启动时候通过单独挂载一层 init 层,通过修改 init 层中的文件达到修改这些文件目的。而这些修改往往只读当前容器生效,而在 docker commit 提交为镜像时候,并不会将 init 层提交。该层文件存放的目录为 / var/lib/docker/overlay2/<init_id>/diff
启动容器所需文件
这几个文件都是 Linux 运行时必须的文件,如果缺少的话会导致某些程序或者库出现异常,所以 docker 需要为容器准备好这些文件:
/dev/console: 在 Linux 主机上,该文件一般指向主机的当前控制台,有些程序会依赖该文件。在容器启动的时候,docker 会为容器创建一个 pts,然后通过 bind mount 的方式将 pts 绑定到容器里面的 / dev/console 上,这样在容器里面往这个文件里面写东西就相当于往容器的控制台上打印数据。这里创建一个空文件相当于占个坑,作为后续 bind mount 的目的路径。
hostname,hosts,resolv.conf:对于每个容器来说,容器内的这几个文件内容都有可能不一样,这里也只是占个坑,等着 docker 在外面生成这几个文件,然后通过 bind mount 的方式将这些文件绑定到容器中的这些位置,即这些文件都会被宿主机中的文件覆盖掉。
/etc/mtab:这个文件在新的 Linux 发行版中都指向 / proc/mounts,里面包含了当前 mount namespace 中的所有挂载信息,很多程序和库会依赖这个文件。
注意: 这里 mtab 指向的路径是固定的,但内容是变化的,取决于你从哪里打开这个文件,当在宿主机上打开时,是宿主机上 /
proc/mounts 的内容,当启动并进入容器后,在容器中打开看到的就是容器中 / proc/mounts 的内容。
启动容器的配置文件:
docker 将用户指定的参数和 image 配置文件中的部分参数进行合并,然后将合并后生成的容器的配置文件放在 / var/lib/docker/containers / 下面,目录名称就是容器的 ID
tree /var/lib/docker/containers/112d4455fc3751c898d1ee78bfb913bb4057b59b278bd3822f5d997ee746aaee/
config.v2.json: 通用的配置,如容器名称,要执行的命令等
hostconfig.json: 主机相关的配置,跟操作系统平台有关,如 cgroup 的配置
checkpoints: 容器的 checkpoint 这个功能在当前版本还是 experimental 状态。
checkpoints 这个功能很强大,可以在当前 node 做一个 checkpoint,然后再到另一个 node
上继续运行,相当于无缝的将一个正在运行的进程先暂停,然后迁移到另一个 node 上并继续运行。
小结
通过以上的内容介绍,一个容器完整的层应由三个部分组成,如下图:
镜像层:也称为 rootfs,提供容器启动的文件系统
init 层: 用于修改容器中一些文件如 / etc/hostname、/etc/resolv.conf 等
容器层:使用联合挂载统一给用户提供的可读写目录。
本章介绍了以 overlayfs 作为存储驱动的的镜像存储原理其中每层的镜像数据保存在 /var/lib/docker/overlay2//diff 目录下,init 层数据保存了在 /var/lib/docker/overlay2//diff 目录下,最后统一视图(容器层)数据在 /var/lib/docker/overlay2/<mount_id>/diff 目录下,docker 通过 image 元数据和 layer 元数据利用内容寻址(chainID)将这些目录组织起来构成容器所运行的文件系统。
/var/lib/docker/image/aufs/imagedb/metadata:里面存放的是本地 image 的一些信息,从服务器上 pull 下来的 image 不会存数据到这个目录(本地指的是自己在本地构建的)
3.镜像加载原理
- bootfs (boot file system)主要包含 bootloader 和 kernel ,bootloader 主要是引导加载 kernel ,Linux 刚启动的时候会加载 bootfs 文件系统,在 Docker 镜像的最底层是引导文件 bootfs。这一层和典型的 Linux/Unix 系统是一样的,包含 bootloader 和 kernel,当 bootloader 加载完成之后整个内核就在内存之中了,此时内存的使用权已经由 bootfs 转交给内核,此时系统也会卸载 bootfs 。
- rootfs(root file system,根文件系统)在bootfs之上,包含的就是典型的unix系统的/dev、 /proc、 /etc等标准目录和文件和一些命令,rootfs就是不同unix系统的发行版,比如Ubuntu、centos等。
我们平时安装的虚拟机centos镜像好几个G,Docker安装的才200多m,因为对于一个精简的OS,rootfs可以很小,只需包含最基本的命令,工具和程序库就行了,因为底层直接使用宿主机的内核,自己只需提供rootfs(相当于操作内核的客户端)就可以,由此可见不同发行版的bootfs基本是一致的,roorfs有差别,因此不同的发行版可以公有bootfs。
第一个图仅仅是bootfs+rootfs,然后如果要制作一个emacs环境的镜像,就在这个基础上新加一层emacs镜像,如图二。如果要在添加一个Apache环境,那就再图二基础上加一个apache镜像。如图三。图中的每一层镜像都能进行复用。
镜像都是一个个独立可复用的镜像,如果下载其他镜像是,某一层镜像是已经存在本地的了,就不用在下载,直接复用该镜像,节省空间。
持续更新中,关注不迷糊…