容器镜像概念
在容器出现之前,传统PaaS在一致面临应用打包以及跨环境部署的困局,Docker出现后,以一种创新的容器镜像的方法,实现了应用和所需环境的高效打包。镜像是一种轻量级的,可执行的独立软件包,用来打包软件运行环境和基于运行环境的开发软件,它包含运行某个软件做需要的所有的内容,包括代码,运行时,库,环境变量和配置文件。所有应用,直接打包docker镜像,就可以直接跑起来!,实现一次打包,任意部署。
容器镜像作为容器化应用的主要载体,也借鉴了git、github的一些思路,有容器仓库的概念,容器镜像在本地仓库或公共仓库中的表现的容器的静态视图,当容器从某个镜像启动之后是容器的动态视图。
最初关于容器镜像的讲述都集中在AUFS联合文件系统层面,容器镜像有多隔层组成,联合挂载,对容器呈现出一个完整的文件系统,称之为rootfs。正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。什么是容器的“一致性”呢?由于云端与本地服务器环境不同,应用的打包过程,一直是使用 PaaS 时最“痛苦”的一个步骤。但有了容器之后,更准确地说,有了容器镜像(即 rootfs)之后,这个问题被非常优雅地解决了。由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。事实上,对于大多数开发者而言,他们对应用依赖的理解,一直局限在编程语言层面。比如 Golang 的 Godeps.json。但实际上,一个一直以来很容易被忽视的事实是,对一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。有了容器镜像“打包操作系统”的能力,这个最基础的依赖环境也终于变成了应用沙盒的一部分。这就赋予了容器所谓的一致性:无论在本地、云端,还是在一台任何地方的机器上,用户只需要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。
这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。不过,这时你可能已经发现了另一个非常棘手的问题:难道我每开发一个应用,或者升级一下现有的应用,都要重复制作一次 rootfs 吗?比如,我现在用 Ubuntu 操作系统的 ISO 做了一个 rootfs,然后又在里面安装了 Java 环境,用来部署我的 Java 应用。那么,我的另一个同事在发布他的 Java 应用时,显然希望能够直接使用我安装过 Java 环境的 rootfs,而不是重复这个流程。一种比较直观的解决办法是,我在制作 rootfs 的时候,每做一步“有意义”的操作,就保存一个 rootfs 出来,这样其他同事就可以按需求去用他需要的 rootfs 了。但是,这个解决办法并不具备推广性。原因在于,一旦你的同事们修改了这个 rootfs,新旧两个 rootfs 之间就没有任何关系了。这样做的结果就是极度的碎片化。那么,既然这些修改都基于一个旧的 rootfs,我们能不能以增量的方式去做这些修改呢?这样做的好处是,所有人都只需要维护相对于 base rootfs 修改的增量内容,而不是每次修改都制造一个“fork”。答案当然是肯定的。这也正是为何,Docker 公司在实现 Docker 镜像时并没有沿用以前制作 rootfs 的标准流程,而是做了一个小小的创新:Docker 在镜像的设计中,引入了层(layer)的概念。而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。
当然,这个想法不是凭空臆造出来的,而是用到了一种叫作联合文件系统(Union File System)的能力。
容器镜像包括只读层、读写层等,以ubuntu:18.04这个容器为例,基础层就是只读层,包括ubuntu:18.04这个系统的所有文件(注意不包括内核),读写层就是一旦基于基础镜像run新容器,在新容器的视图中,基础层作为只读都从base image获取,每个容器都有自己修改的部分,这部分是叠加在基础层上的,对启动的容器来说是可读写的,所以修改都产生在这个层。随着docker版本的更新,现在最新的 docker 版本中默认都是使用的 overlay2。
理解镜像分层
为什么说是镜像分层技术,因为Docker 镜像是以层来组织的,我们可以通过命令 docker image inspect <image> 或者 docker inspect <image> 来查看镜像包含哪些层。下面是一个示例。
root@docker:~# docker image inspect ubuntu:18.04
...
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:824bf068fd3dc3ad967022f187d85250eb052f61fe158486b2df4e002f6f984e"
]
},
...
这时候,Docker 就会从 Docker Hub 上拉取一个 Ubuntu 镜像到本地。这个所谓的“镜像”,实际上就是一个 Ubuntu 操作系统的 rootfs,它的内容是 Ubuntu 操作系统的所有文件和目录。不过,与之前我们讲述的 rootfs 稍微不同的是,Docker 镜像使用的 rootfs,往往由多个“层”组成。其中 RootFS 就是镜像 ubuntu:18.04 的镜像层,只有一层,那么这层数据是存储在宿主机哪里的呢?好问题。动手实践的同学会在上面的输出中看到一个叫做 GraphDriver 的字段内容如下。
...
"GraphDriver": {
"Data": {
"MergedDir": "/var/lib/docker/overlay2/5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb/merged",
"UpperDir": "/var/lib/docker/overlay2/5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb/diff",
"WorkDir": "/var/lib/docker/overlay2/5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb/work"
},
"Name": "overlay2"
},
...
GraphDriver 负责镜像本地的管理和存储以及运行中的容器生成镜像等工作,可以将 GraphDriver 理解成镜像管理引擎,我们这里的例子对应的引擎名字是 overlay2(overlay 的优化版本)。除了 overlay 之外,Docker 的 GraphDriver 还支持 btrfs、aufs、devicemapper、vfs 等。
我们可以看到其中的 Data 包含了多个部分,这个对应 OverlayFS 的镜像组织形式,在下面我们再进行详细介绍。虽然我们上面的例子中的 busybox 镜像只有一层,但是正常情况下很多镜像都是由多层组成的。
这个时候很多同学应该会有这么一个疑问,镜像中的层都是读写的,那么我们运行着的容器的运行时数据是存储在哪里的呢?
镜像和容器在存储上的主要差别就在于容器多了一个读写层。镜像由多个只读层组成,通过镜像启动的容器在镜像之上加了一个读写层。下图是官方的一个配图。我们知道可以通过 docker commit 命令基于运行时的容器生成新的镜像,那么 commit 做的其中一个工作就是将读写层数据写入到新的镜像中。下图是一个示例图:
Container最上面是一个可写的容器层,以及若干只读的镜像层组成,Container的数据就存放在这些层中,这样的分层结构最大的特性是Copy-On-Write(写时复制):
1、新数据会直接存放在最上面的Container层。
2、修改现有的数据会先从Image层将数据复制到容器层,修改后的数据直接保存在Container层,Image层保持不变。
由此可以看出,每个步骤都将创建一个imgid, 一直追溯到我们的base镜像的id 。关于<missing>的部分,则不在本机上。
最后一列是每一层的大小。最后一层只是启动bash,所以没有文件变更,大小是0 。我们创建的镜像是在base镜像之上的,并不是完全复制一份base,然后修改,而是共享base的内容。这时候,如果我们新建一个新的镜像,同样也是共享base镜像。 那修改了base镜像,会不会导致我们创建的镜像也被修改呢? 不会!因为不允许修改历史镜像,只允许修改容器,而容器只可以在最上面的容器层进行 写和变更。
所有写入或者修改运行时容器的数据都会存储在读写层,当容器停止运行的时候,读写层的数据也会被同时删除掉。因为镜像层的数据是只读的,所有如果我们运行同一个镜像的多个容器副本,那么多个容器则可以共享同一份镜像存储层,下图是一个示例。
联合文件系统
Docker 的存储驱动的实现是基于 Union File System,简称 UnionFS,中文可以叫做联合文件系统。UnionFS 设计将其他文件系统联合到一个联合挂载点的文件系统服务。
所谓联合挂载技术,是指在同一个挂载点同时挂载多个文件系统,将挂载点的源目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录。
举个例子:比如我们运行一个 ubuntu 的容器。由于初始挂载时读写层为空,所以从用户的角度来看:该容器的文件系统与底层的 rootfs 没有区别;然而从内核角度来看,则是显式区分的两个层。
当需要修改镜像中的文件时,只对处于最上方的读写层进行改动,不会覆盖只读层文件系统的内容,只读层的原始文件内容依然存在,但是在容器内部会被读写层中的新版本文件内容隐藏。当 docker commit 时,读写层的内容则会被保存。
写时复制(Copy On Write)
这里顺便介绍一下写实复制技术。
我们知道 Linux 系统内核启动时首先挂载的 rootfs 是只读的,在系统正式工作之后,再将其切换为读写模式。Docker 容器启动时文件挂载类似 Linux 内核启动的方式,将 rootfs 设置为只读模式。不同之处在于:在挂载完成之后,利用上面提到的联合挂载技术在已有的只读 rootfs 上再挂载一个读写层。
读写层位于 Docker 容器文件系统的最上层,其下可能联合挂载多个只读层,只有在 Docker 容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层的老版本文件,这就叫做 写实复制,简称 CoW。
OverlayFS 联合文件系统的实现
OverlayFS 是类似 AUFS 的联合文件系统的一种实现,相比 AUFS 性能更好,实现更加简单。Docker 针对 OverlayFS 提供了两种存储驱动:overlay 和 overlay2 ,我们在前面的演示部分就是 overlay2。这两种驱动相比之下,overlay2 在 inode 使用率上更加高效,所以一般也是推荐 overlay2,Linux 内核版本要求是 4.0 或者更高版本。
查看docker是否使用layer2 FS
root@docker:~# docker info
Client:
Context: default
Debug Mode: false
Plugins:
app: Docker App (Docker Inc., v0.9.1-beta3)
buildx: Build with BuildKit (Docker Inc., v0.6.3-docker)
scan: Docker Scan (Docker Inc., v0.9.0)
Server:
Containers: 2
Running: 1
Paused: 0
Stopped: 1
Images: 2
Server Version: 20.10.11
Storage Driver: **overlay2**
Backing Filesystem: extfs
OverlayFS 将镜像层(只读)称为 lowerdir,将容器层(读写)称为 upperdir,最后联合挂载呈现出来的为 mergedir。文件层次结构可以用下图表示。 从图中我们也可以看出相比 AUFS,文件层更少,这也是 OverlayFS 相比 AUFS 性能更好的一个原因。
Overlay FS如何工作
当容器中发生数据修改时候overlayfs存储驱动又是如何进行工作的?以下将阐述其读写过程:
读:
如果文件在容器层(upperdir),直接读取文件;
如果文件不在容器层(upperdir),则从镜像层(lowerdir)读取;
修改:
首次写入: 如果在upperdir中不存在,overlay和overlay2执行copy_up操作,把文件从lowdir拷贝到upperdir,由于overlayfs是文件级别的(即使文件只有很少的一点修改,也会产生的copy_up的行为),后续对同一文件的在此写入操作将对已经复制到容器的文件的副本进行操作。这也就是常常说的写时复制(copy-on-write)
删除文件和目录: 当文件在容器被删除时,在容器层(upperdir)创建whiteout文件,镜像层(lowerdir)的文件是不会被删除的,因为他们是只读的,但without文件会阻止他们显示,当目录在容器内被删除时,在容器层(upperdir)一个不透明的目录,这个和上面whiteout原理一样,阻止用户继续访问,即便镜像层仍然存在。
注意事项
copy_up操作只发生在文件首次写入,以后都是只修改副本,
overlayfs只适用两层目录,,相比于比AUFS,查找搜索都更快。
容器层的文件删除只是一个“障眼法”,是靠whiteout文件将其遮挡,image层并没有删除,这也就是为什么使用docker commit 提交保存的镜像会越来越大,无论在容器层怎么删除数据,image层都不会改变。
下图显示了 在Docker 中镜像和 容器是如何通过OverlayFS分层与互相构造的映射。图像层是lowerdir,容器层是upperdir。统一视图合并到merged目录,该目录实际上是容器安装点。
我在本地ubuntu系统上运行了两个ubuntu容器。
root@docker:~# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
23a10d6c5273 ubuntu:18.04 "bash" About an hour ago Exited (0) About an hour ago epic_neumann
e3713b4f6370 ubuntu:18.04 "/bin/bash" 2 hours ago Up About an hour condescending_almeida
我们第一个正在运行的容器[id为23a10d6c5273]的inspect。
root@docker:~# docker inspect 23a10d6c5273
...
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979-init/diff:/var/lib/docker/overlay2/5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb/diff",
"MergedDir": "/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979/merged",
"UpperDir": "/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979/diff",
"WorkDir": "/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979/work"
},
"Name": "overlay2"
...
可以看到基础的image位于Lowerdir中,进去查看一下,这里边的文件就是基本的ubuntu系统文件,当然我在容器中的修改操作这里看不到。
root@docker:~# cd /var/lib/docker/overlay2/5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb/diff/
root@docker:/var/lib/docker/overlay2/5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb/diff# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
这个容器中,我还确实创建了一个名为vmware的目录,这个目录在容器内可见,在联合文件系统中什么位置呢(一定猜到了在UpperDir中)?
root@docker:~ docker exec -it 23a10d6c5273 /bin/bash
交互进入了容器中
root@23a10d6c5273:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var **vmware**
此时我们进入Upperdir,可以看到vmware文件夹在这一层修改。
root@docker:~# cd /var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979/diff
root@docker:/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979/diff# tree .
.
├── root
└── vmware
2 directories, 0 files
root@docker:/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979/diff#
同时,我们进入第二个容器[id为e3713b4f6370],查看到这个容器的LowerDir也指向5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb/diff,说明上述两个运行的容器的base image是完全相同的ubuntu系统,共用一个基础的ubuntu容器镜像,而每个容器修改的部分,都放在各自的UpperDir目录了。
root@docker:~# docker inspect e3713b4f6370
...
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/6bc192e053e9020fec915c8784ba22b1a6cec58448a7473ce895433d70c61d0f-init/diff:/var/lib/docker/overlay2/5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb/diff",
"MergedDir": "/var/lib/docker/overlay2/6bc192e053e9020fec915c8784ba22b1a6cec58448a7473ce895433d70c61d0f/merged",
"UpperDir": "/var/lib/docker/overlay2/6bc192e053e9020fec915c8784ba22b1a6cec58448a7473ce895433d70c61d0f/diff",
"WorkDir": "/var/lib/docker/overlay2/6bc192e053e9020fec915c8784ba22b1a6cec58448a7473ce895433d70c61d0f/work"
},
"Name": "overlay2"
...
最后,介于只读层和读写层中间是init层,init层是以一个uuid+-init结尾表示,夹在只读层和读写层之间,作用是专门存放/etc/hosts、/etc/resolv.conf等信息,需要这一层的原因是当容器启动时候,这些本该属于image层的文件或目录,比如hostname,用户需要修改,但是image层又不允许修改,所以启动时候通过单独挂载一层init层,通过修改init层中的文件达到修改这些文件目的。而这些修改往往只读当前容器生效,而在docker commit提交为镜像时候,并不会将init层提交。该层文件存放的目录为"/var/lib/docker/overlay2/<init_id>/diff"
root@docker:~# cd /var/lib/docker/overlay2
root@docker:/var/lib/docker/overlay2# ls -al
total 36
drwx--x--- 9 root root 4096 Dec 5 13:49 .
drwx--x--- 13 root root 4096 Dec 2 15:41 ..
drwx--x--- 3 root root 4096 Dec 5 13:06 5928f98f1e1ad2961572d2f38f5ce251b9beaf37c3097e3e425f015186f61beb
drwx--x--- 5 root root 4096 Dec 5 13:46 6bc192e053e9020fec915c8784ba22b1a6cec58448a7473ce895433d70c61d0f
drwx--x--- 4 root root 4096 Dec 5 13:06 6bc192e053e9020fec915c8784ba22b1a6cec58448a7473ce895433d70c61d0f-init
drwx--x--- 3 root root 4096 Dec 5 12:59 d5d3a7e76727ac47c2cb1e32663038d9acd6b14d0d7940f19445e97d3275169f
drwx--x--- 5 root root 4096 Dec 5 15:03 f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979
drwx--x--- 4 root root 4096 Dec 5 13:49 f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979-init
drwx------ 2 root root 4096 Dec 5 13:49 l
root@docker:/var/lib/docker/overlay2# cd f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979-init/diff/
root@docker:/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979-init/diff# ls
dev etc
root@docker:/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979-init/diff# cd etc
root@docker:/var/lib/docker/overlay2/f223f9e0792cfeafd71b2997f801434925e0f09a0a9a0c52e4a2088c42955979-init/diff/etc# ls
hostname hosts mtab resolv.conf
通过以上的内容介绍,一个容器完整的层应由三个部分组成,如下图:
镜像层:也称为rootfs,提供容器启动的文件系统
init层: 用于修改容器中一些文件如/etc/hostname、/etc/resolv.conf等
容器层:使用联合挂载统一给用户提供的可读写目录。
暂无评论