findxc / blog

88 stars 5 forks source link

docker 的 layer, image, container 概念梳理 #57

Open findxc opened 3 years ago

findxc commented 3 years ago

本文是根据 About storage drivers | Docker Documentation 整理的。

layer 指层, image 指镜像, container 指容器。

镜像是由层组成的

在拉取镜像时,可以看到会按层去拉取:

5305C330-CEBE-465A-9BDD-7C5B2AD6B886

如果想查看某个镜像有哪些层,可以使用 docker inspect xxx 命令。 docker inspect node:14.16.1-alpine3.13 的部分内容如下。

镜像有哪些层:

镜像的一些配置参数,注意这里环境变量、 CMD 等也算是镜像的配置参数:

Dockerfile 中哪些命令会增加一层

除了只是修改镜像的配置参数的命令,其它命令都会增加一层。比如 ENV 、 CMD 、 LABEL 就不会增加一层,而 RUN 、 ADD 就会增加一层。

在构建镜像时其实也能看出来哪些命令会增加一层,比如使用下面这个 Dockerfile 来构建镜像。

FROM node:14.16.1-alpine3.13
WORKDIR /app

LABEL author="xxx"
ENV BUILD_ENV=dev

COPY ./hello.txt ./

# 追加内容到 hello.txt 末尾
RUN echo aaabbbcccddd>>hello.txt

06C9C862-F496-4042-B710-D281B6324101

镜像的层和层之间是什么关系

先得说一下 copy-on-write (CoW) 这个策略。维基上介绍如下:

写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

这个策略在构建镜像和创建容器时都有用到。这里我们先说镜像中的层和层。

如果某一层需要读取低层的文件,那就直接读就行了,不用把低层的文件拷贝到当前层,而如果是需要修改一个文件,这个文件在当前层不存在,是存在于低层的,那就先拷贝到当前层,然后修改当前层这个文件。

通过 docker history hello:v1 来查看层的大小,能发现确实就是这样的。 RUN echo aaabbbcccddd>>hello.txt 这一层由于需要修改 COPY ./hello.txt ./ 这一层的文件,就只有将文件拷贝过去再改,大小就是比上一层稍微大一点点。

8BEB3441-8EC8-491C-BC6B-68E78F0928F9

如果我们把最后一层改为 RUN cat hello.txt ,这样就只是读取文件,然后构建镜像后查看层大小,可以看到这一层大小就是 0 了,如下:

57D7AAF6-A34C-47CE-8152-0F0648C033C3

所以简单点理解,镜像的层包含的内容就是这一层文件相对于之前层的 diff 。

镜像和容器文件是由 docker 的 storage driver 来管理的,最常用的就是 overlay2 了,想去看更多细节见 Use the OverlayFS storage driver | Docker Documentation

以层为单位来构建镜像的好处

以层为单位可以更高效利用磁盘空间。比如我们一开始构建了镜像 m ,依次有 a , b , c 三层,然后我们再去构建镜像 n ,依次有 a , b , d 三层,那么 a 和 b 其实会使用同一份文件。

以层为单位可以提高缓存命中率。在构建/拉取/推送镜像时,如果某些层已经存在,就不用再构建/拉取/推送这一层了。比如刚才我们测试 RUN cat hello.txt 的时候:

224E5FD2-4D61-4999-8D88-2BAB49047305

那容器和镜像又是咋回事呢

一图胜千言,图片来自 About storage drivers | Docker Documentation

container-layers

基于 copy-on-write 策略,当镜像构建完之后,其实每一层都是只读了,这和镜像是无状态的不可变的这个观点是吻合的。

在创建容器时,其实就是在镜像基础上增加了 container layer ,这一层是可读可写的,容器运行时的临时性数据就放在这一层。

需要注意,删除容器时这一层也会跟着删除,所以需要持久化存储的数据,比如数据库文件,需要使用 volume 来挂载到容器中,这样文件就脱离容器生命周期了。(比如 docker run -v absolute_local_path:absolute_container_path

同样,由于 copy-on-write 策略,如果容器想修改镜像中某个文件,会先把文件拷贝到 container layer 然后再修改。

如果是基于一个容器启动多个镜像,其实也只是多增加了一些 container layer 。

sharing-layers

这里我们来说说镜像和容器占用的磁盘空间。

由于镜像和镜像之间可能存在相同的层,所以多个镜像占用的磁盘空间可能会小于镜像大小求和。

而对于容器,对于基于一个镜像启动多个容器的场景,其实是镜像大小加上多个容器各自 container layer 大小即可。(这里没考虑容器日志、 volume 设置,以及内存交互等也会增加容器大小)

通过 docker images 可以查看镜像大小。

通过 docker ps -s 可以查看容器大小。下面的 0B 是 container layer 大小,后面的 virtual 是镜像大小加上 container layer 大小。

下面是一个测试容器中的 copy-on-write 策略。如果我们只是启动容器,大小是 0B ,如果修改了一个位于镜像中的文件,由于会把这个文件拷贝到容器中,大小就变为 571B 了。

A62D8FF1-0AC6-47D5-8D7E-0D931CF22A10

jiqing112 commented 3 years ago

docker为什么节省资源

容器和分层技术,我们看到的只有几M的那个空间占用,是基于底层镜像之上做的新增的操作,每一个操作都是一层,叠加起来的隔离只是分层上的隔离,而不是把镜像拿来隔离,这是和虚拟机最大的不同。

首先要明白,Linux操作系统分别由两部分组成 1.内核空间(kernel) 2.用户空间(rootfs)

内核空间是kernel,Linux刚启动时会加载bootfs文件系统,之后bootf会被卸载掉,用户空间的文件系统是rootfs,包含常见的目录,如/dev、/proc、/bin、/etc等等

不同的Linux发行版本(红帽,centos,ubuntu等)主要的区别是rootfs, 多个Linux发行版本的kernel差别不大。因此通过docker pull centos命令下载镜像,实质上下载centos操作系统的rootfs,共用系统的kernel,所以docker下载的centos镜像大小只有200M。像是alpinelinux的docker镜像甚至只有几兆。

jiqing112 commented 3 years ago

共用kernel 区别rootfs 这两个特点有点类似lxc虚拟化,实际上早期的docker也是基于lxc。

后来docker 重写了使用lxc的那部分。

findxc commented 3 years ago

@jiqing112 谢谢补充,学习了,又去看了一下 比较 Docker 容器和虚拟机 ,我更理解了 ✌️

由于容器所需的资源要少得多(例如,它们不需要一个完整的 OS),所以它们易于部署且可快速启动。 这使你能够具有更高的密度,也就是说,这允许你在同一硬件单元上运行更多服务,从而降低了成本。