Docker 镜像分层机制详解
2023-08-09 14:53:19 # Tools # Docker

1. base 镜像

我们在打包项目时,实际上往往需要一个基本的操作系统环境,这样我们才可以在这个操作系统上安装各种依赖软件,比如数据库、缓存等,像这种基本的系统镜像,我们称为base镜像

base 镜像有两层含义:(1)不依赖其他镜像,从 scratch 构建;(2)其他镜像可以之为基础进行扩展。

所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。

image-20230405172002741

可以看到,CentOS的base镜像下载完成后,不像我们使用完整系统一样,base镜像的CentOS省去了内核,所以大小只有231M

1.1 base 镜像机制

Linux 操作系统由内核空间和用户空间组成。

典型的Linux启动到运行需要两个FS,bootfs + rootfs,如下图:

image-20220701133111829

Linux启动后首先会加载 bootfs 文件系统,加载完成后会自动卸载掉,之后会加载用户空间的文件系统,这一层是我们自己可以进行操作的部分:

  • bootfs 包含了 BootLoader 和 Linux 内核,用户是不能对这层作任何修改的,在内核启动之后,bootfs 会自动卸载。
  • rootfs 则包含了系统上的常见的目录结构,包括/dev/proc/bin等等以及一些基本的文件和命令,也就是我们进入系统之后能够操作的整个文件系统,包括我们在Ubuntu下使用的apt和CentOS下使用的yum,都是用户空间上的。
  • 由此可见对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs

base 镜像底层会直接使用宿主主机的内核

  • 也就是说你的宿主机内核版本是多少,base镜像中的 CentOS 内核版本就是多少
  • 因此 base 镜像只需要提供 rootfs 即可,而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了,某些操作系统的base镜像甚至都不到10M。
宿主机 容器
image-20230405172717255 image-20230405172808799

可以看到内核版本是一样的(这也是缺点所在,如果软件对内核版本有要求的话,那么此时使用 Docker 就不如使用虚拟机)

2. 镜像分层

2.1 创建测试镜像

Docker Image 如何而来呢?

  • 简单来说,一个 Image 是通过一个 DockerFile 定义的,然后使用 docker build 命令构建它

  • DockerFile 中的每一条命令的执行结果都会成为 Image 中的一个 Layer

这里,我们通过 Build 一个简单的镜像,来观察 Image 的分层机制:

FROM alpine:3.15.0
RUN dd if=/dev/zero of=file1 bs=10M count=1 #添加一个10M的文件file1
RUN dd if=/dev/zero of=file2 bs=10M count=1 #添加一个10M的文件file2

构建结果:

image-20230405170117559

通过构建结果可以看出,构建的过程就是执行 Dockerfile 文件中我们写入的命令。

如果 Dockerfile 中的内容没有变动,那么相应的镜像在 build 的时候会复用之前的 Layer,以便提升构建效率。并且,即使文件内容有修改,那也只会重新 build 修改的 Layer,其他未修改的也仍然会复用。

image-20230405170536609

2.2 查看镜像

有 2 种方法查看镜像:

  1. 使用docker inspect:获取镜像的元数据
  2. 使用docker history:查看镜像的构建历史

docker inspect

使用 docker inspect 查看镜像的元数据。
其中Parent可以看到父镜像, Layers这一项下面可以看到镜像的所有层。

image-20230405171059129

docker history

使用docker history可以看到镜像的构建历史。
每一行列出了镜像包含的层。

image-20230405171130593

3. Copy-on-Write

为什么 Docker 镜像要采用这种分层结构呢?

  • 共享资源

    • 比如有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;
    • 同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。

    • 而且镜像的每一层都可以被共享

如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改?

  • 答案是不会!修改会被限制在单个容器内。这就是容器的 Copy-on-Write 特性。

Copy-on-write是一种提高文件共享和复制效率的策略。

  • 如果一个文件和目录在低一层的镜像层中存在,并且其它层想要读取这个文件,就直接使用这个文件。

    • 如果多个层中有命名相同的文件,用户只能看到最上面那层中的文件。
  • 新数据会直接存放在最上面的容器层。

  • 修改现有数据会先从镜像层将数据复制到容器层,修改后的数据直接保存在容器层中,镜像层保持不变。

4. 可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。

image-20230405173314451

所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。

只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。