Docker核心原理:镜像和容器
镜像
Docker镜像是一个只读的容器模板,含有启动Docker容器所需的文件系统。Docker镜像的文件内容和一些运行容器的配置文件组成了Docker容器的文件系统运行环境——rootfs。
当我们使用docker pull下载一个nginx镜像后,可以在Docker的工作目录/var/lib/docker/image/overlay2下找到它的相关信息(路径中的overlay2是Docker目前使用的一种存储驱动,我们后面会详细讲解这项技术):
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest f6d0b4767a6c 3 weeks ago 133MB
[root@localhost ~]# ls /var/lib/docker/image/overlay2
distribution imagedb layerdb repositories.json
Repository
在Docker的镜像管理系统中,registry代表镜像仓库,如官方的Docker Hub。而repository则代表镜像组,即包含了不同版本的镜像集合。repositories.json文件中描述了宿主机上所有镜像的repository元数据,主要包括镜像名、tag和镜像ID。而镜像ID是Docker采用SHA256算法,根据镜像元数据配置文件计算得出的。
[root@localhost ~]# cat /var/lib/docker/image/overlay2/repositories.json | python -mjson.tool
{
"Repositories": {
"nginx": {
"nginx:latest": "sha256:f6d0b4767a6c466c178bf718f99bea0d3742b26679081e52dbf8e0c7c4c42d74",
"nginx@sha256:10b8cc432d56da8b61b070f4c7d2543a9ed17c2b23010b43af434fd40e2ca4aa": "sha256:f6d0b4767a6c466c178bf718f99bea0d3742b26679081e52dbf8e0c7c4c42d74"
}
}
}
Image
镜像的元数据信息保存在imagedb目录中,其文件结构如下:
[root@localhost ~]# tree /var/lib/docker/image/overlay2/imagedb
/var/lib/docker/image/overlay2/imagedb
├── content
│ └── sha256
│ └── f6d0b4767a6c466c178bf718f99bea0d3742b26679081e52dbf8e0c7c4c42d74
└── metadata
└── sha256
4 directories, 1 file
打开content/sha256目录下以镜像ID命名的文件,我们可以看到镜像的元数据信息,包括了镜像架构、操作系统 、默认配置、创建时间、历史信息和rootfs等:
[root@localhost ~]# cat /var/lib/docker/image/overlay2/imagedb/content/sha256/f6d0b4* | python -mjson.tool
{
"architecture": "amd64",
...
"created": "2021-01-12T10:17:41.649267496Z",
"docker_version": "19.03.12",
...
"os": "linux",
"rootfs": {
"diff_ids": [
"sha256:cb42413394c4059335228c137fe884ff3ab8946a014014309676c25e3ac86864",
"sha256:1c91bf69a08b515a1f9c36893d01bd3123d896b38b082e7c21b4b7cc7023525a",
"sha256:56bc37de0858bc2a5c94db9d69b85b4ded4e0d03684bb44da77e0fe93a829292",
"sha256:3e5288f7a70f526d6bceb54b3568d13c72952936cebfe28ddcb3386fe3a236ba",
"sha256:85fcec7ef3efbf3b4e76a0f5fb8ea14eca6a6c7cbc0c52a1d401ad5548a29ba5"
],
"type": "layers"
}
在这些信息中,我们最关心的是rootfs。上文提到,rootfs是容器运行的文件系统环境。而从上面的元数据信息中我们发现,rootfs是由多个layer文件组成的。元数据记录了这些layer的diffID,它们是Docker使用SHA256算法根据layer文件内容计算得到的。这样做的好处是可以根据diffID检查layer文件的完整性,并且可以让相同diffID的layer文件被不同镜像共享。
Layer
layerdb/sha256下的目录名称是以layer的chainID来命名的,它的计算方式为:
- 如果layer是最底层,没有任何父layer,那么diffID = chainID;
- 否则,chainID(n)=sha256sum(chainID(n-1)) diffID(n))
[root@localhost ~]# tree /var/lib/docker/image/overlay2/layerdb -L 2
/var/lib/docker/image/overlay2/layerdb
├── sha256
│ ├── 3c90a0917c79b758d74b7040f62d17a7680cd14077f734330b1994a2985283b8
│ ├── 4dfe71c4470c5920135f00af483556b09911b72547113512d36dc29bfc5f7445
│ ├── a1c538085c6f891424160d8db120ea093d4dda393e94cd4713e3fff3c82299b5
│ ├── a3ee2510dcf02c980d7aff635909612006fd1662084d6225e52e769b984abeb5
│ └── cb42413394c4059335228c137fe884ff3ab8946a014014309676c25e3ac86864
└── tmp
我们可以发现只有layercb42413394c*的diffID和chainID相等,因为它就是镜像的最底层。实际上image元数据中layer的diffID是以低层到高层的顺序记录的,我们可以根据公式计算出倒数第二层的chainID:
[root@localhost ~]# echo -n "sha256:cb42413394c4059335228c137fe884ff3ab8946a014014309676c25e3ac86864 sha256:1c91bf69a08b515a1f9c36893d01bd3123d896b38b082e7c21b4b7cc7023525a" | sha256sum -
a3ee2510dcf02c980d7aff635909612006fd1662084d6225e52e769b984abeb5 -
[root@localhost ~]# ls /var/lib/docker/image/overlay2/layerdb/sha256 | grep a3ee2510dcf0*
a3ee2510dcf02c980d7aff635909612006fd1662084d6225e52e769b984abeb5
每一个以layer的chainID命名的目录下都保存了镜像层layer的元数据信息:
[root@localhost ~]# ls /var/lib/docker/image/overlay2/layerdb/sha256/a3ee2510dcf02c980d7*
cache-id diff parent size tar-split.json.gz
- parent:父layer的chainID
- size:layer文件的大小
- cache-id:存储驱动通过cache-id索引到layer的实际文件内容
[root@localhost ~]# cat /var/lib/docker/image/overlay2/layerdb/sha256/a3ee2510dcf02c980d7*/parent
sha256:cb42413394c4059335228c137fe884ff3ab8946a014014309676c25e3ac86864
[root@localhost ~]# cat /var/lib/docker/image/overlay2/layerdb/sha256/a3ee2510dcf02c980d7*/size
63704232
[root@localhost ~]# cat /var/lib/docker/image/overlay2/layerdb/sha256/a3ee2510dcf02c980d7*/cache-id
0363fcae3b4410c394b8a99e0a24d1ec01eb5198c82d3422f9c411ceaad98286
如果我们启动一个容器,便可以发现Docker会在layerdb目录下新生成一个mounts目录:
[root@localhost ~]# docker run -d --name=nginx -v /home:/home nginx
45f30cb6a063a7251db4388f17f85c1226d96277cb74693c1f38bef1d17b6193
[root@localhost ~]# tree /var/lib/docker/image/overlay2/layerdb -L 2
/var/lib/docker/image/overlay2/layerdb
├── mounts
│ └── 45f30cb6a063a7251db4388f17f85c1226d96277cb74693c1f38bef1d17b6193
├── sha256
│ ├── 3c90a0917c79b758d74b7040f62d17a7680cd14077f734330b1994a2985283b8
│ ├── 4dfe71c4470c5920135f00af483556b09911b72547113512d36dc29bfc5f7445
│ ├── a1c538085c6f891424160d8db120ea093d4dda393e94cd4713e3fff3c82299b5
│ ├── a3ee2510dcf02c980d7aff635909612006fd1662084d6225e52e769b984abeb5
│ └── cb42413394c4059335228c137fe884ff3ab8946a014014309676c25e3ac86864
└── tmp
9 directories, 0 files
mounts目录下有着以容器ID命名的文件,其内部记录了容器层layer的元数据信息:
[root@localhost ~]# ls /var/lib/docker/image/overlay2/layerdb/mounts/45f30cb6a063*
init-id mount-id parent
那么容器到底是什么呢?
容器
通过上面的一些探索,我们已经知道了镜像是由多个layer组成的文件,并在容器启动时成为容器文件系统的运行环境——只读的rootfs。而容器其实就是Dokcer利用存储驱动在只读rootfs上挂载一个可读写层后的结果。
联合挂载
联合挂载技术可以在一个挂载点同时挂载多个文件系统,将挂载点的原目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录。
Overlay2是Docker目前使用的一种联合挂载技术,它主要通过四类目录完成工作:
- lower:底层文件系统。对于Docker来说,就是只读的镜像层;
- upper:上层文件系统。对于Docker来说,就是可读写的容器层;
- merged:作为统一视图的联合挂载点。对于Docker来说,就是用户视角下的文件系统;
- work:提供辅助功能。
我们启动一个容器,观察系统中overlay2类型的挂载情况:
[root@localhost ~]# docker run -d --name=nginx -v /home:/home nginx
45f30cb6a063a7251db4388f17f85c1226d96277cb74693c1f38bef1d17b6193
[root@localhost ~]# mount -t overlay
overlay on /var/lib/docker/overlay2/dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4/merged type overlay (rw,relatime,seclabel,lowerdir=/var/lib/docker/overlay2/l/U7ZXQ4ZL7TLD6XEBUVLR77LKS4:/var/lib/docker/overlay2/l/BXVB3Q7277EPHJEMMHKEOR6YS5:/var/lib/docker/overlay2/l/5K76AX5UNX35LFXZKLATNWHOIK:/var/lib/docker/overlay2/l/QTGJLTLBMEML5OGHHZUYM3GHTR:/var/lib/docker/overlay2/l/55T5LSGE3C2NFFQ54FHM7YDNKY:/var/lib/docker/overlay2/l/R2AW2LUWRDIV7DLJFYMS67LB3L,upperdir=/var/lib/docker/overlay2/dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4/diff,workdir=/var/lib/docker/overlay2/dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4/work)
根据输出结果我们可以找到overlay2的挂载点位置:
[root@localhost ~]# ll /var/lib/docker/overlay2/l
总用量 0
lrwxrwxrwx. 1 root root 72 2月 2 07:37 55T5LSGE3C2NFFQ54FHM7YDNKY -> ../0363fcae3b4410c394b8a99e0a24d1ec01eb5198c82d3422f9c411ceaad98286/diff
lrwxrwxrwx. 1 root root 72 2月 2 07:37 5K76AX5UNX35LFXZKLATNWHOIK -> ../14faccdb685b520f09148aaafec45dd31d2e6aa96516b60deaf59228ae9dbe66/diff
lrwxrwxrwx. 1 root root 72 2月 2 07:37 BXVB3Q7277EPHJEMMHKEOR6YS5 -> ../82a99d340893301cf33c79588042cd7d5db55cbcf34ca4b7a6ade609b6d28c96/diff
lrwxrwxrwx. 1 root root 72 2月 2 10:02 M5NBJHQ4ICWANIEYBYMJ5FLCZO -> ../dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4/diff
lrwxrwxrwx. 1 root root 72 2月 2 07:37 QTGJLTLBMEML5OGHHZUYM3GHTR -> ../5b75a3d9c3881dd8041cdc3fa73679a9f1e4f1052d4548e22b5f4e4ccce7c2d0/diff
lrwxrwxrwx. 1 root root 72 2月 2 07:37 R2AW2LUWRDIV7DLJFYMS67LB3L -> ../bd47b78b55e951d504c6e70c0e7af451b020a5741f71ed0e91cb8f8dc77a8664/diff
lrwxrwxrwx. 1 root root 77 2月 2 10:02 U7ZXQ4ZL7TLD6XEBUVLR77LKS4 -> ../dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4-init/diff
- /var/lib/docker/overlay2/l目录下保存的均是软链接文件,其文件名是避免使用mount命令时输出结果达到页面大小限制而生成的短名称;
- 在lowerdir中的软链接,除U7ZXQ4ZL7TLD6XEBUVLR77LKS4外,均指向了只读镜像层文件的挂载点,分别对应了我们下载的ngingx镜像的五层layer;
- 上文提到,layer元数据中的cache-id会索引到layer的实际文件,其路径为:/var/lib/docker/overlay2/<cache-id>/diff;
- U7ZXQ4ZL7TLD6XEBUVLR77LKS4指向的是容器启动时自动生成一层的以读写层cache-id-init命名的init-layer,其中的文件配置了容器的主机名,DNS服务等。
[root@localhost ~]# cd /var/lib/docker/overlay2/l
[root@localhost l]# ls ../bd47b78b55e951d504c6e70c0e7af451b020a5741f71ed0e91cb8f8dc77a8664/diff
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
[root@localhost l]# ls ../0363fcae3b4410c394b8a99e0a24d1ec01eb5198c82d3422f9c411ceaad98286/diff
docker-entrypoint.d etc lib tmp usr var
[root@localhost l]# ls ../dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4-init/diff
dev etc
[root@localhost l]# ls ../dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4-init/diff/etc
hostname hosts mtab resolv.conf
...
我们更加深刻地意识到,镜像层就是多个layer文件经过联合挂载后得到的文件系统。而M5NBJHQ4ICWANIEYBYMJ5FLCZO指向的目录(mount -t overlay命令输出的upperdir)便是读写层,也就是容器层的文件系统:
[root@localhost l]# ls ../dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4/diff
etc run var
overlay2将upper和lower的文件系统联合挂载,就得到了用户视角下的文件系统:
[root@localhost l]# ls ../dabd31fb6ad636b16b6f01f2332d068888de1e3e41a53751a35206e266b5dad4/merged
bin dev docker-entrypoint.sh home lib64 mnt proc run srv tmp var
boot docker-entrypoint.d etc
用户在容器中修改文件,其实只对读写层进行了变动,不会覆盖下层只读层的文件系统。读写层会把只读层的原始版本文件隐藏,所以用户发现文件系统已改变。而如果我们使用docker commit 命令生成新镜像,保存内容仅为读写层更新的文件。
容器信息
可以在/var/lib/docker/containers/容器ID目录下看到容器的相关信息:
[root@localhost ~]# tree /var/lib/docker/containers
/var/lib/docker/containers
└── 45f30cb6a063a7251db4388f17f85c1226d96277cb74693c1f38bef1d17b6193
├── 45f30cb6a063a7251db4388f17f85c1226d96277cb74693c1f38bef1d17b6193-json.log
├── checkpoints
├── config.v2.json
├── hostconfig.json
├── hostname
├── hosts
├── mounts
├── resolv.conf
└── resolv.conf.hash
3 directories, 7 files
目录下的config.v2.json文件描述了容器的详细配置信息,和使用docker inspect <container>命令对容器的查看结果是基本一致的:
cat /var/lib/docker/containers/45f30cb6a063a7*/config.v2.json | python -mjson.tool
还可以在hostname、hosts和resolv.conf文件中看到容器的主机名,DNS配置等,它们和init-layer文件系统中的内容一致。
[root@localhost ~]# cat /var/lib/docker/containers/45f30cb6a063a7*/hostname
45f30cb6a063
[root@localhost ~]# cat /var/lib/docker/containers/45f30cb6a063a7*/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2 45f30cb6a063
[root@localhost ~]# cat /var/lib/docker/containers/45f30cb6a063a7*/resolv.conf
# Generated by NetworkManager
nameserver 192.168.1.1
nameserver 192.168.0.1