本篇的内容主要是关于docker
镜像的。在我们安装好docker
之后,要想使用它,第
一步就是要下载一些镜像。本文将依据此流程分析docker
中镜像的拉取、存储等相关内容。
简介
docker
中几乎所有的操作都是通过WEB API
的方式执行的,所以当我们在命令行下敲下
docker pull
或者通过Docker Remote API
来拉取镜像时,docker
便准备好各项参数,
开始向内部的web server
发送http
请求,最终由提前注册好的Handlers
来执行相关
操作。我们将按照这个步骤来逐步分析与镜像拉取,存储相关的源码。
子命令执行
我们仍从docker
的main
函数入口处开始。在
docker 主程序分析
中里面我已经提到,如果没有-d
参数,最终便当作client
对待并且将参数当作子命令来
解析执行,代码如下:
1
2
3
4
5
6
7
8
9
|
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
可以是多种多样的,有以下几类:
- 只有名字 比如
ubuntu
- 名字和 tag 比如
ubuntu:14.04
- 命名空间,名字,(tag) 比如
tutum/redis
或者后面加个tag
- 前面有私有仓库地址 比如
127.0.0.1:5000/ubuntu:14.04
…
所以我们既要检测参数中是否包含非法字符,也要对这各种情况解析出正确的host
地址
和name
,tag
.不过了解了其结构之后,解析的代码就显得简单多了,不再具
体分析.
1
2
3
4
5
6
7
8
|
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-tags
为false
,则只会拉取tag
为latest
的镜像。
参数中的hostname
部分需要单独解析出来,因为有安全认证的考虑.需要读取相关的配置
文件,并解析参数,最终要作为http header
中的参数发送出去.
1
2
3
4
5
6
7
8
|
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
的相关环境的设定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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
,分别是pull
和import
,都
是用来创建镜像的,所以它们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
在拉取同一
个镜像:
1
2
3
4
5
6
7
8
9
10
|
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
可以对pull
和push
两个过程进行检测(通过第一个参数),然后通过一个map
确认是否已经有client
在拉取第二个参数标识的镜像。
名称解析
CmdPull
传入的参数有两个:image
和tag
。image
是包含仓库地址的,我们需要在拉取
之前将其解析出来,建立连接,并对镜像名做进一步规范化处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
}
|
注意remoteName
和localName
的区别。我们在拉取镜像时会发现有的镜像没有命名空间,
其实它是有一个默认值library
。remote
就是带了命名空间的规范化镜像名称.
Registry API 版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 是一种较新的架构,改
动较大,具体可见本文后面的链接。下面简要介绍一下官方仓库的拉取流程(示例):

-
docker client
从官方 Index(“index.docker.io/v1”)查询镜像(“samalba/busybox”)的
位置
-
Index 回复:
samalba/busybox
在Registry A
上
samalba/busybox
的校验码
- token
-
docker client
连接Registry A
表示自己要获取samalba/busybox
-
Registry A
询问Index
这个客户端(token/user
)是否有权限下载镜像
-
Index
回复是否可以下载
-
下载镜像的所有layers
V1 和 V2 的区分即是再registry
这一层。我们使用私有仓库的时候,都要在拉取的时候指定
仓库的 URL,这时候的流程与官方相比就少了Index
服务这一层,所以安全性就不高。而且
因为没有了校验码,即使镜像损坏,也无法检测到。V2 的设计就是想统一各个仓库之间的不
一致,规范其安全性和可靠性等。
具体再使用上,两者的区分现在主要是在官方镜像和非官方镜像之间(isOfficial
参数),如果是官方镜像
(ubuntun
,library/ubuntu
),则是从V2
拉取,否则是从v1
拉取。下面就以 V2 为中心分
析拉取流程。
在拉取之前,有一个叫trust_update_base
的job
先执行了,从名字上便知是与安全相关
的,也是 V2 引入的安全机制之一。docker
为此创建了一个新的
libtrust项目,感兴趣的可以自行参考一下。
trust_update_base
对应的job
在github.com/docker/docker/trust
包中,不再详述。
Tag 处理
镜像拉取首先要确定的是tag
,分两种情况,一种是指定了--all-tags
,需要获取所有
tag
的信息,另外就是单个的tag
,不管是自己指定的还是默认的latest
。
github.com/docker/docker/graph/pull.go#pullV2Repository
:
1
2
3
4
5
6
7
8
9
10
11
|
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
):

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

返回的结果就是一个tags
的string
列表。有了tags
列表,下面要做的就是遍历列表一
个一个地下载。
Manifest
要下载一个镜像,我们要先知道它的一些关键信息,比如校验码,层级,各层的联系以及其
他各种细节。所以首先要下载的便是这些manifest
数据:

先看下MainfestData
的定义:
1
2
3
4
5
6
7
8
|
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:5
的manifest
信息如下:
(1). fslayers
各层的checksum
信息

(2). History
各层的详细信息,json
格式:

(3). 签名及其他

github.com/docker/docker/graph/pull.go#pullV2Tag
:
1
2
3
4
5
6
|
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))
|
1
2
3
4
5
6
7
8
|
type downloadInfo struct {
imgJSON []byte
img *image.Image
tmpFile *os.File
length int64
downloaded bool
err chan error
}
|
downloadInfo
里最主要的信息便是镜像的json
描述文件,也是从ManifestData
中获取
的。tmpFile
的类型为*os.File
,表明真正的下载要开始了。
1
2
3
4
5
6
7
8
9
10
11
|
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
是否已经存在:
1
2
3
4
|
if s.graph.Exists(img.ID) {
log.Debugf("Image already exists: %s", img.ID)
continue
}
|
如果已经存在,表示本地已有此镜像,跳过。
(2). 冲突检测
之前提到过在镜像拉取之前有冲突检测,那个是针对指定的镜像名的(比如ubuntu
),而此
处的冲突主要是针对指定镜像名的各个layer
之间的。我们知道很多镜像底层的layer
都
是共享的,所以如果我们同时在拉取两个不同的镜像,其各自的layers
可能会有重叠的部
分,所以在每层layer
拉取之前都要检测:
1
2
3
4
5
6
7
8
9
|
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). 下载
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 本地文件名
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
之间是否是并行下载:
1
2
3
4
5
6
7
8
9
10
11
|
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
实例用来管理镜像,我们新下载的镜像也需要
向其“报到”,以纳入整个镜像关系网(树)中。
1
2
3
4
5
6
7
|
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
1
2
3
4
5
|
type Graph struct {
Root string
idIndex *truncindex.TruncIndex
driver graphdriver.Driver
}
|
三个字段:根目录,索引,底层driver
.TruncIndex
的作用是使我们可以用长 ID 的前缀来检索镜像:
1
2
3
4
5
|
type TruncIndex struct {
sync.RWMutex
trie *patricia.Trie
ids map[string]struct{}
}
|
patricia
是一个基数树的golang
实现。具体可参见go-patricia.
Dirver 则定义了一组抽象的文件系统接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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
的基本功能集:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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 验证
1
2
3
|
if err := utils.ValidateID(img.ID); err != nil {
return err
}
|
用正则表达式检验其是否包含非法字符,规则是只能包含英语字母和数字
2. 检查是否已经存在
1
2
3
4
5
6
7
8
9
|
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
为例探讨。
1
2
3
|
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)
}
|
首先,创建mnt
和diff
两个目录(在/var/lib/docker/aufs
下):
github.com/docker/docker/daemon/graphdriver/aufs/aufs.go#Create
:
1
2
3
|
if err := a.createDirsFor(id); err != nil {
return err
}
|
github.com/docker/docker/daemon/graphdriver/aufs/aufs.go#createDirsFor
:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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
:
1
2
3
4
|
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
:
1
2
3
4
5
6
7
8
9
|
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
:
1
2
3
4
5
6
7
8
|
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)
}
|
1
2
3
4
|
// 解压数据
func (a *Driver) applyDiff(id string, diff archive.ArchiveReader) error {
return chrootarchive.Untar(diff, path.Join(a.rootPath(), "diff", id), nil)
}
|
1
2
3
4
5
|
// 遍历目录计算大小
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
:
1
2
3
4
|
img.Size = size
if err := img.SaveSize(root); err != nil {
return err
}
|
同样的,json
描述文件也会暂时存在/var/lib/docker/graph/_tmp/<ID>/json
中:
1
2
3
4
5
6
7
8
|
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
:
1
2
3
4
5
|
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
:
1
2
3
|
if err := os.Rename(tmp, graph.ImageRoot(img.ID)); err != nil {
return err
}
|
5. 将镜像 ID 加入索引
github.com/docker/docker/graph/graph.go#Register
:
1
|
graph.idIndex.Add(img.ID)
|
即前面所说的TruncIndex
中。
Graph
结构存储了各个镜像的元数据及其之间的关系,但仍有一个维度的数据它没有建立
关联:名字。我们需要一个能够将镜像名字和tag
(ubuntu:14.04
)与其镜像
ID(d0955f21bf24
)关联起来的数据结构,这就是TagStore
:
github.com/docker/docker/graph/tags.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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
:
1
2
3
|
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
:
1
2
3
4
5
6
|
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
:
1
2
3
4
5
6
7
8
9
10
11
|
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
|
之后便是对repoName
和tag
的校验,最后将相关信息写入TagStore
的
Repositories
(map
)中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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/
下面的四个目录有关:
- ./aufs 镜像实际数据,layer 关系
- ./graph 镜像 json 描述文件,layersize
- ./tmp 镜像临时下载目录
- ./trust 认证相关
所以能实际结合这几个目录下的数据来分析源码,一定会事半功倍.
参考链接
- V2 Registry Talk
- Registry next generation
- Proposal: JSON Registry API V2.1
- The Docker Hub and the Registry spec
- Proposal: Private registry name-spacing as part of V2 image names
- Proposal: Self-describing images
- Proposal: Provenance step 1 - Transform images for validation and verification