本篇的内容主要是关于docker镜像的。在我们安装好docker之后,要想使用它,第 一步就是要下载一些镜像。本文将依据此流程分析docker中镜像的拉取、存储等相关内容。

简介

docker中几乎所有的操作都是通过WEB API的方式执行的,所以当我们在命令行下敲下 docker pull或者通过Docker Remote API来拉取镜像时,docker便准备好各项参数, 开始向内部的web server发送http请求,最终由提前注册好的Handlers来执行相关 操作。我们将按照这个步骤来逐步分析与镜像拉取,存储相关的源码。

子命令执行

我们仍从dockermain函数入口处开始。在 docker主程序分析 中里面我已经提到,如果没有-d参数,最终便当作client对待并且将参数当作子命令来 解析执行,代码如下:

if err := cli.Cmd(flag.Args()...); err != nil {
    if sterr, ok := err.(*utils.StatusError); ok {
        if sterr.Status != "" {
	        log.Println(sterr.Status)
        }
        os.Exit(sterr.StatusCode)
    }
    log.Fatal(err)
}

比如我在命令行下敲下docker pull ubuntu这个命令,那么在Cmd函数中,首先要做的 便是找到与pull相对应的handler.在这里并不是像一般的做法那样通过提前注册好的 map来查找,而是直接进行一些字符串转换,比如pull对应的叫CmdPull,push对应 的叫CmdPush,规则就是首字母大写并且加上一个Cmd前缀.

github.com/docker/docker/api/client/cli.go:

func (cli *DockerCli) getMethod(args ...string) (func(...string) error, bool) {
    camelArgs := make([]string, len(args))
    for i, s := range args {
        if len(s) == 0 {
            return nil, false
        }
        camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
    }
    methodName := "Cmd" + strings.Join(camelArgs, "")
    method := reflect.ValueOf(cli).MethodByName(methodName)
    if !method.IsValid() {
        return nil, false
    }
    return method.Interface().(func(...string) error), true
}

所以我们可以在github.com/docker/docker/api/client/commands.go文件中见到很多个 这样的函数:

发送请求

找到了执行函数,下面就是将其他参数传进去并开始执行,我们来看下CmdPull的流程:

我们在拉取镜像时,参数中的image name可以是多种多样的,有以下几类:

  1. 只有名字 比如ubuntu
  2. 名字和tag 比如ubuntu:14.04
  3. 命名空间,名字,(tag) 比如tutum/redis或者后面加个tag
  4. 前面有私有仓库地址 比如 127.0.0.1:5000/ubuntu:14.04

所以我们既要检测参数中是否包含非法字符,也要对这各种情况解析出正确的host地址 和name,tag.不过了解了其结构之后,解析的代码就显得简单多了,不再具 体分析.

taglessRemote, tag := parsers.ParseRepositoryTag(remote)
if tag == "" && !*allTags {
    newRemote = taglessRemote + ":" + graph.DEFAULTTAG
}

if tag != "" && *allTags {
    return fmt.Errorf("tag can't be used with --all-tags/-a")
}

注意其中的DEFAULTTAG,其值为latest,所以如果我们pull镜像时没有指定tag并且 --all-tagsfalse,则只会拉取taglatest的镜像。

参数中的hostname部分需要单独解析出来,因为有安全认证的考虑.需要读取相关的配置 文件,并解析参数,最终要作为http header中的参数发送出去.

hostname, _, err := registry.ResolveRepositoryName(taglessRemote)
if err != nil {
	return err
}

cli.LoadConfigFile()

authConfig := cli.configFile.ResolveAuthConfig(hostname)

需要注意的是,如果参数中没有指定hostname,则默认是从官方仓库拉取镜像,其值由变量 INDEXSERVER定义: https://index.docker.io/v1/

参数解析好之后,便可以执行http请求了。docker pull所发起的是POST请求,地址为 /iamges/create:

v  = url.Values{}

v.Set("fromImage", newRemote)

pull := func(authConfig registry.AuthConfig) error {
    buf, err := json.Marshal(authConfig)
    if err != nil {
        return err
    }
    registryAuthHeader := []string{
        base64.URLEncoding.EncodeToString(buf),
    }
        
    return cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{
        "X-Registry-Auth": registryAuthHeader,
    })
}

if err := pull(authConfig); err != nil {
    ...
}

web server端,我们可以查到/images/create所对应的handler名称。

github.com/docker/docker/api/server/server.go :

