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

jopen 9年前

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所示:

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

图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所示:


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

来自:http://www.infoq.com/cn/articles/docker-source-code-analysis-part11