Docker源码分析(十一):镜像存储
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所示:
来自:http://www.infoq.com/cn/articles/docker-source-code-analysis-part11