所以下面我们就开始分析postImageCreate的执行流程.

请求处理

之前提到过,daemon将各种操作统一为job的形式,镜像的创建也不例外。所以postImageCreate 的主要工作即是进行job的相关环境的设定:

image = r.Form.Get("fromImage")
repo  = r.Form.Get("repo")
tag   = r.Form.Get("tag")


if image != "" { //pull
    if tag == "" {
        image, tag = parsers.ParseRepositoryTag(image)
    }
    metaHeaders := map[string][]string{}
    for k, v := range r.Header {
        if strings.HasPrefix(k, "X-Meta-") {
            metaHeaders[k] = v
        }
    }
    job = eng.Job("pull", image, tag)
    job.SetenvBool("parallel", version.GreaterThan("1.3"))
    job.SetenvJson("metaHeaders", metaHeaders)
    job.SetenvJson("authConfig", authConfig)
}

需要注意的是,postImageCreate对应了两个subcommand,分别是pullimport,都 是用来创建镜像的,所以它们post的地址一样。二者通过传递不同的参数来区分创建不同 的job,具体就是fromImage这个参数.在CmdPull中,我们设置了这个参数,但是在CmdImport 中,则设置了其他的参数,二者流程类似,所以本文只对pull的流程做深入解析.

我们可以从github.com/docker/docker/graph/service.go文件中查到名字为pulljob对应的handler:

到了这个CmdPull(不要与前面的CmdPull搞混),才真正开始镜像拉取的过程。

镜像拉取

冲突检测

大家可能有过这样的经验,在一个终端下执行docker pull,时间太长,也不确定是不是成 功了,就Ctrl-C掉,然后重开一个终端重新执行,这时候就会提示已经有一个client在 拉取了,需要等待。这就是CmdPull最开始做的事情:确保只有一个client在拉取同一 个镜像:

