[TOC]
## 1.前言
Docker Hub汇总众多Docker用户的镜像,极大得发挥Docker镜像开放的思想。Docker用户在全球任意一个角度,都可以与Docker Hub交互,分享自己构建的镜像至Docker Hub,当然也完全可以下载另一半球Docker开发者上传至Docker Hub的Docker镜像。
无论是上传,还是下载Docker镜像,镜像必然会以某种形式存储在Docker Daemon所在的宿主机文件系统中。Docker镜像在宿主机的存储,关键点在于:在本地文件系统中以如何组织形式,被Docker Daemon有效的统一化管理。这种管理,可以使得Docker Daemon创建Docker容器服务时,方便获取镜像并完成union mount操作,为容器准备初始化的文件系统。
本文主要从Docker 1.2.0源码的角度,分析Docker Daemon下载镜像过程中存储Docker镜像的环节。分析内容的安排有以下5部分:
(1) 概述Docker镜像存储的执行入口,并简要介绍存储流程的四个步骤;
(2) 验证镜像ID的有效性;
(3) 创建镜像存储路径;
(4) 存储镜像内容;
(5) 在graph中注册镜像ID。
## 2.镜像注册
Docker Daemon执行镜像下载任务时,从Docker Registry处下载指定镜像之后,仍需要将镜像合理地存储于宿主机的文件系统中。更为具体而言,存储工作分为两个部分:
(1) 存储镜像内容;
(2) 在graph中注册镜像信息。
说到镜像内容,需要强调的是,每一层layer的Docker Image内容都可以认为有两个部分组成:镜像中每一层layer中存储的文件系统内容,这部分内容一般可以认为是未来Docker容器的静态文件内容;另一部分内容指的是容器的json文件,json文件代表的信息除了容器的基本属性信息之外,还包括未来容器运行时的动态信息,包括ENV等信息。
存储镜像内容,意味着Docker Daemon所在宿主机上已经存在镜像的所有内容,除此之外,Docker Daemon仍需要对所存储的镜像进行统计备案,以便用户在后续的镜像管理与使用过程中,可以有据可循。为此,Docker Daemon设计了graph,使用graph来接管这部分的工作。graph负责记录有哪些镜像已经被正确存储,供Docker Daemon调用。
Docker Daemon执行CmdPull任务的pullImage阶段时,实现Docker镜像存储与记录的源码位于[./docker/graph/pull.go#L283-L285](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L283-L285),如下:
~~~
err = s.graph.Register(imgJSON,utils.ProgressReader(layer,
imgSize, out, sf, false, utils.TruncateID(id), “Downloading”),img)
~~~
以上源码的实现,实际调用了函数Register,Register函数的定义位于[./docker/graph/graph.go#L162-L218](https://github.com/docker/docker/blob/v1.2.0/graph/graph.go#L162-L218):
~~~
func (graph *Graph) Register(jsonData []byte, layerData
archive.ArchiveReader, img *image.Image) (err error)
~~~
分析以上Register函数定义,可以得出以下内容:
(1) 函数名称为Register;
(2) 函数调用者类型为Graph;
(3) 函数传入的参数有3个,第一个为jsonData,类型为数组,第二个为layerData,类型为archive.ArchiveReader,第三个为img,类型为*image.Image;
(4) 函数返回对象为err,类型为error。
Register函数的运行流程如图11-1所示:
![](https://box.kancloud.cn/2015-11-12_56443b0c7808a.jpg)
图11-1 Register函数执行流程图
## 3.验证镜像ID
Docker镜像注册的第一个步骤是验证Docker镜像的ID。此步骤主要为确保镜像ID命名的合法性。功能而言,这部分内容提高了Docker镜像存储环节的鲁棒性。验证镜像ID由三个环节组成。
(1) 验证镜像ID的合法性;
(2) 验证镜像是否已存在;
(3) 初始化镜像目录。
验证镜像ID的合法性使用包utils中的ValidateID函数完成,实现源码位于[./docker/graph/graph.go#L171-L173](https://github.com/docker/docker/blob/v1.2.0/graph/graph.go#L171-L173),如下:
~~~
if err := utils.ValidateID(img.ID); err != nil {
return err
}
~~~
ValidateID函数的实现过程中,Docker Dameon检验了镜像ID是否为空,以及镜像ID中是否存在字符‘:’,以上两种情况只要成立其中之一,Docker Daemon即认为镜像ID不合法,不予执行后续内容。
镜像ID的合法性验证完毕之后,Docker Daemon接着验证镜像是否已经存在于graph。若该镜像已经存在于graph,则Docker Daemon返回相应错误,不予执行后续内容。代码实现如下:
~~~
if graph.Exists(img.ID) {
return fmt.Errorf("Image %s already exists", img.ID)
}
~~~
验证工作完成之后,Docker Daemon为镜像准备存储路径。该部分源码实现位于[./docker/graph/graph.go#L182-L196](https://github.com/docker/docker/blob/v1.2.0/graph/graph.go#L182-L196),如下:
~~~
if err := os.RemoveAll(graph.ImageRoot(img.ID)); err != nil && !os.IsNotExist(err) {
return err
}
// If the driver has this ID but the graph doesn't, remove it from the driver to start fresh.
// (the graph is the source of truth).
// Ignore errors, since we don't know if the driver correctly returns ErrNotExist.
// (FIXME: make that mandatory for drivers).
graph.driver.Remove(img.ID)
tmp, err := graph.Mktemp("")
defer os.RemoveAll(tmp)
if err != nil {
return fmt.Errorf("Mktemp failed: %s", err)
}
~~~
Docker Daemon为镜像初始化存储路径,实则首先删除属于新镜像的存储路径,即如果该镜像路径已经在文件系统中存在的话,立即删除该路径,确保镜像存储时不会出现路径冲突问题;接着还删除graph.driver中的指定内容,即如果该镜像在graph.driver中存在的话,unmount该镜像在宿主机上的目录,并将该目录完全删除。以AUFS这种类型的graphdriver为例,镜像内容被存放在/var/lib/docker/aufs/diff目录下,而镜像会被mount至目录/var/lib/docker/aufs/mnt下的指定位置。
至此,验证Docker镜像ID的工作已经完成,并且Docker Daemon已经完成对镜像存储路径的初始化,使得后续Docker镜像存储时存储路径不会冲突,graph.driver对该镜像的mount也不会冲突。
## 4.创建镜像路径
创建镜像路径,是镜像存储流程中的一个必备环节,这一环节直接让Docker使用者了解以下概念:镜像以何种形式存在于本地文件系统的何处。创建镜像路径完毕之后,Docker Daemon首先将镜像的所有祖先镜像通过aufs文件系统mount至mnt下的指定点,最终直接返回镜像所在rootfs的路径,以便后续直接在该路径下解压Docker镜像的具体内容(只包含layer内容)。
### 4.1创建mnt、diff和layers
创建镜像路径的源码实现位于[./docker/graph/graph.go#L198-L206](https://github.com/docker/docker/blob/v1.2.0/graph/graph.go#L198-L206), 如下:
~~~
// Create root filesystem in the driver
if err := graph.driver.Create(img.ID, img.Parent); err != nil {
return fmt.Errorf("Driver %s failed to create image rootfs %s: %s", graph.driver, img.ID, err)
}
// Mount the root filesystem so we can apply the diff/layer
rootfs, err := graph.driver.Get(img.ID, "")
if err != nil {
return fmt.Errorf("Driver %s failed to get image rootfs %s: %s", graph.driver, img.ID, err)
}
~~~
以上源码中Create函数在创建镜像路径时起到举足轻重的作用。那我们首先分析graph.driver.Create(img.ID, img.Parent)的具体实现。由于在Docker Daemon启动时,注册了具体的graphdriver,故graph.driver实际的值为具体注册的driver。方便起见,本章内容全部以aufs类型为例,即在graph.driver为aufs的情况下,阐述Docker镜像的存储。在ubuntu 14.04系统上,Docker Daemon的根目录一般为/var/lib/docker,而aufs类型driver的镜像存储路径一般为/var/lib/docker/aufs。
AUFS这种联合文件系统的实现,在union多个镜像时起到至关重要的作用。首先来关注,Docker Daemon如何为镜像创建镜像路径,以便支持通过aufs来union镜像。Aufs模式下,graph.driver.Create(img.ID, img.Parent)的具体源码实现位于[./docker/daemon/graphdriver/aufs/aufs.go#L161-L190](https://github.com/docker/docker/blob/v1.2.0/daemon/graphdriver/aufs/aufs.go#L161-L190),如下:
~~~
// Three folders are created for each id
// mnt, layers, and diff
func (a *Driver) Create(id, parent string) error {
if err := a.createDirsFor(id); err != nil {
return err
}
// Write the layers metadata
f, err := os.Create(path.Join(a.rootPath(), "layers", id))
if err != nil {
return err
}
defer f.Close()
if parent != "" {
ids, err := getParentIds(a.rootPath(), parent)
if err != nil {
return err
}
if _, err := fmt.Fprintln(f, parent); err != nil {
return err
}
for _, i := range ids {
if _, err := fmt.Fprintln(f, i); err != nil {
return err
}
}
}
return nil
}
~~~
在Create函数的实现过程中,createDirsFor函数在Docker Daemon根目录下的aufs目录/var/lib/docker/aufs中,创建指定的镜像目录。若当前aufs目录下,还不存在mnt、diff这两个目录,则会首先创建mnt、diff这两个目录,并在这两个目录下分别创建代表镜像内容的文件夹,文件夹名为镜像ID,文件权限为0755。假设下载镜像的镜像ID为image_ID,则创建完毕之后,文件系统中的文件为/var/lib/docker/aufs/mnt/image_ID与/var/lib/docker/aufs/diff/image_ID。回到Create函数中,执行完createDirsFor函数之后,随即在aufs目录下创建了layers目录,并在layers目录下创建image_ID文件。
如此一来,在aufs下的三个子目录mnt,diff以及layers中,分别创建了名为镜像名image_ID的文件。继续深入分析之前,我们直接来看Docker对这三个目录mnt、diff以及layers的描述,如图11-2所示:
![](https://box.kancloud.cn/2015-11-12_56443b0c92b74.png)
图11-2 aufs driver目录结构图
简要分析图11-2,图中的layers、diff以及mnt为目录/var/lib/docker/aufs下的三个子目录,1、2、3是镜像ID,分别代表三个镜像,三个目录下的1均代表同一个镜像ID。其中layers目录下保留每一个镜像的元数据,这些元数据是这个镜像的祖先镜像ID列表;diff目录下存储着每一个镜像所在的layer,具体包含的文件系统内容;mnt目录下每一个文件,都是一个镜像ID,代表在该层镜像之上挂载的可读写layer。因此,下载的镜像中与文件系统相关的具体内容,都会存储在diff目录下的某个镜像ID目录下。
再次回到Create函数,此时mnt,diff以及layer三个目录下的镜像ID文件已经创建完毕。下一步需要完成的是:为layers目录下的镜像ID文件填充元数据。元数据内容为该镜像所有的祖先镜像ID列表。填充元数据的流程如下:
(1) Docker Daemon首先通过f, err := os.Create(path.Join(a.rootPath(), "layers", id))打开layers目录下镜像ID文件;
(2) 然后,通过ids, err := getParentIds(a.rootPath(), parent)获取父镜像的祖先镜像ID列表ids;
(3) 其次,将父镜像镜像ID写入文件f;
(4) 最后,将父镜像的祖先镜像ID列表ids写入文件f。
最终的结果是:该镜像的所有祖先镜像的镜像ID信息都写入layers目录下该镜像ID文件中。
### 4.2 mount祖先镜像并返回根目录
Create函数执行完毕,意味着创建镜像路径并配置镜像元数据完毕,接着Docker Daemon返回了镜像的根目录,源码实现如下:
~~~
rootfs, err := graph.driver.Get(img.ID, "")
~~~
Get函数看似返回了镜像的根目录rootfs,实则执行了更为重要的内容——挂载祖先镜像文件系统。具体而言,Docker Daemon为当前层的镜像完成所有祖先镜像的Union Mount。Mount完毕之后,当前镜像的read-write层位于/var/lib/docker/aufs/mnt/image_ID。Get函数的具体实现位于[./docker/daemon/graphdriver/aufs/aufs.go#L247-L278](https://github.com/docker/docker/blob/v1.2.0/daemon/graphdriver/aufs/aufs.go#L247-L278),如下:
~~~
func (a *Driver) Get(id, mountLabel string) (string, error) {
ids, err := getParentIds(a.rootPath(), id)
if err != nil {
if !os.IsNotExist(err) {
return "", err
}
ids = []string{}
}
// Protect the a.active from concurrent access
a.Lock()
defer a.Unlock()
count := a.active[id]
// If a dir does not have a parent ( no layers )do not try to mount
// just return the diff path to the data
out := path.Join(a.rootPath(), "diff", id)
if len(ids) > 0 {
out = path.Join(a.rootPath(), "mnt", id)
if count == 0 {
if err := a.mount(id, mountLabel); err != nil {
return "", err
}
}
}
a.active[id] = count + 1
return out, nil
}
~~~
分析以上Get函数的定义,可以得出以下内容:
(1) 函数名为Get;
(2) 函数调用者类型为Driver;
(3) 函数传入参数有两个:id与mountlabel;
(4) 函数返回内容有两部分:string类型的镜像根目录与错误对象error。
清楚Get函数的定义,再来看Get函数的实现。分析Get函数实现时,有三个部分较为关键,分别是Driver实例a的active属性、mount操作、以及返回值out。
首先分析Driver实例a的active属性。分析active属性之前,需要追溯到Aufs类型的graphdriver中Driver类型的定义以及graphdriver与graph的关系。两者的关系如图11-3所示:
![](https://box.kancloud.cn/2015-11-12_56443b0cb94ef.jpg)
图11-3 graph与graphdriver关系图
Driver类型的定义位于[./docker/daemon/graphdriver/aufs/aufs#L53-L57](https://github.com/docker/docker/blob/v1.2.0/daemon/graphdriver/aufs/aufs.go#L53-L57),如下:
~~~
type Driver struct {
root string
sync.Mutex // Protects concurrent modification to active
active map[string]int
}
~~~
Driver结构体中root属性代表graphdriver所在的根目录,为/var/lib/docker/aufs。active属性为map类型,key为string,具体运用时key为Docker Image的ID,value为int类型,代表该层镜像layer被引用的次数总和。Docker镜像技术中,某一层layer的Docker镜像被引用一次,则active属性中key为该镜像ID的value值会累加1。用户执行镜像删除操作时,Docker Dameon会检查该Docker镜像的引用次数是否为0,若引用次数为0,则可以彻底删除该镜像,若不是的话,则仅仅将active属性中引用参数减1。属性sync.Mutex用于多个Job同时操作active属性时,确保active数据的同步工作。
接着,进入mount操作的分析。一旦Get参数传入的镜像ID参数不是一个Base Image,那么说明该镜像存在父镜像,Docker Daemon需要将该镜像所有的祖先镜像都mount到指定的位置,指定位置为/var/lib/docker/aufs/mnt/image_ID。所有祖先镜像的原生态文件系统内容分别位于/var/lib/docker/aufs/diff/。其中mount函数用以实现该部分描述的功能,mount的过程包含很多与aufs文件系统相关的参数配置与系统调用。
最后,Get函数返回out与nil。其中out的值为/var/lib/docker/aufs/mnt/image_ID,即使用该层Docker镜像时其根目录所在路径,也可以认为是镜像的RW层所在路径,但一旦该层镜像之上还有镜像,那么在mount后者之后,在上层镜像看来,下层镜像仍然是只读文件系统。
## 5.存储镜像内容
存储镜像内容,Docker Daemon的运行意味着已经验证过镜像ID,同时还为镜像准备了存储路径,并返回了其所有祖先镜像union mount后的路径。万事俱备,只欠“镜像内容的存储”。
Docker Daemon存储镜像具体内容完成的工作很简单,仅仅是通过某种合适的方式将两部分内容存储于本地文件系统并进行有效管理,它们是:镜像压缩内容、镜像json信息。
存储镜像内容的源码实现位于[./docker/graph/graph.go#L209-L211](https://github.com/docker/docker/blob/v1.2.0/graph/graph.go#L209-L211),如下:
~~~
if err := image.StoreImage(img, jsonData, layerData, tmp, rootfs); err != nil {
return err
}
~~~
其中,StoreImage函数的定义位于[./docker/docker/image/image.go#L74](https://github.com/docker/docker/blob/v1.2.0/image/image.go#L74),如下:
~~~
func StoreImage(img *Image, jsonData []byte, layerData
archive.ArchiveReader, root, layer string) error {
~~~
分析StoreImage函数的定义,可以得出以下信息:
(1) 函数名称:StoreImage;
(2) 函数传入参数名:img,jsonData,layerData,root,layer;
(3) 函数返回类型error。
简要分析传入参数的含义如表11-1所示:
表11-1 StoreImage函数参数表
| 参数名称 | 参数含义 |
|---|---|
| img | 通过下载的imgJSON信息创建出的Image对象实例 |
| jsonData | Docker Daemon之前下载的imgJSON信息 |
| layerData | 镜像作为一个layer的压缩包,包含镜像的具体文件内容 |
| root | graphdriver根目录下创建的临时文件”_tmp”,值为/var/lib/docker/aufs/_tmp |
| layer | Mount完所有祖先镜像之后,该镜像在mnt目录下的路径 |
掌握StoreImage函数传入参数的含义之后,理解其实现就十分简单。总体而言,StoreImage亦可以分为三个步骤:
(1) 解压镜像内容layerData至diff目录;
(2) 收集镜像所占空间大小,并记录;
(3) 将jsonData信息写入临时文件。
以下详细深入三个步骤的实现。
### 5.1解压镜像内容
StoreImage函数传入的镜像内容是一个压缩包,Docker Daemon理应在镜像存储时将其解压,为后续创建容器时直接使用镜像创造便利。
既然是解压镜像内容,那么这项任务的完成,除了需要代表镜像的压缩包之后,还需要解压任务的目标路径,以及解压时的参数。压缩包为StoreImage传入的参数layerData,而目标路径为/var/lib/docker/aufs/diff/。解压流程的执行源代码位于[./docker/docker/image/image.go#L85-L120](https://github.com/docker/docker/blob/v1.2.0/image/image.go#L85-L120),如下:
~~~
// If layerData is not nil, unpack it into the new layer
if layerData != nil {
if differ, ok := driver.(graphdriver.Differ); ok {
if err := differ.ApplyDiff(img.ID, layerData); err != nil {
return err
}
if size, err = differ.DiffSize(img.ID); err != nil {
return err
}
} else {
start := time.Now().UTC()
log.Debugf("Start untar layer")
if err := archive.ApplyLayer(layer, layerData); err != nil {
return err
}
log.Debugf("Untar time: %vs", time.Now().UTC().Sub(start).Seconds())
if img.Parent == "" {
if size, err = utils.TreeSize(layer); err != nil {
return err
}
} else {
parent, err := driver.Get(img.Parent, "")
if err != nil {
return err
}
defer driver.Put(img.Parent)
changes, err := archive.ChangesDirs(layer, parent)
if err != nil {
return err
}
size = archive.ChangesSize(layer, changes)
}
}
}
~~~
可见当镜像内容layerData不为空时,Docker Daemon需要为镜像压缩包执行解压工作。以aufs这种graphdriver为例,一旦aufs driver实现了graphdriver包中的接口Diff,则Docker Daemon会使用aufs driver的接口方法实现后续的解压操作。解压操作的源代码如下:
~~~
if differ, ok := driver.(graphdriver.Differ); ok {
if err := differ.ApplyDiff(img.ID, layerData); err != nil {
return err
}
if size, err = differ.DiffSize(img.ID); err != nil {
return err
}
}
~~~
以上代码即实现了镜像压缩包的解压,与镜像所占空间大小的统计。代码differ.ApplyDiff(img.ID, layerData)将layerData解压至目标路径。理清目标路径,且看aufs这个driver中ApplyDiff的实现,位于[./docker/docker/daemon/graphdriver/aufs/aufs.go#L304-L306](https://github.com/docker/docker/blob/v1.2.0/daemon/graphdriver/aufs/aufs.go#L304-L306),如下:
~~~
func (a *Driver) ApplyDiff(id string, diff archive.ArchiveReader) error {
return archive.Untar(diff, path.Join(a.rootPath(), "diff", id), nil)
}
~~~
解压过程中,Docker Daemon通过aufs driver的根目录/var/lib/docker/aufs、diff目录与镜像ID,拼接出镜像的解压路径,并执行解压任务。举例说明diff文件的作用,镜像27d474解压后的内容如图11-4所示:
![](https://box.kancloud.cn/2015-11-12_56443b0cdf350.png)
图11-4镜像解压后示意图
回到StoreImage函数的执行流中,ApplyDiff任务完成之后,Docker Daemon通过DiffSize开启镜像磁盘空间统计任务。
### 5.2收集镜像大小并记录
Docker Daemon接管镜像存储之后,Docker镜像被解压到指定路径并非意味着“任务完成”。Docker Daemon还额外做了镜像所占空间大小统计的空间,以便记录镜像信息,最终将这类信息传递给Docker用户。
镜像所占磁盘空间大小的统计与记录,实现过程简单且有效,源代码位于[./docker/docker/image/image.go#L122-L125](https://github.com/docker/docker/blob/v1.2.0/image/image.go#L122-L125),如下:
~~~
img.Size = size
if err := img.SaveSize(root); err != nil {
return err
}
~~~
首先Docker Daemon将镜像大小收集起来,更新Image类型实例img的Size属性,然后通过img.SaveSize(root)将镜像大小写入root目录,由于传入的root参数为临时目录_tmp,即写入临时目录_tmp下。深入SaveSize函数的实现,如以下源码:
~~~
func (img *Image) SaveSize(root string) error {
if err := ioutil.WriteFile(path.Join(root, "layersize"), []
byte(strconv.Itoa(int(img.Size))), 0600); err != nil {
return fmt.Errorf("Error storing image size in %s/layersize: %s", root, err)
}
return nil
}
~~~
SaveSize函数在root目录(临时目录/var/lib/docker/graph/_tmp)下创建文件layersize,并写入镜像大小的值img.Size。
### 5.3存储jsonData信息
Docker镜像中jsonData是一个非常重要的概念。在笔者看来,Docker的镜像并非只是Docker容器文件系统中的文件内容,同时还包括Docker容器运行的动态信息。这里的动态信息更多的是为了适配Dockerfile的标准。以Dockerfile中的ENV参数为例,ENV指定了Docker容器运行时,内部进程的环境变量。而这些只有容器运行时才存在的动态信息,并不会被记录在静态的镜像文件系统中,而是存储在以jsonData的形式先存储在宿主机的文件系统中,并与镜像文件系统做清楚的区分,存储在不同的位置。当Docker Daemon启动Docker容器时,Docker Daemon会准备好mount完毕的镜像文件系统环境;接着加载jsonData信息,并在运行Docker容器内部进程时,使用动态的jsonData内部信息为容器内部进程配置环境。
当Docker Daemon下载Docker镜像时,关于每一个镜像的jsonData信息均会被下载至宿主机。通过以上jsonData的功能描述可以发现,这部分信息的存储同样扮演重要的角色。Docker Daemon如何存储jsonData信息,实现源码位于[./docker/docker/image/image.go#L128-L139](https://github.com/docker/docker/blob/v1.2.0/image/image.go#L128-L139),如下:
~~~
if jsonData != nil {
if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil {
return err
}
} else {
if jsonData, err = json.Marshal(img); err != nil {
return err
}
if err := ioutil.WriteFile(jsonPath(root), jsonData, 0600); err != nil {
return err
}
}
~~~
可见Docker Daemon将jsonData写入了文件jsonPath(root)中,并为该文件设置的权限为0600。而jsonPath(root)的实现如下,即在root目录(/var/lib/docker/graph/_tmp目录)下创建文件json:
~~~
func jsonPath(root string) string {
return path.Join(root, "json")
}
~~~
镜像大小信息layersize信息统计完毕,jsonData信息也成功记录,两者的存储文件均位于/var/lib/docker/graph/_tmp下,文件名分别为layersize和json。使用临时文件夹来存储这部分信息并非偶然,11.6节将阐述其中的原因。
## 6.注册镜像ID
Docker Daemon执行完镜像的StoreImage操作,回到Register函数之后,执行镜像的commit操作,即完成镜像在graph中的注册。
注册镜像的代码实现位于[./docker/docker/graph/graph.go#L212-L216](https://github.com/docker/docker/blob/v1.2.0/graph/graph.go#L212-L216),如下:
~~~
// Commit
if err := os.Rename(tmp, graph.ImageRoot(img.ID)); err != nil {
return err
}
graph.idIndex.Add(img.ID)
~~~
11.5节StoreImage过程中使用到的临时文件_tmp在注册镜像环节有所体现。镜像的注册行为,第一步就是将tmp文件(/var/lib/docker/graph/_tmp )重命名为graph.ImageRoot(img.ID),实则为/var/lib/docker/graph/。使得Docker Daemon在而后的操作中可以通过img.ID在/var/lib/docker/graph目录下搜索到相应镜像的json文件与layersize文件。
成功为json文件与layersize文件配置完正确的路径之后,Docker Daemon执行的最后一个步骤为:添加镜像ID至graph.idIndex。源代码实现是graph.idIndex.Add(img.ID),graph中idIndex类型为*truncindex.TruncIndex, TruncIndex的定义位于[./docker/docker/pkg/truncindex/truncindex.go#L22-L28](https://github.com/docker/docker/blob/v1.2.0/pkg/truncindex/truncindex.go#L22-L28),如下:
~~~
// TruncIndex allows the retrieval of string identifiers by any of their unique prefixes.
// This is used to retrieve image and container IDs by more convenient shorthand prefixes.
type TruncIndex struct {
sync.RWMutex
trie *patricia.Trie
ids map[string]struct{}
}
~~~
Docker用户使用Docker镜像时,一般可以通过指定镜像ID来定位镜像,如Docker官方的mongo:2.6.1镜像id为c35c0961174d51035d6e374ed9815398b779296b5f0ffceb7613c8199383f4b1,该ID长度为64。当Docker用户指定运行这个mongo镜像Repository中tag为2.6.1的镜像时,完全可以通过64为的镜像ID来指定,如下:
~~~
docker run –it c35c0961174d51035d6e374ed9815398b779296b5f0ffceb7613c8199383f4b1 /bin/bash
~~~
然而,记录如此长的镜像ID,对于Docker用户来说稍显不切实际,而TruncIndex的概念则大大帮助Docker用户可以通过简短的ID定位到指定的镜像,使得Docker镜像的使用变得尤为方便。原理是:Docker用户指定镜像ID的前缀,只要前缀满足在全局所有的镜像ID中唯一,则Docker Daemon可以通过TruncIndex定位到唯一的镜像ID。而graph.idIndex.Add(img.ID)正式完成将img.ID添加保存至TruncIndex中。
为了达到上一条命令的效果,Docker 用户完全可以使用TruncIndex的方式,当然前提是c35这个字符串作为前缀全局唯一,命令如下:
~~~
docker run –it c35 /bin/bash
~~~
至此,Docker镜像存储的整个流程已经完成。概括而言,主要包含了验证镜像、存储镜像、注册镜像三个步骤。
## 7.总结
Docker镜像的存储,使得Docker Hub上的镜像能够传播于世界各地变为现实。Docker镜像在Docker Registry中的存储方式与本地化的存储方式并非一致。Docker Daemon必须针对自身的graphdriver类型,选择适配的存储方式,实施镜像的存储。本章的分析,也在不断强调一个事实,即Docker镜像并非仅仅包含文件系统中的静态文件,除此之外还包含了镜像的json信息,json信息中有Docker容器的配置信息,如暴露端口,环境变量等。
可以说Docker容器的运行强依赖于Docker镜像,Docker镜像的由来就变得尤为重要。Docker镜像的下载,Docker镜像的commit以及docker build新的镜像,都无法跳出镜像存储的范畴。Docker镜像的存储知识,也会有助于Docker其他概念的理解,如docker commit、docker build等。
## 8.作者介绍
**孙宏亮**,[DaoCloud](http://www.daocloud.io/)初创团队成员,软件工程师,浙江大学VLIS实验室应届研究生。读研期间活跃在PaaS和Docker开源社区,对Cloud Foundry有深入研究和丰富实践,擅长底层平台代码分析,对分布式平台的架构有一定经验,撰写了大量有深度的技术博客。2014年末以合伙人身份加入DaoCloud团队,致力于传播以Docker为主的容器的技术,推动互联网应用的容器化步伐。邮箱:[allen.sun@daocloud.io](mailto:allen.sun@daocloud.io)
## 参考文献
[http://aufs.sourceforge.net/aufs.html](http://aufs.sourceforge.net/aufs.html)