Docker镜像管理
- rootfs
- 主要特点
- 分层
- 写时复制
- 内容寻址
- 联合挂载
- 关键概念
- registry
- repository
- manifest
- image和layer
- 镜像构建
- commit镜像
- build构建镜像
- 镜像分发
- Docker镜像是一个只读的容器模板,含有启动Docker容器所需的文件系统结构及其内容,因此是启动一个Docker容器的基础。Docker镜像的文件内容以及一些运行Docker容器的配置文件组成了Docker容器的静态文件系统运行环境 — \text{---} —rootfs。可以理解为Docker镜像是Docker容器的静态视角,Docker容器是Docker镜像的运行状态
rootfs
- rootfs是Docker容器在启动时内部进程可见的文件系统,即Docker容器的根目录。rootfs通常包含一个操作系统运行所需的文件系统,例如可能包含典型的类Unix操作系统中的目录系统,如 / d e v , / p r o c , / b i n , / e t c , / l i b , / u s r , / t m p /dev,/proc,/bin,/etc,/lib,/usr,/tmp /dev,/proc,/bin,/etc,/lib,/usr,/tmp以及运行Docker容器所需的配置文件、工具等
- 传统的Linux操作系统内核启动时,首先挂载一个只读 ( r e a d − o n l y ) (read-only) (read−only)的rootfs,当系统检测其完整性之后,再将其切换为读写 ( r e a d − w r i t e ) (read-write) (read−write)模式。而在Docker架构中,当Docker daemon为Docker容器挂载rootfs时,沿用了Linux内核启动的方法,即将rootfs设为只读模式。在挂载完毕之后,利用联合挂载(union mount)技术在已有的只读rootfs上再挂载一个读写层,只有在Docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到读写层,并隐藏只读层中的老版本文件
主要特点
分层
- Docker镜像时采用分层的方式构建的,每个镜像都由一系列的“镜像层”组成。分层结构是Docker镜像如此轻量的主要原因,当需要修改容器镜像内的某个文件时,只对处于最上方的读写层进行变动,不覆写下层已有文件系统的内容,已有文件在只读层中的原始版本仍然存在,但会被读写层中的新版文件所隐藏。当使用
docker commit
提交这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。这样的分层实现了在不同镜像之间共享镜像层的效果
写时复制
- Docker镜像使用了写时复制(copy-on-write)的策略,在多个容器之间共享镜像,每个容器在启动的时候并不需要单独复制一份镜像文件,而是将所有镜像层以只读的方式挂载到一个挂载点,再在上面覆盖一个可读写的文件层。在未更改文件内容时,所有容器共享同一份数据,只有在Docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的老版本文件。写时复制配合分层机制减少了镜像对磁盘空间的占用和容器启动时间
内容寻址
- 根据文件内容来索引镜像和镜像层,根据镜像层的内容计算一个哈希值,作为该镜像层的唯一标识,这样还可以在pull、push、load、save等操作之后通过该值检测数据的完整性。对于来自不同构建的镜像层,着急要拥有相同的内容哈希,也能被不同的镜像共享
联合挂载
- 联合挂载技术可以在一个挂载点同时挂载多个文件系统,将挂载点的原目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录。实现这种联合挂载技术的文件系统通常被称为联合文件系统(union filesystem)。如下图所示
- 初始挂载的时候,读写层为空,所以从用户视角看,该容器的文件系统与底层的rootfs没有差别;然而从内核的角度来看,则是显式区分开的两个层次。当修改镜像内的某个文件时,只对处于最上方的读写层进行了变动,不覆写下层已有文件系统的内容,已有文件在只读层中的原始版本仍然存在,但会被读写层中的新版文件所隐藏。当
docker commit
这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过的文件。联合挂载是用于将多个镜像层的文件系统挂载到同一个挂载点来实现一个统一文件系统视图的途径,是下层存储驱动(如aufs、overlay等)实现分层合并的方式,所以严格来说,联合挂载并不是Docker镜像的必须技术,也就是说,文件系统分层的效果也可以通过其他手段来实现,例如快照等
关键概念
registry
- registry用以保存Docker镜像,其中还包括镜像层次结构和关于镜像的元数据。可以将registry简单的想象成类似于Git仓库之类的实体
- 用户可以在自己的数据中心搭建私有的registry,也可以使用Docker官方的功用registry 服务,即Docker Hub。它是由Docker公司维护的一个公共镜像仓库,供用户下载使用。Docker Hub中有两种类型的仓库,即用户仓库(user repository)和顶层仓库(top-level repository)。用户仓库由普通的Docker Hub用户创建,顶层仓库则由Docker公司负责维护,提供官方版本镜像。理论上顶层仓库中的镜像经过Docker公司验证,被认为是架构良好且安全的
repository
- repository即由具有某个功能的Docker镜像的所有迭代版本构成的镜像组。registry由一系列经过命名的repository组成,repository通过命名规范对用户仓库和顶层仓库进行组织。用户仓库的命名由用户名和repository名两部分组成,中间以"/"隔开,即
username/repository_name
的形式。registry是repository的集合,repository是镜像的集合,其中包含了多个不同版本的镜像,使用标签进行版本区分。例如对于ubuntu:14.04、ubutu:12.04
都属于ubuntu这个repository
manifest
- manifest(描述文件)主要存在于registry中作为Docker镜像的元数据文件,在pull、push、save和load中作为镜像结构和基础信息的描述文件。在镜像被pull或者load到Docker宿主机时,manifest被转化为本地的镜像配置文件config。新版本(v2、schema 2)的manifest list可以组合不同架构实现同名Docker镜像的manifest,用以支持多架构Docker镜像
image和layer
- Docker内部的image概念是用来存储一组镜像相关的元数据信息,主要包括镜像的架构(如
amd64
),、镜像默认配置信息、构建镜像的容器配置信息、包含所有镜像层信息的rootfs
。Docker利用rootfs
中的diff_id
计算出内容寻址的索引(chainID
)来获取layer
相关信息,进而获取每一个镜像层的文件内容 layer
(镜像层)是一个Docker用来管理镜像层的中间概念,镜像由镜像层组成,单个镜像层可能被多个镜像共享,所以Docker将layer与image的概念分离。Docker镜像管理中的layer
主要存放了镜像层的diff_id
、size
、cache-id
和parent
等内容,实际的文件内容则是由存储驱动来管理,并可以通过cache-id
在本地索引到
镜像构建
- Dockerfile是通过
docker build
命令构建自己的Docker镜像时需要用到的定义文件。它允许用户使用基本的DSL(Domain Specific Language
领域特定语言)语法来定义Docker镜像,每一条指令描述了构建镜像的步骤 - 原则上来说,用户不能无中生有的创建一个镜像,无论是启动一个容器或者构建一个镜像,都是在其他镜像的基础上进行的,Docker游一系列镜像称为基础镜像,基础镜像就是镜像构建的起点。
docker commit
是将容器提交为一个镜像,也就是从容器更新或者构建镜像,而docker build
是在一个镜像的基础上构建镜像
commit镜像
docker commit
命令只提交容器镜像发生变更了的部分,即修改后的容器镜像与当前仓库中对应镜像的差异部分,这使得该操作时机需要提交的文件并不算多- Docker daemon接收到对应的HTTP请求后,需要执行的步骤如下
- 根据用户输入pause参数的设置确定是否暂停该Docker容器的运行
- 将容器的可读写层导出打包,该读写层代表了当前运行容器的文件系统与当初启动容器的镜像之间的差异
- 在层存储(layerStore)中注册可读写层差异包
- 更新镜像历史信息和
rootfs
,并据此在镜像存储中创建一个新的镜像,记录其元数据 - 如果指定了
repository
信息,则给上述镜像添加tag
信息
build构建镜像
- 一般来说,用户主要使用
Docker
和docker build
命令来完成一个新镜像的构建。这条命令的格式如下
Usage: docker build [OPTIONS] PATH |URL| -
- 其中PATH或URL指向的文件称为context(上下文),context包含build Docker镜像过程中需要的Dockerfile以及其他的资源文件。
构建镜像的时候client
端就是根据docker build
命令的相关参数guiding进行设置,然后将这些参数组装成一个http请求,发送给server端;server端接收到这些请求之后,执行的流程如下
- 创建一个临时目录,并将context指定的文件系统解压到该目录下
- 读取并解析Dockerfile
- 根据解析出的Dockerfile遍历其中的所有指令,并分发到不同的模块中去执行。Dockerfile每条指令的格式均为
INSTRUCTION arguments
,INSTRUCTION
是一些特定的关键词,包括FROM,RUN
等,都会映射到不同的parser进行处理 - parser为上述每一个指令创建一个对应的临时容器,在临时容器中执行当前指令,然后通过commit使用此容器声成一个镜像层
- Dokerfile中所有的指令对应的层的集合,就是此次build后的结果。如果指定了tag参数,便给镜像打上对应的tag。最后一次commit生成的镜像ID就会作为最终的镜像ID返回
镜像分发
- 为了在不同的机器上创造无差别的应用运行环境,因此导出和导入容器的功能非常重要,
docker export
和docker import
命令实现了这一功能 - 容器迁移也可以通过镜像分发的方式达成,
docker push
和docker pull
是通过线上Docker Hub的方式迁移的;而docker save
和docker load
命令是通过线下包分发的方式进行迁移 - 直接对容器进行持久化(
docker export
)和使用镜像进行持久化(docker push, docker save
)的区别主要是将容器导出后再导入会丢失所有的历史,而保存后再加载的镜像则没有丢失历史和层,也就是后者可通过docker tag
命令实现的历史层回滚,而前者不行 - 相关代码可通过源码https://github.com/moby/moby/blob/master/api/server/router/router.go找到相关路由查看