c, err := s.poolAdd("pull", localName+":"+tag)
if err != nil {
    if c != nil {
        job.Stdout.Write(sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", localName))
        <-c
        return engine.StatusOK
    }
    return job.Error(err)
}
defer s.poolRemove("pull", localName+":"+tag)

pollAdd可以对pullpush两个过程进行检测(通过第一个参数),然后通过一个map 确认是否已经有client在拉取第二个参数标识的镜像。

名称解析

CmdPull传入的参数有两个:imagetagimage是包含仓库地址的,我们需要在拉取 之前将其解析出来,建立连接,并对镜像名做进一步规范化处理:

hostname, remoteName, err := registry.ResolveRepositoryName(localName)

endpoint, err := registry.NewEndpoint(hostname, s.insecureRegistries)

r, err := registry.NewSession(authConfig, registry.HTTPRequestFactory(metaHeaders), endpoint, true)

if endpoint.VersionString(1) == registry.IndexServerAddress() {
    localName = remoteName

    isOfficial = isOfficialName(remoteName)
    if isOfficial && strings.IndexRune(remoteName, '/') == -1 {
        remoteName = "library/" + remoteName
    }

    mirrors = s.mirrors
}

注意remoteNamelocalName的区别。我们在拉取镜像时会发现有的镜像没有命名空间, 其实它是有一个默认值libraryremote就是带了命名空间的规范化镜像名称.

Registry API 版本

if len(mirrors) == 0 && (isOfficial || endpoint.Version == registry.APIVersion2) {
    j := job.Eng.Job("trust_update_base")
    if err = j.Run(); err != nil {
        return job.Errorf("error updating trust base graph: %s", err)
    }

    if err := s.pullV2Repository(job.Eng, r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel")); err == nil {
        if err = job.Eng.Job("log", "pull", logName, "").Run(); err != nil {
            log.Errorf("Error logging event 'pull' for %s: %s", logName, err)
        }
        return engine.StatusOK
    } else if err != registry.ErrDoesNotExist {
        log.Errorf("Error from V2 registry: %s", err)
    }
}

if err = s.pullRepository(r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel"), mirrors); err != nil {
    return job.Error(err)
}

从代码中可以看到有两种拉取,分别对应于仓库的两个API(V1和V2)版本。V2是一种较新的架构,改 动较大,具体可见本文后面的链接。下面简要介绍一下官方仓库的拉取流程(示例):

  1. docker client从官方Index(“index.docker.io/v1”)查询镜像(“samalba/busybox”)的 位置
  2. Index回复:

    • samalba/busyboxRegistry A
    • samalba/busybox的校验码
    • token
  3. docker client 连接Registry A表示自己要获取samalba/busybox

  4. Registry A 询问Index 这个客户端(token/user)是否有权限下载镜像
  5. Index回复是否可以下载
  6. 下载镜像的所有layers

V1和V2的区分即是再registry这一层。我们使用私有仓库的时候,都要在拉取的时候指定 仓库的URL,这时候的流程与官方相比就少了Index服务这一层,所以安全性就不高。而且 因为没有了校验码,即使镜像损坏,也无法检测到。V2的设计就是想统一各个仓库之间的不 一致,规范其安全性和可靠性等。

具体再使用上,两者的区分现在主要是在官方镜像和非官方镜像之间(isOfficial参数),如果是官方镜像 (ubuntun,library/ubuntu),则是从V2拉取,否则是从v1拉取。下面就以V2为中心分 析拉取流程。

在拉取之前,有一个叫trust_update_basejob先执行了,从名字上便知是与安全相关 的,也是V2引入的安全机制之一。docker为此创建了一个新的 libtrust项目,感兴趣的可以自行参考一下。 trust_update_base对应的jobgithub.com/docker/docker/trust包中,不再详述。

Tag处理

镜像拉取首先要确定的是tag,分两种情况,一种是指定了--all-tags,需要获取所有 tag的信息,另外就是单个的tag,不管是自己指定的还是默认的latest

github.com/docker/docker/graph/pull.go#pullV2Repository:

tags, err := r.GetV2RemoteTags(remoteName, nil)
if err != nil {
    return err
}
for _, t := range tags {
    if downloaded, err := s.pullV2Tag(eng, r, out, localName, remoteName, t, sf, parallel); err != nil {
        return err
    } else if downloaded {
        layersDownloaded = true
    }
}

上面代码展示的便是要拉取所有tag的情况,用的仍是标准的REST API,GetV2RemoteTags便是一个标准的go语言的GET方法实现,不在详述。 我们可以直接从日志中查看到相关信息(docker pull -a centos):

也可以自己再命令行下用curlhttpie工具直接获取:

返回的结果就是一个tagsstring列表。有了tags列表,下面要做的就是遍历列表一 个一个地下载。

Manifest

要下载一个镜像,我们要先知道它的一些关键信息,比如校验码,层级,各层的联系以及其 他各种细节。所以首先要下载的便是这些manifest数据:

先看下MainfestData的定义:

type ManifestData struct {
    Name          string             `json:"name"`
    Tag           string             `json:"tag"`
    Architecture  string             `json:"architecture"`
    FSLayers      []*FSLayer         `json:"fsLayers"`
    History       []*ManifestHistory `json:"history"`
    SchemaVersion int                `json:"schemaVersion"`
}

在命令行下之下用httpie获取centos:5manifest信息如下:

(1). fslayers

各层的checksum信息

(2). History

各层的详细信息,json格式:

(3). 签名及其他

github.com/docker/docker/graph/pull.go#pullV2Tag:

log.Debugf("Pulling tag from V2 registry: %q", tag)
manifestBytes, err := r.GetV2ImageManifest(remoteName, tag, nil)
// 验证各项信息是否正确
manifest, verified, err := s.verifyManifest(eng, manifestBytes)

downloads := make([]downloadInfo, len(manifest.FSLayers))
type downloadInfo struct {
    imgJSON    []byte
    img        *image.Image
    tmpFile    *os.File
    length     int64
    downloaded bool
    err        chan error
}

downloadInfo里最主要的信息便是镜像的json描述文件,也是从ManifestData中获取 的。tmpFile的类型为*os.File,表明真正的下载要开始了。

for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
    var (
        sumStr  = manifest.FSLayers[i].BlobSum
        imgJSON = []byte(manifest.History[i].V1Compatibility)
    )

    img, err := image.NewImgJSON(imgJSON)
    if err != nil {
        return false, fmt.Errorf("failed to parse json: %s", err)
    }
    downloads[i].img = img

下载

整个下载流程大概分为以下几步:

(1). 确认此镜像(layer)的ID是否已经存在:

if s.graph.Exists(img.ID) {
    log.Debugf("Image already exists: %s", img.ID)
    continue
}

如果已经存在,表示本地已有此镜像,跳过。

(2). 冲突检测 之前提到过在镜像拉取之前有冲突检测,那个是针对指定的镜像名的(比如ubuntu),而此 处的冲突主要是针对指定镜像名的各个layer之间的。我们知道很多镜像底层的layer都 是共享的,所以如果我们同时在拉取两个不同的镜像,其各自的layers可能会有重叠的部 分,所以在每层layer拉取之前都要检测:

if c, err := s.poolAdd("pull", "img:"+img.ID); err != nil {
    if c != nil {
        out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Layer already being pulled by another client. Waiting.", nil))
        <-c
        out.Write(sf.FormatProgress(utils.TruncateID(img.ID), "Download complete", nil))
    } else {
        log.Debugf("Image (id: %s) pull is already running, skipping: %v", img.ID, err)
    }
} 

(3). 下载

// 本地文件名
tmpFile, err := ioutil.TempFile("", "GetV2ImageBlob")
if err != nil {
    return err
}

// 下载
r, l, err := r.GetV2ImageBlobReader(remoteName, sumType, checksum, nil)
if err != nil {
    return err
}
defer r.Close()
io.Copy(tmpFile, utils.ProgressReader(r, int(l), out, sf, false, utils.TruncateID(img.ID), "Downloading"))

docker有自己的临时目录,一般是/var/lib/docker/tmp/,里面都是这种 GetV2RemoteTags开头的文件。GetV2ImageBlobReader仍是执行GET请求,我们可以从 日志里看到其URL的格式:

下载地址主要是由Manifest中的blobSum字段组成,在浏览器里粘贴就可以下载这个文 件(最终重定向到AWS)。

在之前的CmdPull job中,有一个parallel的参数,它用来控制下载时各layer之间是否是并行下载:

if parallel {
    downloads[i].err = make(chan error)
    go func(di *downloadInfo) {
        di.err <- downloadFunc(di)
    }(&downloads[i])
} else {
    err := downloadFunc(&downloads[i])
    if err != nil {
        return false, err
    }
}

docker版本1.3以上的都是并行下载。

镜像存储

docker的使用过程中,本地缓存的镜像会越来越多,我们需要有一个组件来管理这些镜 像及其之间的关系,在 Daemon启动流程 中我们提到的graph就是起这个作用。daemon启动时会创建一个Graph实例用来管理镜像,我们新下载的镜像也需要 向其“报到”,以纳入整个镜像关系网(树)中。

if d.tmpFile != nil {
    err = s.graph.Register(d.img,
        utils.ProgressReader(d.tmpFile, int(d.length), out, sf, false, utils.TruncateID(d.img.ID), "Extracting"))
    if err != nil {
        return false, err
    }
}

所以我们需要先探究以下Graph的具体构造。

Graph

type Graph struct {
    Root    string
    idIndex *truncindex.TruncIndex
    driver  graphdriver.Driver
}

三个字段:根目录,索引,底层driver.TruncIndex的作用是使我们可以用长ID的前缀来检索镜像:

type TruncIndex struct {
    sync.RWMutex
    trie *patricia.Trie
    ids  map[string]struct{}
}

patricia是一个基数树的golang实现。具体可参见go-patricia.

Dirver则定义了一组抽象的文件系统接口:

type Driver interface {
    ProtoDriver
    // Diff produces an archive of the changes between the specified
    // layer and its parent layer which may be "".
    Diff(id, parent string) (archive.Archive, error)
    // Changes produces a list of changes between the specified layer
    // and its parent layer. If parent is "", then all changes will be ADD changes.
    Changes(id, parent string) ([]archive.Change, error)
    // ApplyDiff extracts the changeset from the given diff into the
    // layer with the specified id and parent, returning the size of the
    // new layer in bytes.
    ApplyDiff(id, parent string, diff archive.ArchiveReader) (bytes int64, err error)
    // DiffSize calculates the changes between the specified id
    // and its parent and returns the size in bytes of the changes
    // relative to its base filesystem directory.
    DiffSize(id, parent string) (bytes int64, err error)
}

PhotoDriver则定义了一个driver的基本功能集:

type ProtoDriver interface {
    // String returns a string representation of this driver.
    String() string
    // Create creates a new, empty, filesystem layer with the
    // specified id and parent. Parent may be "".
    Create(id, parent string) error
    // Remove attempts to remove the filesystem layer with this id.
    Remove(id string) error
    // Get returns the mountpoint for the layered filesystem referred
    // to by this id. You can optionally specify a mountLabel or "".
    // Returns the absolute path to the mounted layered filesystem.
    Get(id, mountLabel string) (dir string, err error)
    // Put releases the system resources for the specified id,
    // e.g, unmounting layered filesystem.
    Put(id string)
    // Exists returns whether a filesystem layer with the specified
    // ID exists on this driver.
    Exists(id string) bool
    // Status returns a set of key-value pairs which give low
    // level diagnostic status about this driver.
    Status() [][2]string
    // Cleanup performs necessary tasks to release resources
    // held by the driver, e.g., unmounting all layered filesystems
    // known to this driver.
    Cleanup() error
}

daemon启动时,调用了NewGraph接口,如果本身没有镜像的话,那么这个函数所做的 基本上只是一些变量的初始化,如果本身已经有镜像存在,则需要重新读取并建立它们之间 的联系。

Register

一个新的镜像(layer)向Graph注册主要有以下流程:

1. ID验证

if err := utils.ValidateID(img.ID); err != nil {
    return err
}

用正则表达式检验其是否包含非法字符,规则是只能包含英语字母和数字

2. 检查是否已经存在

if graph.Exists(img.ID) {
    return fmt.Errorf("Image %s already exists", img.ID)
}

if err := os.RemoveAll(graph.ImageRoot(img.ID)); err != nil && !os.IsNotExist(err) {
    return err
}

graph.driver.Remove(img.ID)

之所以要这么多步删除是为了应对一些特殊情况,比如切换graph driver等,这时候就可 能信息不一致的地方。

3. 创建image rootfs

到这一步就牵扯到了具体的graph driver实现了,ubuntu上现在都是aufs,下面就以 aufs为例探讨。

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)
}

