最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

Docker 源码分析(十一):镜像存储

  • 2015-07-14
  • 本文字数:11111 字

    阅读完需:约 36 分钟

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 ,如下:

复制代码
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 :

复制代码
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 所示:

图 11-1 Register 函数执行流程图

3. 验证镜像 ID

Docker 镜像注册的第一个步骤是验证 Docker 镜像的 ID。此步骤主要为确保镜像 ID 命名的合法性。功能而言,这部分内容提高了 Docker 镜像存储环节的鲁棒性。验证镜像 ID 由三个环节组成。

(1) 验证镜像 ID 的合法性;

(2) 验证镜像是否已存在;

(3) 初始化镜像目录。

验证镜像 ID 的合法性使用包 utils 中的 ValidateID 函数完成,实现源码位于./docker/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 ,如下:

复制代码
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 , 如下:

复制代码
// 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 , 如下:

复制代码
// 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 所示:

图 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 ,如下:

复制代码
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 所示:

图 11-3 graph 与 graphdriver 关系图

Driver 类型的定义位于./docker/daemon/graphdriver/aufs/aufs#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 ,如下:

复制代码
if err := image.StoreImage(img, jsonData, layerData, tmp, rootfs); err != nil {
return err
}

其中,StoreImage 函数的定义位于./docker/docker/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/<image_ID>。解压流程的执行源代码位于./docker/docker/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 ,如下:

复制代码
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 所示:

图 11-4 镜像解压后示意图

回到 StoreImage 函数的执行流中,ApplyDiff 任务完成之后,Docker Daemon 通过 DiffSize 开启镜像磁盘空间统计任务。

5.2 收集镜像大小并记录

Docker Daemon 接管镜像存储之后,Docker 镜像被解压到指定路径并非意味着“任务完成”。Docker Daemon 还额外做了镜像所占空间大小统计的空间,以便记录镜像信息,最终将这类信息传递给 Docker 用户。

镜像所占磁盘空间大小的统计与记录,实现过程简单且有效,源代码位于./docker/docker/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 ,如下:

复制代码
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 ,如下:

复制代码
// 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/<img.ID>。使得 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 ,如下:

复制代码
// 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 初创团队成员,软件工程师,浙江大学 VLIS 实验室应届研究生。读研期间活跃在 PaaS 和 Docker 开源社区,对 Cloud Foundry 有深入研究和丰富实践,擅长底层平台代码分析,对分布式平台的架构有一定经验,撰写了大量有深度的技术博客。2014 年末以合伙人身份加入 DaoCloud 团队,致力于传播以 Docker 为主的容器的技术,推动互联网应用的容器化步伐。邮箱: allen.sun@daocloud.io

参考文献

http://aufs.sourceforge.net/aufs.html


感谢郭蕾对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015-07-14 00:196486

评论

发布
暂无评论
发现更多内容

上海丨阿里云 Serverless 技术实战营邀你来玩!

Serverless Devs

阿里云 Serverless 中间件

发现你的职业价值观:打造成功职业生涯的关键

Jack

云原生可观测性的几大误区

Yestodorrow

云原生 APM 监控 可观测性

JavaScript中eval和with语句如何影响作用域链:探索深度知识

控心つcrazy

《好好学习》:如何管理知识?

郭明

IDO&IEO盘点,包括PoseiSwap等即将面向市场的潜力打新活动

BlockChain先知

IDO&IEO盘点,包括PoseiSwap等即将面向市场的潜力打新活动

股市老人

Django笔记三十一之全局异常处理

Hunter熊

Python django 异常处理 全局异常

浅谈基于Shapley值的数据融合反欺骗数据判断相关

天翼云开发者社区

数据 Shapley

苹果系统更新:MacOS 11-13.x(PKG系统安装包及IPSW固件)

Rose

mac系统 macOS 13 Ventura 苹果最新系统 苹果系统下载

深入浅出 OkHttp 源码解析及应用实践

vivo互联网技术

okhttp 拦截器 源代码

全屋智能,始终在等一双“究极手”

脑极体

智能家居

Scrum进入疲惫期?三点帮你走出困境

敏捷开发

Scrum 敏捷开发 软件开发 Scrum Master 疲倦期

中央企业数字化转型专业委员会正式揭牌!

用友BIP

财务管理

HashMap 底层是如何实现的?

javacn.site

java面试

CDN网关超大range计算方法

天翼云开发者社区

CDN

IDO&IEO盘点,包括PoseiSwap等即将面向市场的潜力打新活动

EOSdreamer111

从 DevOps 到平台工程:软件开发的新范式

SEAL安全

DevOps 软件研发 平台工程

开通 ChatGPT Plus 的一些经验分享(66/100)

hackstoic

ChatGPT

旭阳数字郗维宝:数智化转型助力焦化企业打破行业困境

用友BIP

升级企业数智化底座 2023用友技术大会

标签系列:标签管理平台的架构与设计

Taylor

数据 标签 数据管理 标签体系

BATJ架构师首推!分布式事务原理与实战,出神入化

程序知音

Java 分布式 java架构 Java进阶 后端技术

打造绿色低碳存储方案,助推数据中心绿色长“存”

天翼云开发者社区

CIO

全球首款通过HDR Vivid认证的平板发布,华为视频全场景观影体验再升级

最新动态

2023-05-18:有 n 名工人。 给定两个数组 quality 和 wage , 其中,quality[i] 表示第 i 名工人的工作质量,其最低期望工资为 wage[i] 。 现在我们想雇佣

福大大架构师每日一题

Go 算法 rust

异常体系与项目实践

Java 架构

高级修图软件:Affinity Photo中文Mac版

真大的脸盆

Mac 图像处理 图像编辑 编辑图像 处理图像工具

VM虚拟机 v13.0.2激活版 for Mac许可秘钥

Rose

VMware Fusion Pro 13 VM虚拟机破解版 Mac虚拟机 VMware Fusion激活秘钥

展会回顾 | 2023元宇宙生态博览会圆满落幕,3DCAT荣获“元宇宙交互技术奖”

3DCAT实时渲染

元宇宙 实时云渲染 实时渲染云

C语言编程—常量

二哈侠

奋楫扬帆 津鸿智放丨中软国际携手深开鸿亮相第七届世界智能大会

科技热闻

Docker源码分析(十一):镜像存储_语言 & 开发_孙宏亮_InfoQ精选文章