什么是 docker 镜像
docker 镜像是一个只读的 docker 容器模板,含有启动 docker 容器所需的文件系统结构及其内容,因此是启动一个 docker 容器的基础。
docker 镜像的文件内容以及一些运行 docker 容器的配置文件组成了 docker 容器的静态文件系统运行环境:rootfs
。可以这么理解,docker 镜像是 docker 容器的静态视角,docker 容器是 docker 镜像的运行状态。我们可以通过下图来理解 docker daemon、docker镜像以及 docker 容器三者的关系:
从上图中我们可以看到,当由 ubuntu:14.04 镜像启动容器时,ubuntu:14.04 镜像的镜像层内容将作为容器的 rootfs;而 ubuntu:14.04 镜像的 json 文件,会由 docker daemon 解析,并提取出其中的容器执行入口 CMD 信息,以及容器进程的环境变量 ENV 信息,最终初始化容器进程。当然,容器进程的执行入口来源于镜像提供的 rootfs
。
Docker镜像的核心原理一:镜像的存储和管理方式
镜像的设计
一个镜像都由多个镜像层组成。
为了区别镜像层,Docker为每个镜像层都计算了UUID,根据镜像层中的数据使用加密哈希算法生成UUID。所有镜像层和容器层都保存在宿主机的文件系统/var/lib/docker/中,由存储驱动进行管理。在下载镜像时,Docker Daemon会检查镜像中的镜像层与宿主机文件系统中的镜像层进行对比,如果存在则不下载,只下载不存在的镜像层。
栈层式管理镜像层
docker 中存储驱动用于管理镜像层和容器层。不同的存储驱动使用不同的算法和管理方式。在容器和镜像管理中,使用的两大技术是栈式层管理和写时复制。Dockerfile中的每一条指令都会对应于Docker镜像中的一层,因此在docker build完毕之后,镜像的总大小将等于每一层镜像的大小总和。每个镜像都由多个镜像层组成,从下往上以栈的方式组合在一起形成容器的根文件系统,Docker的存储驱动用于管理这些镜像层,对外提供单一的文件系统。Docker 的镜像实际上由一层一层的文件系统组成,这种层级的文件系统叫 UnionFS(联合文件系统)。
由于联合文件系统的存在,容器文件系统内容的大小不等于Docker镜像大小。
由于联合文件系统的存在,如果镜像层是相同的,则不同的镜像会共享该层。
如上图中,镜像A与镜像B就共享第二层镜像,使得A+B镜像文件大小并不等于A+B镜像占用宿主机存储空间容量的大小。docker images
命令列出的镜像体积总和并不能代表实际使用的磁盘空间,需要使用docker system df
命令来代替。
UnionFS (联合文件系统)
联合文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。
分层的原因:
- 分层最大的一个好处就是共享资源
- 有多个镜像都从相同的base镜像构建而来,那么宿主机只需在磁盘上保存一份base镜像;
- 同时内存中也只需加载一份base镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享。
联合文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。
通俗地讲,联合挂载技术可以在一个挂载点同时挂载多个文件系统,将挂载点的原目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录。实现这种联合挂载技术的文件系统通常被称为联合文件系统(union filesystem)
。以下图所示的运行 Ubuntu:14.04 镜像后的容器中的 aufs 文件系统为例:
Docker镜像的核心原理二:镜像结构
容器镜像和操作系统镜像
- 容器镜像:
- 分层结构:容器镜像是由多个只读层组成的。每一层通常对应一次文件系统的改动,如安装软件包或者配置文件的更改。Docker等容器管理工具利用这些分层来实现镜像的高效性和可复用性,因为共同的基础层可以在多个容器之间共享。
- 轻量级:容器镜像常常只包含运行应用程序所需要的最小依赖集,没有完整的操作系统内核。这使得它们启动迅速,占用资源少。
- 便携性:镜像一次创建,可以在任何支持的容器平台上运行而不需要修改。
- 操作系统:
- 整体结构:操作系统是一个包含内核、系统库、系统工具和用户接口等多个组件的完整软件系统。它负责管理硬件资源、提供底层服务和运行应用程序。
- 内核层:从分层角度来看,操作系统的底部是内核层,它直接与硬件交互,提供进程管理、内存管理和设备驱动等功能。
- 用户层和系统工具:在内核之上是用户空间,这里有各种系统库和工具集成,为应用程序提供丰富的API和运行环境。
- 重量级:通常较为庞大,启动时间长,占用更多系统资源。
从整体来看,容器镜像依赖于主机系统的内核,共享一个操作系统环境,通过镜像的分层技术实现高效性。而操作系统从内核到用户应用提供了一个全功能的平台,用于运行各种应用程序和服务。容器技术实际上是在操作系统之上加了一层抽象,使应用程序的部署和管理更为灵活和高效。
要了解docker的镜像结构,需要先对linux的文件系统结构有所了解。
linux文件系统结构
Linux 文件系统由 bootfs
和 rootfs
两部分组成。
bootfs(boot file system) 主要包含 bootloader 和 kernel,bootloader 主要是引导加载 kernel(内核),当 kernel 被加载到内存中后 bootfs 就被 umount (卸载)了。
rootfs (root file system) 包含的就是典型 Linux 系统中的 /dev,/proc,/bin,/etc 等标准目录和文件。rootfs就是各种Linux发行版。比如redcat、centOS。
docker镜像结构
docker的分层镜像结构如图所示,镜像的最底层必须是一个启动文件系统(bootfs
)的镜像层。bootfs
的上层镜像称为根镜像(rootfs
)或者基础镜像(Base Image
),它一般是操作系统,比如centos、debian或者Ubuntu。在传统的 Linux 操作系统内核启动时,首先挂载一个只读的 rootfs,当系统检测其完整性之后,再将其切换为读写模式。而在 docker 架构中,当 docker daemon 为 docker 容器挂载 rootfs 时,沿用了 Linux 内核启动时的做法,即将 rootfs 设为只读模式。在挂载完毕之后,利用联合挂载(union mount)技术在已有的只读 rootfs 上再挂载一个读写层。这样,可读写的层处于 docker 容器文件系统的最顶层,其下可能联合挂载了多个只读的层,只有在 docker 容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的旧版本文件。
用户的镜像必须构建在基础镜像之上。如图所示, emacs镜像层就是在基础镜像上安装emacs创建出来的镜像,在此基础上安装apache又创建了新的镜像层。利用这个新的镜像层启动的容器里运行的是一个已经安装好emacs和apache的Debian系统。
docker镜像分层的理解
docker 镜像是采用分层的方式构建的,每个镜像都由一系列的 “镜像层” 组成。分层结构是 docker 镜像如此轻量的重要原因。当需要修改容器镜像内的某个文件时,只对处于最上方的读写层进行变动,不覆写下层已有文件系统的内容,已有文件在只读层中的原始版本仍然存在,但会被读写层中的新版本所隐藏。当使用 docker commit
提交这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。分层达到了在不的容器同镜像之间共享镜像层的效果。
查看镜像分层方式可以通过docker image inspect [IMAGE]
命令。其中RootFS部分则是表示了分层信息。
所有的Docker镜像都起始于一个基础镜像层,当镜像修改或者新增新的内容时,就会在当前镜像层之上,创建新的镜像层。即在添加额外的镜像层的同时,镜像始终保持是当前所有镜像的组合。docker通过存储引擎(新版本采用快照机制)的方式实现镜像层堆栈,并保证多个镜像层对外展示为统一的文件系统。示例:
这个镜像中包含了三个镜像层,第一层有三个文件,第二层也有三个文件,第三层镜像中仅有一个文件,且这个文件是对第二层镜像中的文件5的一个更新版本。在这种情况下,上层镜像层中的文件会覆盖底层镜像层的文件,这样就使得文件的更新版本作为一个新的镜像层添加到镜像当中。
最后docker通过存储引擎将所有镜像层堆叠并合并,对外提供统一的视图。
Dockerfile中的操作对于镜像分层的影响:在镜像构建过程中需要向镜像写入数据的时候会产生分层,一个写操作指令产生一个分层。
综合考虑镜像的层级结构,以及 volume
、init-layer
、可读写层这些概念,一个完整的、在运行的容器的所有文件系统结构可以用下图来描述:
从图中我们不难看到,除了 echo hello 进程所在的 cgroups 和 namespace 环境之外,容器文件系统其实是一个相对独立的组织。可读写部分(read-write layer 以及 volumes)、init-layer、只读层(read-only layer) 这 3 部分结构共同组成了一个容器所需的下层文件系统,它们通过联合挂载的方式巧妙地表现为一层,使得容器进程对这些层的存在一无所知。
Docker镜像的核心原理三:写时复制策略(Copy On Write)
当某个容器修改了基础镜像的内容,比如 /bin文件夹下的文件,这时其他容器的/bin文件夹是否会发生变化呢?答案是不会的。根据容器镜像的写时复制(Copy-on-Write)技术,某个容器对基础镜像的修改会被限制在单个容器内。
写时复制策略采用了共享和复制技术,针对相同的数据系统只保留一份,所有操作都访问这一份数据。当有操作需要修改或添加数据时,操作系统会把这部分数据复制到新的地方再进行修改或添加,而其他操作仍然访问原数据区数据,这项技术节约了镜像的存储空间,加快了系统启动时间。
如图所示,当需要对镜像中的文件进行修改时,会将文件复制到容器层进行修改,上层文件会覆盖原始镜像文件。注意:该文件存在于容器层,容器重启之后容器层重新建立,上一次容器运行时对于文件的修改全部丢失!
只有当需要修改时才复制一份数据,这种特性被称作Copy-on-Write
。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。
Docker镜像的核心原理四:内容寻址原理
在docker中,内容寻址就是根据文件内容来索引对应的镜像和镜像层,实际上就是对于镜像层的内容计算和校验后生成一个内容哈希值,并作为这个镜像层的唯一ID。在构造镜像时,根据这个ID来索引镜像层。
在 docker 1.10 版本后,docker 镜像改动较大,其中最重要的特性便是引入了内容寻址存储(content-addressable storage)
的机制,根据文件的内容来索引镜像和镜像层。与之前版本对每个镜像层随机生成一个 UUID 不同,新模型对镜像层的内容计算校验和,生成一个内容哈希值,并以此哈希值代替之前的 UUID 作为镜像层的唯一标识。该机制主要提高了镜像的安全性,并在 pull、push、load 和 save 操作后检测数据的完整性。另外,基于内容哈希来索引镜像层,在一定程度上减少了 ID 的冲突并且增强了镜像层的共享。对于来自不同构建的镜像层,主要拥有相同的内容哈希,也能被不同的镜像共享。
发布者:LJH,转发请注明出处:https://www.ljh.cool/39443.html