首先,创建mntdiff两个目录(在/var/lib/docker/aufs下):

github.com/docker/docker/daemon/graphdriver/aufs/aufs.go#Create:

if err := a.createDirsFor(id); err != nil {
	return err
}

github.com/docker/docker/daemon/graphdriver/aufs/aufs.go#createDirsFor:

func (a *Driver) createDirsFor(id string) error {
    paths := []string{
        "mnt",
        "diff",
    }

    for _, p := range paths {
        if err := os.MkdirAll(path.Join(a.rootPath(), p, id), 0755); err != nil {
            return err
        }
    }
    return nil
}

然后,创建layers目录,里面记录了镜像之间的层级关系:

github.com/docker/docker/daemon/graphdriver/aufs/aufs.go#Create:

f, err := os.Create(path.Join(a.rootPath(), "layers", id))
if err != nil {
    return err
}

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
        }
    }
}

我们可以在/var/lib/docker/aufs/layers目录下找一个文件看一下其中的内容:

每一行代表一个镜像,每一行都是上一行的parent镜像。可以猜想大多数的最后一行都一 样,即来自于同一个基本镜像 511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158

4. 解压数据,计算layersize

github.com/docker/docker/graph/graph.go#Register:

img.SetGraph(graph)
if err := image.StoreImage(img, layerData, tmp); err != nil {
	return err
}

