docker基础-镜像
docker公司在实现docker镜像时并没有沿用以前只做roots的标准流程,而是做了一个小小的创新:
docker在镜像的设计中引入了层(layer)的概念,也就是说,用户制作镜像的每一步操作,都会生成
一个层,也就是一个增量的roofs。
当然,这个想法不是凭空臆造出来的,而是用到了一种叫做联合文件系统(union file system)的能力。
union file system 也叫unionfs,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下,比如,我限制有两个目录A和B,它们分别有两个文件。
# tree
.
├── A
│ ├── a
│ └── x
└── B
├── b
└── x
然后使用联合挂载的方式,将这两个目录挂在到一个公共的目录C上。
# mkdir C
# mount -t aufs -o dirs=./A:./B none ./C
这时,我再查看目录C目录的内容,就能看到目录A和B下的文件被合并到了一起.
tree ./C
./C
├── a
├── b
└── x
可以看到,在合并后的C目录里,有a、b、x三个文件,并且x文件只有一份,这就是“合并”的含义,此外,如果你在目录C里对a、b、x文件做修改,这些修改也会在对应的目录A、B中生效。
那么,在docker项目中,又是如何使用这种unionfs呢?
我的环境是ubuntu 和docker ce,这对组合牧人使用的是auFS这个联合文件系统的实现,可以通过命令docker info查看dockre信息。
aufs的全程是another unionfs,后来改名alternative unionfs,再后来又改名advance unionfs,从这些名字里可以看出两个事实:
1、它是对Linux原生unionfs的重写和改进;
2、它的作者怨气应该很大,猜想应该是Linus Torvalds(Linux 之父)一直不让 aufs进入Linux内核主干的缘故,所以我们只能在ubuntu和debian这些发行版本上使用。
对于aufs来说,它最关键的目录结构在/var/lib/docker路径下的diff目录:
/var/lib/docker/aufs/diff/<layer_id>
而这个目录的作用,我们不妨通过一个例子来看下:
现在,我启动一个容器,比如:
docker run -d ubuntu:latest sleep 3600
这时候,docker就会从docker hub上或者你指定的镜像仓库进行pull镜像到本地的系统上。
这个所谓的“镜像”,实际上就是一个Linux操作系统的rootfs,它的内容是Linux操作系统的所有文件和目录,不过,与之前讲述的rootfs稍有不同的是docker镜像使用的rootfs,往往由多个“层”组成:
# docker image inspect ubuntu:latest
...
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:f49017d4d5ce9c0f544c...",
"sha256:8f2b771487e9d6354080...",
"sha256:ccd4d61916aaa2159429...",
"sha256:c01d74f99de40e097c73...",
"sha256:268a067217b5fe78e000..."
]
}
不出意外的,这个目录里面正式一个完整的ubuntu操作系统
# ls /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
镜像分为5个层挂载到一个完成的ubuntu文件系统
这个信息记录在aufs的系统目录/sys/fs/aufs下面。
首先,通过查看aufs的挂载信息,我们可以找到这个目录对应的aufs的内部ID(也叫si):
# cat /proc/mounts| grep aufs
none /var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufs rw,relatime,si=972c6d361e6b32ba,dio,dirperm1 0 0
即,si=972c6d361e6b32ba
然后使用这个ID,你就可以在/sys/fs/aufs下查看被联合挂载在一起的各个层的信息:
cat /sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw
/var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh
/var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh
/var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh
/var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh
/var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh
/var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh
从这些信息里,我们可以看到,镜像的层都放置在/var/lib/docker/aufs/diff目录下,然后被联合挂载在/var/lib/docker/aufs/mnt里面。
而且,从下面的结构可以看出,这个容器的rootfs由三部分组成:
第一部分,只读层。
它是这个容器的rootfs最下面的5层,对应的正式ubuntu:latest镜像的5层,可以看到,它们的挂载方式都是只读的(ro+wh,即readonly+whiteout)
这时,我们可以分别查看一下这些层的内容:
ls /var/lib/docker/aufs/diff/72b0744e06247c7d0...
etc sbin usr var
$ ls /var/lib/docker/aufs/diff/32e8e20064858c0f2...
run
$ ls /var/lib/docker/aufs/diff/a524a729adadedb900...
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
可以看到,这些层,都以增量的方式分别包含了ubuntu操作系统的一部分。
第二部分,可读写层。
它是这个容器的rootfs最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,可读写,在没有卸任文件之前,这个目录是空的,而一旦在容器里做了写操作,修改产生的内容就会以增量的方式出现在这个层中。
可是,有没有想过一个问题,如果现在删除只读里的一个文件?
为了实现这样的操作,aufs会在可读写层新建一个whiteout文件,把只读层里的文件“遮挡”起来。
比如,你要删除只读层里一个叫test的文件,那么这个删除操作时机上是在可读写层创建一个名叫.wh.test的文件,这样当两个层被联合挂载之后,test文件就会被.wh.test文件“遮挡”起来,“消失”了,这个功能就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。
所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。
第三部分,init层
它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。
需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。
可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。
所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。
最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。