一、镜像分层
1、什么是docker镜像
抛开docker镜像不谈,一个完整版Linux镜像(以Ubuntu:14.04镜像为例)是由一个Linux内核发行版(也就是某一个版本下的Linux内核) 和 Ubuntu系统发行版(不包含内核,但包含软件管理包,如apt-get管理包) 两部分组成。
Linux内核 和 系统发行版 是相互独立的,这意味着Ubuntu 14.04的内核从Linux 3.8 升级为 Linux 3.10,但Ubuntu的版本仍然是 14.04。除非是将apt-get中的软件包更新才会更新Ubuntu的版本,这些软件包可以在不同(更高)版本的Linux内核运行。
一个docker镜像是一个只包含操作系统(Centos/Ubuntu等)软件包而不包含操作系统内核的分层的联合文件系统(UnionFS),说直白些就是一系列的目录文件。
docker镜像包含镜像层文件内容 和 每一层的镜像json文件,该json文件配置了当前镜像层要运行什么命令和进程,配置什么环境变量等。
2、docker镜像和容器的关系
docker镜像的本质是一个静态的文件系统(即一系列目录组成的联合目录),容器本质则是由Docker守护进程(父进程)创建的一个或多个进程,是动态的。
docker镜像到docker容器的转化,本质上是docker守护进程读取docker镜像上json文件为容器配置相应环境,并运行json文件规定的进程,让这些进程运行在该容器内;
容器内的进程运行起来后,镜像的json文件就失去了作用,而镜像的文件系统则为容器内的进程提供了访问文件资源的环境,容器运行过程中对文件的操作和存储都会发生在镜像所提供的的文件系统中。
但问题就出现了,容器对文件的写操作是否会作用在镜像上,导致镜像内容发生改变?
答案是不会,要解答这个问题就需要介绍镜像分层的概念。
3、镜像分层
一个docker镜像是由一系列的镜像层(Image Layers)构建而成的联合文件系统(UnionFS)。
什么是联合文件系统?
就是把不同物理位置的目录合并到一个目录X中(通过mount命令),这样的目录X就是一个联合文件系统。
下面是一个创建联合目录的例子:
将两个文件夹进行联合挂载。命令中默认dirs后第一个文件夹为读写权限,之后的文件夹为只读权限。
我们分别修改读写层以及只读层的文件。
结果:针对读写层writeLayer的 writeFile 的修改,直接在源文件上生效;针对只读层readLayer的 readFile 的修改,其源文件readLayer/readFile的内容保持不变,系统将readFile文件拷贝到 writeLayer 目录后再做出修改。
docker镜像的每一个镜像层(Image Layer)本质就是一个个不同路径的物理文件夹,通过虚拟化技术组合成一个联合文件系统。每一层都包含一些文件。
当容器启动时,一个新的可写层被加载到镜像的顶部。docker容器就是在镜像之上添加一个可写层,也可以称为容器层。容器层以下的层都是只读层,也称为镜像层。多个容器实例(多个容器层)可以以只读的权限共享一个镜像所有镜像层的文件。
容器内可见的文件是所有只读层文件系统和可写层文件系统叠加而成的文件系统,对下层只读文件修改时,系统会拷贝一份该文件到容器层再修改,从而避免该修改影响到镜像。
同理,在容器层删除镜像层的文件时,只是在容器层记录该删除操作,而不会真正删除镜像层的文件。
这就解答了为什么容器使用镜像的文件系统,但容器对文件的写操作不会作用在镜像上的原因。
通过上面的描述我们知道,docker镜像使用了 copy-on-write 的机制,可以使得基于一个镜像创建出来的多个镜像和容器能共享磁盘空间。
例如我docker run 了5个基于ubuntu 18.04镜像(假设该镜像80M)的容器,并在5个容器中做出了不同的修改后,使得每个容器增加了10K内容,这5个容器共享1份镜像的磁盘空间。执行commit生成5个不同的 myUbuntu 镜像,这5个镜像会共享原ubuntu镜像的文件系统,系统不会将原镜像文件全部拷贝到新镜像中,而是只拷贝改变了的部分。因此磁盘空间只被多占用了50K,而非多400M。
综上:
1、基于同一镜像启动了两个容器,只会占用一份磁盘空间;
2、在容器内修改、删除和新建某文件,不会修改原镜像;
3、基于某镜像新建一个新镜像,无需将原镜像拷贝到新镜像中,而只拷贝修改了的部分;
docker镜像采用联合文件系统的最大好处是共享资源,节约磁盘空间。
4、容器的大小
docker ps -s指令可查看容器的大小
size表示每个容器的容器层占用大小。
virtual size表示容器的镜像层加容器层大小。一个镜像run出来的多个容器可以共享一份镜像层数据,多个容器在磁盘上占用的总大小等于所有容器的size之和 加上 一个镜像的大小 virtual size。
5、base 镜像
base镜像是不依赖其他任何镜像的镜像,能称为base 镜像的通常都是各种 Linux 最小安装发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。
例如执行:docker pull centos 就拉取了一个centos发行版的base镜像到本地。
其他镜像都是在base镜像基础上添加Image Layer镜像层建立而成的。例如一个redis镜像就是在base镜像上安装了redis并且在启动容器时会运行redis进程的镜像。
一个base镜像至少包含 bootfs + rootfs 两层。
bootfs (boot file system) 主要包含 bootloader 和 kernel,bootloader负责引导加载kernel,当boot成功后 kernel 被加载到内存中,而后 bootfs就从镜像的联合文件中被umount。
rootfs (root file system) 是用户空间文件系统包含的典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。
对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别,。不同的发行版可以共用bootfs。
所有容器都共用 host 的 kernel,在容器中没办法对 kernel 升级。
6、查看镜像层信息
--no-trunc 表示不要截断输出,要把所有层都展示出来。
详细镜像名包括镜像名字:版本号。
7、commit 容器
该命令会根据指定的容器生成一个镜像。
每一次commit都会将当前容器的容器层固化为一个新的镜像层,将新的镜像层 + 父类镜像的镜像层打包就得到commit命令生成的新镜像。也就是说,每commit一次,新镜像就会比父类镜像多一层,多出的这一层包含了用户对容器运行期间做出的更改(例如安装了新软件、更改了文件、开启新进程等)。
例如: