Hang

docker 源码分析(5) -- 镜像拉取及存储

2015-03-30

本篇的内容主要是关于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文件中查到名字为pull
job对应的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的校验,最后将相关信息写入TagStore
Repositories(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
Tags: docker