tmp即是/var/lib/docker/graph/_tmp,用来暂时存储数据。

首先,解压数据,并存入/var/lib/docker/diff中:

github.com/docker/docker/image/image.go#StoreImage:

layerDataDecompressed, err := archive.DecompressStream(layerData)

if layerTarSum, err = tarsum.NewTarSum(layerDataDecompressed, true, tarsum.VersionDev); err != nil {
    return err
}

if size, err = driver.ApplyDiff(img.ID, img.Parent, layerTarSum); err != nil {
    return err
}

实际的数据解压存储实在driver.ApplyDiff中执行的,aufs的实现中是不需要parent image id的,所以较为简单,只用简单地解压数据并计算大小即可:

github.com/docker/daemon/graphdriver/aufs/aufs.go:

func (a *Driver) ApplyDiff(id, parent string, diff archive.ArchiveReader) (bytes int64, err error) {
    // AUFS doesn't need the parent id to apply the diff.
    if err = a.applyDiff(id, diff); err != nil {
        return
    }

    return a.DiffSize(id, parent)
}
// 解压数据
func (a *Driver) applyDiff(id string, diff archive.ArchiveReader) error {
    return chrootarchive.Untar(diff, path.Join(a.rootPath(), "diff", id), nil)
}
// 遍历目录计算大小
func (a *Driver) DiffSize(id, parent string) (bytes int64, err error) {
    // AUFS doesn't need the parent layer to calculate the diff size.
    return utils.TreeSize(path.Join(a.rootPath(), "diff", id))
}

