本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Docker 镜像原理
Docker 镜像
镜像是一种轻量级, 可执行的独立软件包, 用来打包软件运行环境和基于运行环境开发的软件, 它包含运行某个软件所需要的所有内容, 包括代码, 运行时, 库, 环境变量和配置文件.
UnionFS 联合文件系统
UnionFS 文件系统是一种分层, 轻量级, 并且高性能的文件系统. 它支持对文件系统的修改作为一次提交来一层层的叠加, 同时可以将不同目录挂载到同一个虚拟文件系统下.
Union 文件系统是 Docker 镜像的基础. 镜像可以通过分层来进行继承, 基于基础镜像, 可以制作各种具体应用的镜像.
特性 : 一次同时加载多个文件系统, 但从外面看起来, 只能看到一个文件系统, 联合记载会把各层文件系统叠加起来, 这样最终的文件系统会包含所有底层的文件和目录.
其实就和 Java 中的 子类继承抽象类一样,对于子类中没有重写的方法,会去使用抽象类中的实现逻辑.
Docker 镜像加载原理
Docker 的镜像实际上由一层一层的文件系统叠加而成, 这种层级的文件系统就是 UnionFS. 如下图所示:
bootFS ( boot file system ) 即引导文件系统, 主要包含 bootloader 和 kernel. Linux 刚启动时会加载 bootFS 文件系统, 而在 Docker 镜像的最底层就是 bootFS. 这与 linux 系统是一样的. 当 boot 加载完成之后, 整个内核就都在内存中了, 此时内存的使用权已经由 bootFS 提交给内核, 此时系统会卸载掉 bootFS. 以留出更多的内存供 initrd 磁盘镜像使用.
rootFS ( root file system ) 即 root 文件系统, 位于 bootFS 之上, 也就是说 Docker 镜像的第二层就是 rootFS. rootFS 包含的就是典型 linux 系统中的 /dev , /bin, /etc 等标准目录和文件. rootfs 就是各种不同的操作系统发行版, 比如 centos, unbuntu. 对于不同的 Linux 发行版, bootfs 基本是一致的, rootfs 会有所差别, 因此不同的发行版可以共用 bootfs.
在传统的 Linux 引导过程中, root 文件系统会最先以只读的方式加载, 当引导结束并完成了完整性检查之后, 它才会被切换为读写模式. 但是在 Docker 里, root 文件系统永远只能是只读状态, 并且 Docker 利用联合加载技术又会在 root 文件系统层上加载更多的只读文件系统.
Docker 将这样的文件系统称为镜像. 一个镜像可以放到另一个镜像的顶部. 位于下面的镜像称为父镜像(parent image), 可以依次类推, 直到镜像栈的最底部, 最底部的镜像称为基础镜像( base image ). 最后, 当从一个镜像启动容器时, Docker 会在该镜像的最顶层加载一个读写文件系统. 我们在 Docker 中运行的程序就是在这个读写层中执行的.
分层的镜像
Docker 支持通过扩展现有的镜像来创建新的镜像. 实际上, Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件而构建出来的.
比如我们现在构建一个新的镜像, 它的 Dockerfile 文件内容如下:
#① 表示该镜像以 Debian 镜像作为父镜像, 在其上进行构建.
FROM debian
#② 表示安装 emacs 软件.
RUN apt-get install emacs
#③ 表示安装 apache2 软件.
RUN apt-get install apache2
#④ 表示容器启动时运行 bash.
CMD ["/bin/basn"]
这个镜像的构建过程如下:
可以看到, 新镜像是从基础镜像 ( 此处是 Debin 镜像 ) 一层一层叠加生成的. 每安装一个软件, 就在现有镜像的基础上增加一层.
这时候, 你应该可以理解为什么 docker 上下载的 tomcat 居然要 400M+ ?
因为它下载下的是带有 centos , jdk 等等软件的 tomcat.
为什镜像要采用分层结构?
分层结构最大的一个好处就是共享资源.
大多数镜像都是从相同的基础镜像构建而来的, 那么宿主机上只需要保存一份基础镜像, 内存中也只需要加载一份基础镜像, 就可以为容器服务了.
可能就有人会问:如果多个容器共享一份基础镜像, 当某个容器修改了基础镜像的内容, 比如 /etc 下的文件, 这时其他容器的 /etc 是否也会被修改?
答案是不会!修改会被限制在单个容器内. 这是因为容器的 Copy-on-Write 特性.
容器的写时复制特性
特点 :
- 当容器启动时,
一个新的可写层被加载到镜像的顶部. 这一层通常被称为容器层, 容器层之下的都叫镜像层. - 所有对容器的改动 ,无论添加, 删除, 还是修改文件, 都
只会发生在容器层中. - Docker 镜像都是只读的.
只有容器层是可写的, 容器层下面的所有镜像层都是只读的.
容器层的细节 :
镜像层的数量可能会很多, 所有镜像层会联合在一起组成一个统一的文件系统. 如果不同层中存在一个相同路径的文件, 比如 /a, 上层镜像层的 /a 会覆盖下层镜像层的 /a, 也就是说用户只能访问到上层镜像层中的文件 /a. 在容器层中, 用户看到的是一个叠加之后的文件系统.
- 添加文件, 在容器中创建文件时, 新文件将被添加到容器层中.
- 读取文件, 在容器中读取某个文件时, Docker 会从上往下依次在各镜像层中查找此文件. 一旦找到, 立即将其
复制到容器层, 然后打开并读入内存. - 修改文件, 在容器中修改已存在的文件时, Docker 会从上往下依次在各镜像层中查找此文件. 一旦找到, 立即将其复制到容器层, 然后修改之. 该文件的原始只读版本依然存在其镜像层之中, 只不过是被容器层中的该文件的副本给隐藏掉了. 这样就可以实现不会修改共有镜像中的文件.
- 删除文件, 在容器中删除文件时, Docker 也是从上往下依次在镜像层中查找此文件. 找到后, 会在容器层中
记录一下这个文件被删除的标志.
结论 : 容器层保存的是镜像变化的部分, 不会对镜像本身进行任何修改. 正因为所有镜像层都是只读的, 不会被容器修改, 所以镜像可以被多个容器共享.
镜像的缓存特性
Docker 会缓存已有镜像的镜像层, 构建新镜像时, 如果某镜像层已经存在, 就直接使用, 无需重新创建.
如果我们希望在构建镜像时不使用缓存, 可以在 docker build 命令中加上 --no-cache 参数.
Dockerfile 中每一个指令都会创建一个镜像层, 上层镜像层是依赖于下层镜像层构建的. 无论什么时候, 只要某一层发生变化, 其上面所有层的缓存都会失效. 也就是说, 如果我们改变 Dockerfile 指令的执行顺序, 或者修改或添加指令, 都会使缓存失效.