docker 镜像管理
镜像是什么
docker镜像是一个只读的Docker容器模板,含有启动docker容器所需的文件系统结构及其内容,因此是启动一个docker容器的基础。docker镜像的文件内容以及一些docker容器的配置文件组成了Docker容器的静态文件系统运行环境——rootfs。可以这么理解,docker镜像是docker容器的静态视角,docker容器时docker镜像的运行状态
rootfs
rootfs是docker容器启动时内部进程可见的文件系统,即docker容器根目录。rootfs通常包含一个操作系统运行所需的文件系统,包含典型的类Unix操作系统的目录系统/dev、/proc、/bin、/etc、/lib、/usr、/tmp以及docker容器所需的配置文件工具等
在传统的Linux操作系统内核启动时,首先挂载一个只读(read-only)的rootfs,当操作系统检测其完整性后,再切换为读写(read-write)模式,在Docker架构中,当Docker daemon为Docker容器挂载rootfs时,沿用Linux内核启动时的方法,即将rootfs设置只读模式。在挂载完成后,利用联合挂载(union mount)技术在已有只读rootfs上在挂载一个读写层,这样可读写层处于Docker容器文件系统最顶端,其下可能联合挂载多个只读层,只有在Docker运行过程中文件系统发生变化时,才会把变化的文件内容写到读写层,并隐藏只读层中的老版本。
Docker 镜像特点
(1)分层:Docker镜像是采用分层方式构建的,每层镜像都由一系列的“镜像层”组成。分层结构是Docker镜像结构如此轻巧的重要原因,当需要修改容器内某个文件时,只对处于上方的读写层进行变动,不覆写下层已有文件系统内容,已有文件在只有读写层中的原始版本仍旧存在,但会被读写层中的新版文件隐藏。当使用docker commit 提交过这个修改过的容器文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统被更新过的文件。分层达到了在不同镜像之间共享镜像层的效果
(2)写时复制:Docker镜像使用了写时复制策略,在多个容器之间共享镜像,每个容器在启动时并不需要单独复制一份镜像文件,而是将所有镜像层以只读的方式挂载到一个挂载点上,在上面覆盖一层可读写的容器层。未更改时,所有容器共享一份数据,只有在Docker容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层中的老版本文件。写时复制配合分层机制减少了镜像对磁盘的占用率和容器启动的时间
(3)内容寻址:在Docker 1.10版本后,Docker镜像改动较大,其中最重要特性便是引进了内容寻址存储的机制,根据文件内容来索引镜像和镜像层。与之前版本对每一个镜像层随机生成的UUID不同,新模型对镜像层的内容计算校验和,生成一个内容哈希值,并以此哈希值代替之前的UUID作为镜像层的唯一标识,该机制主要提高了镜像的安全性,并在pull、push、load和save操作后检查数据完整性,另外,基于内容哈希来索引镜像层,在一定程度上减少了ID的冲突并且增强了镜像层的共享。对于来自不同构建的镜像层,只有拥有相同的内容哈希,也能被不同的镜像共享
(4)联合挂载:通俗讲联合挂载技术可以在一个挂载点挂载多个文件系统,将挂载点的原目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录。实现这种联和挂载技术的文件系统通常被称为联和文件系统。以运行Ubuntu:14.04镜像后容器中的aufs文件系统为例。由于最初挂载时读写层为空,所有从用户视角看,该容器文件系统与底层的rootfs没有什么差别;从内核视角看,则是显示区分开两个层次。当需要修改镜像内的某文件时,只对处于最上方的读写层进行了变动,不覆写下次已有的文件系统的内容,已有的文件只在读层中的原始版本仍然存在,但会被读写层新文件所隐藏,当docker commit这个文件系统为一个新的镜像时,保存的内容仅为最上层读写文件系统中被更新过后的文件;联和挂载是用于将多个镜像层文件挂载到一个挂载点上来实现统一文件系统视图的途径,是上下层存储驱动实现分层合并的方式。严格说,联和挂载并不是Docker镜像的必须技术,假如我们在使用Device Mapper存储驱动时,其实是使用了快照技术来达到分层的效果,没有联和挂载这一概念。
Docker 镜像概念
(1)registry:用来保存Docker镜像的,其中包含镜像层次和关于镜像的元数据,可以将registry简单想象成git仓库之类的实体;用户可以在自己的数据中心搭建私有的registry,也可以用Docker官方的共用registry服务,即Docker hub 。有Docker公司维护的一个公共镜像仓库,供用户下载使用,Docker Hub 有两类型的仓库,即用户仓库与顶级仓库,用户仓库有普通的Docker Hub用户创建,顶仓库由Docker公司负责维护,提供官方镜像。理论上,顶级仓库中镜像经过Docke官方认证的,被认为是架构良好且安全的
(2)repository:由具体某个功能的Docker镜像的所有迭代版本构成的镜像组,由上文可知registry由一系列经过命名的repository组成,repository通过命名规范对用户仓库和顶级仓库进行组织。用户仓库命名由用户名和repository名组成,中间以“/”隔开,即username/repository_name的形式,repository通常表示镜像所具有的功能;而顶级仓库则只包含repository名的部分。一言以蔽之,registry是repository的集合,repository是镜像集合
(3)manifest:文件描述符主要存在于registry中作为Docker镜像的元数据文件,在pull、push、save和load中作为镜像结构和基础信息的描述文件。在镜像被pull或save到Docker宿主机时,manifest被转化为本地的镜像配置文件config。新版本(v2,schema 2)的manifest list可以组合不同架构实现同名Docker镜像的manifest,用以支持多架构Docker镜像。
(4) image和layer:Docker内部的image概念是用来存储一组镜像相关的元数据信息,主要包括镜像的架构、镜像默认配置、构建镜像的容器配置信息,包含所有镜像层的rootfs中的diff_id计算出内容寻址索引来获取layer相关信息,进而获取每一个镜像层的文件内容;layer(镜像层)是一个Docker用来管理镜像层的中间概念,镜像是由镜像层组成的,而单个镜像层可能被多个镜像共享,所有Docker将layer和image的概念分离,Docker镜像管理中的layer主要存放了镜像层的diff_id、size、cache-id和parent等内容,实际的文件是由存储驱动管理的,并可以通过cache-id在本地索引到
(5)Dockerfile:Dockerfile是通过docker build命令构建自己的Docker镜像时需要使用到的定义文件,允许用户基于DSL语法来定义Docker镜像,每条指令都描述了构建镜像的步骤。
Docker镜像构建步骤
Docker提供了比较简单的方式构建镜像或者更新镜像——docker build和docker commit,不过原则上讲用户不能无中生有地创建出一个镜像,或者启动一个容器构建一个镜像,无论启动容器或构建镜像,都是在其他镜像基础上进行的,Docker有一系列镜像称为基础镜像,基础镜像便是镜像构建的起点。不同的是,docker commit是将容器提交为一个镜像,也就是从容器更新或者构建镜像;而docker build是在一个镜像的基础上构建镜像。
1. commit 镜像: docker commit命令只提交容器镜像发生变更的部分,即修改后的容器镜像与当前仓库中对应镜像之间的差异部分,这使得该操作实际需要提交的文件往往并不多
Docker daemon接受到对应的HTTP请求后,需要执行的步骤
(1)根据用户输入pause参数的设置确定是否暂停该容器运行
(2)将容器可读写层导出打包,该读写层代表了当前运行的容器文件系统与当初启动该容器的镜像之间的差异
(3)在层存储中注册可读写层差异包
(4)更新历史镜像信息和rootfs,并据此在镜像存储中创建一个新的镜像,记录其元数据
(5)如果指定了repository信息,则给上述镜像添加tag信息
2.build构建镜像: Docker client接收用户命令,首先解析命令行参数,根据第一个参数不同,将分为下面4中情况
1.第一个参数为-
docker build - < Dockerfil
或者
docker build - < context.tar.gz
此时命令行输入的参数对Dockerfile和context
2.第一个参数URL,且是 git repository URL
docker build github.com/craeck/docker-firefox
则调用git clone --depth 1 --recursive 命令克隆该GitHub repository,该操作会在本地的一个临时目录进行,命令成功之后该目录将作为context传给Docker daemon,该目录中Dockerfile会被用来进行后续构建Docker镜像。
3.第一个参数为URI,且不是git repository URI,则从该URI下载context,并将其封装为一个io流——io.Reader,后面处理与情况1相同,只是将STDIN换成io.Reader。
4.其他情况,即context为本地文件或者目录的情况
使用当前文件夹作为context docker build -t vieux/apache:2.0
或者
使用/home/me/myapp/dockerfile/debug作为Dockerfile,并使用/home/me/myapp/作为context cd /home/me/myapp/some/dir/really/deep docker build -f /home/me/myapp/dockerfile/debug /home/me/myapp
如果目录有.dockerignore文件,则将context中文名满足其定义的规则的文件都从上传列表中排出,不打包传给Docker daemon。但唯一的例外是.dockerignore文件中若误写入了.dockerignore本身或者Dockerfile,将不会产生作用。如果用户定义了tag,则对其指定的repository和tag进行验证。
完成了相关信息的设置之后,Docker client向Docker server 发送POST/build的HTTP请求,包含了所需的context信息
Docker server端
Docker daemon接到相应的HTTP请求后,需要做的工作如下
(1)创建一个临时目录,并将context指定的指定的文件系统解压到该目录
(2)读取并解析Dockerfile
(3)根据解析出的Dockerfile遍历其中的所有指令,并分发到不同的模块去执行。Dockerfile每一条指令的格式均为INSTRUCTION是一些特定的关键词,包括FROM 、RUI、USER等,都会映射到不同的parser进行处理
(4)parser为上述每一个指令创建一个对应的临时容器,在临时容器中执行当前指令,然后通过commit使用此容器生成一个镜像层
(5)Dockerfile中所有的指令对应的层的集合,就是此次build后的结果。如果指定了tag参数,便给镜像打上对应的tag参数。最后一次commit生成的镜像ID就会作为最终镜像ID返回
Docker 镜像的分发方法
Docker 技术兴起的源动力之一,是在不同的机器上创造出无差别的应用运行环境,因此能够方便的实现“在某台机器上导出一个docker容器并且在另一台机器上导入”这一操作,就显然非常必要。docker export与docker import命令实现了这一功能。当然,由于Docker容器与镜像的天然联系性,容器迁移的操作也可以通过镜像分发的方式实现,这里可以用到的方式是docker push和docker pull,或者docker save 和docker load命令进行镜像的分发,不同的是docker push通过线上的Docker Hub的方式迁移,而docker save则是通过线下包分发的方式迁移
所以,我们不难看到同样是对容器进行持久化操作,直接对容器进行持久化和使用镜像镜像持久化的与别在于
(1)两者应用的对象不同,docker export用于持久化容器,而docker push 和docker save 用于持久化镜像
(2)将容器导出后再导入后的容器会丢失所有的历史,而保存再进行加载的镜像则没有丢失历史和层,这就意味着后者可以通过docker tag命令实现历史回滚,而前者不行
pull 镜像
Docker daemon收到用户发起pull请求后处理工作
(1)根据用户命令行参数解析出其希望拉取的repository信息,这里repository可能为tag格式也可能是digest格式
(2)将repository信息解析为RepositoryInfo并验证其合法性
(3)根据待拉取的repository是否为official版本以及用户没有配置Docker Mirrors版获取endpoint列表,并遍历endpoint,向该endpoint指定的repositry发起会话。endpoint偏好顺序为API版本v2>v1协议https>http
(4)如果待拉取的repository为official版本,或者endpoint的API版本为V2,Docker便不再尝试对v1 endpoint发起会话,直接指向v2 registry拉取镜像
(5)如果想v2 registry拉取失败,则尝试拉取v1 registry镜像
v2拉取镜像过程
(1)获取v2 registry的endpoint
(2)由endpoint和待拉取镜像名创建HTTP会话、获取拉取指定镜像的认证信息并验证API版本
(3)如果tag值为空,即没有指定标签,则获取v2 registry中repository中的tag list,然后对于tag list 每个标签,都执行一次pullV2Tag方法,该方法功能两大部分,1验证用户请求2是当且仅当某一层不在本地时进行拉取这一层文件到本地
(4)如果tag值为空,则只对只对标签的镜像进行上述工作
push镜像
户制作出自己的镜像后,希望将它上传至仓库,此时通过docker pull命令完成
(1)解析出repository信息
(2)获取所有Docker Mirror的endpoint列表,并验证repository在本地是否存在,遍历endpoint,然后发起同registry的会话。如果确认对方API版本是v2,则不再对v1 endpoint发起会话
(3)如果endpoint对应版本为v2 registry,则验证被推registry的访问权限,创建V2Pusher,调用pushV2 Repository方法。这个方法会判断用户输入的repository名字是否含有tag,如果含有则在本地repository中获取对应镜像的ID,调用pushV2Tag方法;如果不含tag,则会在本地repository中查询对应有同名repository,对其中每一个获取镜像ID,执行pushV2Tag
(4)这个方法会首先验证用户指定的镜像ID在本地ImageStore中是否存在,接下来,该方法会从顶向下逐个构建一个描述结构体,上传这些镜像。这些镜像上传完成后,再将一份描述文件manifes上传到registry
(5)如果镜像不属于上述情况,则Docker会调用PushRepository方法推送镜像到v1 registry,并根据待推送的repository和tag信息保证党且仅当某layer在enpoint上不存在时,才上传layer
docker export命令导出镜像
(1)根据命令行参数(容器名称)找到待导出的容器
(2)对该容器调用containerExport()函数找到待导出容器的所有数据,包括挂载待导出容器文件系统、打包该容器basefs下所有文件。basefs对应的是aufs/mnt下对应容器ID目录
(3)将导出的数据回写HTTP请求应答中
docker save 命令保存镜像
(1)为每一个被要求导出的镜像创建一个文件夹,以镜像ID命名
(2)在该文件夹下创建VERSION文件,写“1.0”
(3)在该文件下创建json文件,在该文件中写入镜像的元数据信息,包括镜像ID、父镜像ID以及对于的Docker容器ID等
(4)在该文件夹下传layer.tar文件,压缩镜像的filesystem。该过程的核心函数为TarLayer,对存储镜像的diff路径中文件进行打包
(5)对该layer的父layer执行下一次循环