计算好的大小会暂时存在/var/lib/docker/graph/_tmp/<ID>/layersize文件中:

github.com/docker/docker/image/image.go#StoreImage:

img.Size = size
if err := img.SaveSize(root); err != nil {
    return err
}

同样的,json描述文件也会暂时存在/var/lib/docker/graph/_tmp/<ID>/json中:

f, err := os.OpenFile(jsonPath(root), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
if err != nil {
    return err
}

defer f.Close()

return json.NewEncoder(f).Encode(img)

在整个流程中也包含了校验码的对比验证:

github.com/docker/docker/image/image.go#StoreImage:

checksum := layerTarSum.Sum(nil)

if img.Checksum != "" && img.Checksum != checksum {
    log.Warnf("image layer checksum mismatch: computed %q, expected %q", checksum, img.Checksum)
}

image.StoreImage结束后,将_tmp下的数据移到正式的目录里面:

github.com/docker/docker/graph/graph.go#Register:

if err := os.Rename(tmp, graph.ImageRoot(img.ID)); err != nil {
    return err
}

5. 将镜像ID加入索引

github.com/docker/docker/graph/graph.go#Register:

graph.idIndex.Add(img.ID)

即前面所说的TruncIndex中。

TagStore

Graph结构存储了各个镜像的元数据及其之间的关系,但仍有一个维度的数据它没有建立 关联:名字。我们需要一个能够将镜像名字和tag(ubuntu:14.04)与其镜像 ID(d0955f21bf24)关联起来的数据结构,这就是TagStore

github.com/docker/docker/graph/tags.go

type TagStore struct {
    path               string
    graph              *Graph
    mirrors            []string
    insecureRegistries []string
    Repositories       map[string]Repository
    sync.Mutex
    // FIXME: move push/pull-related fields
    // to a helper type
    pullingPool map[string]chan struct{}
    pushingPool map[string]chan struct{}
}

type Repository map[string]string

Daemon启动流程中 中已经提到,TagStore的数据存在/var/lib/docker/graph/repositories-<driver-name>中,对aufs来说,就是 repositories-aufs,里面记录了镜像名与镜像ID之间的映射:

在镜像向Graph注册之后,我们也需要向TagStore注册:

github.com/docker/docker/graph/pull.go#pullV2Tag:

if err = s.Set(localName, tag, downloads[0].img.ID, true); err != nil {
    return false, err
}

localname是没有进行过命名空间规整的镜像名(即不会有额外的library/)。 downloadds[0].img.ID即是与镜像名相关联的镜像ID(在Manifest数据中的 History列表中的第一位)。

首先获取这个镜像的Image对象:

github.com/docker/docker/graph/tags.go#Set:

img, err := store.LookupImage(imageName)
store.Lock()
defer store.Unlock()
if err != nil {
    return err
}

如果在TagStore中找不到的话会到Graph中寻找: github.com/docker/docker/graph/tags.go#LookupImage:

img, err := store.GetImage(repos, tag)
store.Lock()
defer store.Unlock()
if err != nil {
    return nil, err
} else if img == nil {
    if img, err = store.graph.Get(name); err != nil {
        return nil, err
    }
}
return img,nil

之后便是对repoNametag的校验,最后将相关信息写入TagStoreRepositories(map)中。

if err := store.reload(); err != nil {
    return err
}
var repo Repository
if r, exists := store.Repositories[repoName]; exists {
    repo = r
    if old, exists := store.Repositories[repoName][tag]; exists && !force {
        return fmt.Errorf("Conflict: Tag %s is already set to image %s, if you want to replace it, please use -f option", tag, old)
    }
} else {
    repo = make(map[string]string)
    store.Repositories[repoName] = repo
}
repo[tag] = img.ID
return store.save()

总结

镜像的拉取和存储主要与/var/lib/docker/下面的四个目录有关:

  1. ./aufs 镜像实际数据,layer关系
  2. ./graph 镜像json描述文件,layersize
  3. ./tmp 镜像临时下载目录
  4. ./trust 认证相关

所以能实际结合这几个目录下的数据来分析源码,一定会事半功倍.

参考链接

  1. V2 Registry Talk
  2. Registry next generation
  3. Proposal: JSON Registry API V2.1
  4. The Docker Hub and the Registry spec
  5. Proposal: Private registry name-spacing as part of V2 image names
  6. Proposal: Self-describing images
  7. Proposal: Provenance step 1 - Transform images for validation and verification