(十):Docker镜像下载
最后更新于:2022-04-01 04:49:19
[TOC=3]
## 1.前言
说Docker Image是Docker体系的价值所在,没有丝毫得夸大其词。Docker Image作为容器运行环境的基石,彻底解放了Docker容器创建的生命力,也激发了用户对于容器运用的无限想象力。
玩转Docker,必然离不开Docker Image的支持。然而“万物皆有源”,Docker Image来自何方,Docker Image又是通过何种途径传输到用户机器,以致用户可以通过Docker Image创建容器?回忆初次接触Docker的场景,大家肯定对两条命令不陌生:docker pull和docker run。这两条命令中,正是前者实现了Docker Image的下载。Docker Daemon在执行这条命令时,会将Docker Image从Docker Registry下载至本地,并保存在本地Docker Daemon管理的graph中。
谈及Docker Registry,Docker爱好者首先联想到的自然是[Docker Hub](https://hub.docker.com/)。Docker Hub作为Docker官方支持的Docker Registry,拥有全球成千上万的Docker Image。全球的Docker爱好者除了可以下载Docker Hub开放的镜像资源之外,还可以向Docker Hub贡献镜像资源。在Docker Hub上,用户不仅可以享受公有镜像带来的便利,而且可以创建私有镜像库。Docker Hub是全国最大的Public Registry,另外Docker还支持用户自定义创建Private Registry。Private Registry主要的功能是为私有网络提供Docker镜像的专属服务,一般而言,镜像种类适应用户需求,私密性较高,且不会占用公有网络带宽。
## 2.本文分析内容安排
本文作为《Docker源码分析》系列的第十篇——Docker镜像下载篇,主要从源码的角度分析Docker下载Docker Image的过程。分析流程中,docker的版本均为1.2.0。
分析内容的安排如以下4部分:
(1) 概述Docker镜像下载的流程,涉及Docker Client、Docker Server与Docker Daemon;
(2) Docker Client处理并发送docker pull请求;
(3) Docker Server接收docker pull请求,并创建镜像下载任务并触发执行;
(4) Docker Daemon执行镜像下载任务,并存储镜像至graph。
## 3.Docker镜像下载流程
Docker Image作为Docker生态中的精髓,下载过程中需要Docker架构中多个组件的协作。Docker镜像的下载流程如图3.1:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-12_56443b0bb7560.jpg)
图3.1 Docker镜像下载流程图
如上图,下载流程,可以归纳为以上3个步骤:
(1) 用户通过Docker Client发送pull请求,作用为:让Docker Daemon下载指定名称的镜像;
(2) Docker Daemon中负责Docker API请求的Docker Server,接收Docker镜像的pull请求,创建下载镜像任务并触发执行;
(3) Docker Daemon执行镜像下载任务,从Docker Registry中下载指定镜像,并将其存储与本地的graph中。
下文即从三个方面分析docker pull请求执行的流程。
## 4.Docker Client
Docker架构中,Docker用户的角色绝大多数由Docker Client来扮演。因此,用户对Docker的管理请求全部由Docker Client来发送,Docker镜像下载请求自然也不例外。
为了更清晰的描述Docker镜像下载,本文结合具体的命令进行分析,如下:
~~~
docker pull ubuntu:14.04
~~~
以上的命令代表:用户通过docker二进制可执行文件,执行pull命令,镜像参数为ubuntu:14.04,镜像名称为ubuntu,镜像标签为14.04。此命令一经触发,第一个接受并处理的Docker组件为Docker Client,执行内容包括以下三个步骤:
(1) 解析命令中与Docker镜像相关的参数;
(2) 配置Docker下载镜像时所需的认证信息;
(3) 发送RESTful请求至Docker Daemon。
### 4.1 解析镜像参数
通过docker二进制文件执行docker pull ubuntu:14.04 时,Docker Client首先会被创建,随后通过参数处理分析出请求类型pull,最终执行pull请求相应的处理函数。关于Docker Client的创建与命令执行可以参见[《Docker源码分析》系列第二篇——Docker Client篇](http://www.infoq.com/cn/articles/docker-source-code-analysis-part2)。
Docker Client执行pull请求相应的处理函数,源码位于[./docker/api/client/command.go#L1183-L1244](https://github.com/docker/docker/blob/v1.2.0/api/client/commands.go#L1183-L1244),有关提取镜像参数的源码如下:
~~~
func (cli *DockerCli) CmdPull(args ...string) error {
cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry")
tag := cmd.String([]string{"#t", "#-tag"}, "", "Download tagged image in a repository")
if err := cmd.Parse(args); err != nil {
return nil
}
if cmd.NArg() != 1 {
cmd.Usage()
return nil
}
var (
v = url.Values{}
remote = cmd.Arg(0)
)
v.Set("fromImage", remote)
if *tag == "" {
v.Set("tag", *tag)
}
remote, _ = parsers.ParseRepositoryTag(remote)
// Resolve the Repository name from fqn to hostname + name
hostname, _, err := registry.ResolveRepositoryName(remote)
if err != nil {
return err
}
……
}
~~~
结合命令docker pull ubuntu:14.04,来分析CmdPull函数的定义,可以发现,该函数传入的形参为args,实参只有一个字符串ubuntu:14.04。另外,纵观以上源码,可以发现Docker Client解析的镜像参数无外乎4个:tag、remote、v和hostname,四者各自的作用如下:
* tag:带有Docker镜像的标签;
* remote:带有Docker镜像的名称与标签;
* v:类型为url.Values,实质是一个map类型,用于配置请求中URL的查询参数;
* hostname:Docker Registry的地址,代表用户希望从指定的Docker Registry下载Docker镜像。
#### 4.1.1 解析tag参数
Docker镜像的tag参数,是第一个被Docker Client解析的镜像参数,代表用户所需下载Docker镜像的标签信息,如:docker pull ubuntu:14.04请求中镜像的tag信息为14.04,若用户使用docker pull ubuntu请求下载镜像,没有显性指定tag信息时,Docker Client会默认该镜像的tag信息为latest。
Docker 1.2.0版本除了以上的tag信息传入方式,依旧保留着代表镜像标签的flag参数tag,而这个flag参数在1.2.0版本的使用过程中已经被遗弃,并会在之后新版本的Docker中被移除,因此在使用docker 1.2.0版本下载Docker镜像时,不建议使用flag参数tag。传入tag信息的方式,建议使用docker pull NAME[:TAG]的形式。
Docker 1.2.0版本依旧保留的flag参数tag,其定义与解析的源码位于:[./docker/api/client/commands.go#1185-L1188](https://github.com/docker/docker/blob/v1.2.0/api/client/commands.go#L1185-L1188),如下:
~~~
tag := cmd.String([]string{"#t", "#-tag"}, "", "Download tagged image in a repository")
if err := cmd.Parse(args); err != nil {
return nil
}
~~~
以上的源码说明:CmdPull函数解析tag参数时,Docker Client首先定义一个flag参数,flag参数的名称为”#t”或者 “#-tag”,用途为:指定Docker镜像的tag参数,默认值为空字符串;随后通过cmd.Parse(args)的执行,解析args中的tag参数。
#### 4.1.2 解析remote参数
Docker Client解析完tag参数之后,同样需要解析出Docker镜像所属的repository,如请求docker pull ubuntu:14.04中,Docker镜像为ubuntu:14.04,镜像的repository信息为ubuntu,镜像的tag信息为14.04。
Docker Client通过解析remote参数,使得remote参数携带repository信息和tag信息。Docker Client解析remote参数的第一个步骤,源码如下:
~~~
remote = cmd.Arg(0)
~~~
其中,cmd的第一个参数赋值给remote,以docker pull ubuntu:14.04为例,cmd.Arg(0)为ubuntu:14.04,则赋值后remote值为ubuntu:14.04。此时remote参数即包含Docker镜像的repository信息也包含tag信息。若用户请求中带有Docker Registry的信息,如docker pull localhost.localdomain:5000/docker/ubuntu:14.04,cmd.Arg(0)为localhost.localdomain:5000/docker/ubuntu:14.04,则赋值后remote值为localhost.localdomain:5000/docker/ubuntu:14.04,此时remote参数同时包含repository信息、tag信息以及Docker Registry信息。
随后,在解析remote参数的第二个步骤中,Docker Client通过解析赋值完毕的remote参数,从中解析中repository信息,并再次覆写remote参数的值,源码如下:
~~~
remote, _ = parsers.ParseRepositoryTag(remote)
~~~
ParseRepositoryTag的作用是:解析出remote参数的repository信息和tag信息,该函数的实现位于[./docker/pkg/parsers/parsers.go#L72-L81](https://github.com/docker/docker/blob/v1.2.0/pkg/parsers/parsers.go#L72-L81),源码如下:
~~~
func ParseRepositoryTag(repos string) (string, string) {
n := strings.LastIndex(repos, ":")
if n < 0 {
return repos, ""
}
if tag := repos[n+1:]; !strings.Contains(tag, "/") {
return repos[:n], tag
}
return repos, ""
}
~~~
以上函数的实现过程,充分考虑了多种不同Docker Registry的情况,如:请求docker pull ubuntu:14.04中remote参数为ubuntu:14.04,而请求docker pull localhost.localdomain:5000/docker/ubuntu:14.04中用户指定了Docker Registry的地址localhost.localdomain:5000/docker,故remote参数还携带了Docker Registry信息。
ParseRepositoryTag函数首先从repos参数的尾部往前寻找”:”,若不存在,则说明用户没有显性指定Docker镜像的tag,返回整个repos作为Docker镜像的repository;若”:”存在,则说明用户显性指定了Docker镜像的tag,”:”前的内容作为repository信息,”:”后的内容作为tag信息,并返回两者。
ParseRepositoryTag函数执行完,回到CmdPull函数,返回内容的repository信息将覆写remote参数。对于请求docker pull localhost.localdomain:5000/docker/ubuntu:14.04,remote参数被覆写后,值为localhost.localdomain:5000/docker/ubuntu,携带Docker Registry信息以及repository信息。
#### 4.1.3 配置url.Values
Docker Client发送请求给Docker Server时,需要为请求配置URL的查询参数。CmdPull函数的执行过程中创建url.Value并配置的源码实现位于[./docker/api/client/commands.go#L1194-L1203](https://github.com/docker/docker/blob/v1.2.0/api/client/commands.go#L1194-L1203),如下:
~~~
var (
v = url.Values{}
remote = cmd.Arg(0)
)
v.Set("fromImage", remote)
if *tag == "" {
v.Set("tag", *tag)
}
~~~
其中,变量v的类型url.Values,配置的URL查询参数有两个,分别为”fromImage”与”tag”,”fromImage”的值是remote参数没有被覆写时值,”tag”的值一般为空,原因是一般不使用flag参数tag。
#### 4.1.4 解析hostname参数
Docker Client解析镜像参数时,还有一个重要的环节,那就是解析Docker Registry的地址信息。这意味着用户希望从指定的Docker Registry中下载Docker镜像。
解析Docker Registry地址的代码实现位于[./docker/api/client/commands.go#L1207](https://github.com/docker/docker/blob/v1.2.0/api/client/commands.go#L1207),如下:
~~~
hostname, _, err := registry.ResolveRepositoryName(remote)
~~~
Docker Client通过包registry中的函数ResolveRepositoryName来解析hostname参数,传入的实参为remote,即去tag化的remote参数。ResolveRepositoryName函数的实现位于[./docker/registry/registry.go#L237-L259](https://github.com/docker/docker/blob/v1.2.0/registry/registry.go),如下:
~~~
func ResolveRepositoryName(reposName string) (string, string, error) {
if strings.Contains(reposName, "://") {
// It cannot contain a scheme!
return "", "", ErrInvalidRepositoryName
}
nameParts := strings.SplitN(reposName, "/", 2)
if len(nameParts) == 1
|| (!strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":") &&
nameParts[0] != "localhost") {
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
err := validateRepositoryName(reposName)
return IndexServerAddress(), reposName, err
}
hostname := nameParts[0]
reposName = nameParts[1]
if strings.Contains(hostname, "index.docker.io") {
return "", "", fmt.Errorf("Invalid repository name, try \"%s\" instead", reposName)
}
if err := validateRepositoryName(reposName); err != nil {
return "", "", err
}
return hostname, reposName, nil
}
~~~
ResolveRepositoryName函数首先通过”/”分割字符串reposName,如下:
~~~
nameParts := strings.SplitN(reposName, "/", 2)
~~~
如果nameParts的长度为1,则说明reposName中不含有字符”/”,意味着用户没有指定Docker Registry。另外,形如”samalba/hipache”的reposName同样说明用户并没有指定Docker Registry。当用户没有指定Docker Registry时,Docker Client默认返回IndexServerAddress(),该函数返回常量INDEXSERVER,值为”https://index.docker.io/v1”。也就是说,当用户下载Docker镜像时,若不指定Docker Registry,默认情况下,Docker Client通知Docker Daemon去Docker Hub上下载镜像。例如:请求docker pull ubuntu:14.04,由于没有指定Docker Registry,Docker Client默认使用全球最大的Docker Registry——Docker Hub。
当不满足返回默认Docker Registry时,Docker Client通过解析reposNames,得出用户指定的Docker Registry地址。例如:请求docker pull localhost.localdomain:5000/docker/ubuntu:14.04中,解析出的Docker Registry地址为localhost.localdomain:5000。
至此,与Docker镜像相关的参数已经全部解析完毕,Docker Client将携带这部分重要信息,以及用户的认证信息,构建RESTful请求,发送给Docker Server。
### 4.2 配置认证信息
用户下载Docker镜像时,Docker同样支持用户信息的认证。用户认证信息由Docker Client配置;Docker Client发送请求至Docker Server时,用户认证信息也被一并发送;随后,Docker Daemon处理下载Docker镜像请求时,用户认证信息在Docker Registry被验证。
Docker Client配置用户认证信息包含两个步骤,实现源码如下:
~~~
cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(hostname)
~~~
可见,第一个步骤是使cli(Docker Client)加载ConfigFile,ConfigFile是Docker Client用来存放有关Docker Registry的用户认证信息的对象。DockerCli、ConfigFile以及AuthConfig三种数据结构之间的关系如图4.1:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-12_56443b0c02f23.jpg)
图4.1 DockerCli、ConfigFile以及AuthConfig关系图
DockerCli结构体的属性configFile为一个指向registry.ConfigFile的指针,而ConfigFile结构体的属性Configs属于map类型,其中key为string,代表Docker Registry的地址,value的类型为AuthConfig。AuthConfig类型具体含义为用户在某个Docker Registry上的认证信息,包含用户名,密码,认证信息,邮箱地址等。
加载完用户所有的认证信息之后,Docker Client第二个步骤是:通过用户指定的Docker Registry,即之前解析出的hostname参数,从用户所有的认证信息中找出与指定hostname相匹配的认证信息。新创建的authConfig,类型即为AuthConfig,将会作为用户在指定Docker Registry上的认证信息,发送至Docker Server。
### 4.3 发送API请求
Docker Client解析完所有的Docker镜像参数,并且配置完毕用户的认证信息之后,Docker Client需要使用这些信息正式发送镜像下载的请求至Docker Server。
Docker Client定义了pull函数,来实现发送镜像下载请求至Docker Server,源码位于[./docker/api/client/commands.go#L1217-L1229](https://github.com/docker/docker/blob/v1.2.0/api/client/commands.go#L1217-L1229),如下:
~~~
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,
})
}
~~~
pull函数的实现较为简单,首先通过authConfig对象,创建registryAuthHeader,最后发送POST请求,请求的URL为"/images/create?"+v.Encode(),在URL中传入查询参数包括”fromImage”与”tag”,另外在请求的HTTP Header中添加认证信息registryAuthHeader,。
执行以上的pull函数时,Docker镜像下载请求被发送,随后Docker Client等待Docker Server的接收、处理与响应。
## 5.Docker Server
Docker Server作为Docker Daemon的入口,所有Docker Client发送请求都由Docker Server接收。Docker Server通过解析请求的URL与请求方法,最终路由分发至相应的handler来处理。Docker Server的创建与请求处理,可以参看[《Docker源码分析》系列之Docker Server篇](http://www.infoq.com/cn/articles/docker-source-code-analysis-part5)。
Docker Server接收到镜像下载请求之后,通过路由分发最终由具体的handler——postImagesCreate来处理。postImagesCreate的实现位于[./docker/api/server/server.go#L466-L524](https://github.com/docker/docker/blob/v1.2.0/api/server/server.go#L466-L524),的、其执行流程主要分为3个部分:
(1) 解析HTTP请求中包含的请求参数,包括URL中的查询参数、HTTP header中的认证信息等;
(2) 创建镜像下载job,并为该job配置环境变量;
(3) 触发执行镜像下载job。
### 5.1 解析请求参数
Docker Server接收到Docker Client发送的镜像下载请求之后,首先解析请求参数,并未后续job的创建与运行提供参数依据。Docker Server解析的请求参数,主要有:HTTP请求URL中的查询参数”fromImage”、”repo”以及”tag”,以及有HTTP请求的header中的”X-Registry-Auth”。
请求参数解析的源码如下:
~~~
var (
image = r.Form.Get("fromImage")
repo = r.Form.Get("repo")
tag = r.Form.Get("tag")
job *engine.Job
)
authEncoded := r.Header.Get("X-Registry-Auth")
~~~
需要特别说明的是:通过”fromImage”解析出的image变量包含镜像repository名称与镜像tag信息。例如用户请求为docker pull ubuntu:14.04,那么通过”fromImage”解析出的image变量值为ubuntu:14.04,并非只有Docker镜像的名称。
另外,Docker Server通过HTTP header中解析出authEncoded,还原出类型为registry.AuthConfig的对象authConfig,源码实现如下:
~~~
authConfig := ®istry.AuthConfig{}
if authEncoded != "" {
authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
if err := json.NewDecoder(authJson).Decode(authConfig); err != nil {
// for a pull it is not an error if no auth was given
// to increase compatibility with the existing api it is defaulting to be empty
authConfig = ®istry.AuthConfig{}
}
}
~~~
解析出HTTP请求中的参数之后,Docker Server对于image参数,再次进行解析,从中解析出属于repository与tag信息,其中repository有可能暂时包含Docker Registry信息,源码实现如下:
~~~
if tag == "" {
image, tag = parsers.ParseRepositoryTag(image)
}
~~~
Docker Server的参数解析工作至此全部完成,在这之后Docker Server将创建镜像下载任务并开始执行。
### 5.2 创建并配置job
Docker Server只负责接收Docker Client发送的请求,并将其路由分发至相应的handler来处理,最终的请求执行还是需要Docker Daemon来协作完成。Docker Server在handler中,通过创建job并触发job执行的形式,把控制权交于Docker Daemon。
Docker Server创建镜像下载job并配置环境变量的源码实现如下:
~~~
job = eng.Job("pull", image, tag)
job.SetenvBool("parallel", version.GreaterThan("1.3"))
job.SetenvJson("metaHeaders", metaHeaders)
job.SetenvJson("authConfig", authConfig)
~~~
其中,创建的job名为pull,含义是下载Docker镜像,传入参数为image与tag,配置的环境变量有parallel、metaHeaders与authConfig。
### 5.3 触发执行job
Docker Server创建完Docker镜像下载job之后,需要触发执行该job,实现将控制权交于Docker Daemon。
Docker Server触发执行job的源码如下:
~~~
if err := job.Run(); err != nil {
if !job.Stdout.Used() {
return err
}
sf := utils.NewStreamFormatter(version.GreaterThan("1.0"))
w.Write(sf.FormatError(err))
}
~~~
由于Docker Daemon在启动时,已经配置了名为”pull”的job所对应的handler,实际为graph包中的CmdPull函数,故一旦该job被触发执行,控制权将直接交于Docker Daemon的CmdPull函数。Docker Daemon启动时Engine的handler注册,可以参见[《Docker源码分析》系列的第三篇——Docker Daemon启动篇](http://www.infoq.com/cn/articles/docker-source-code-analysis-part3)。
## 6.Docker Daemon
Docker Daemon是完成job执行的主要载体。Docker Server为镜像下载job准备好所有的参数配置之后,只等Docker Daemon来完成执行,并返回相应的信息,Docker Server再将响应信息返回至Docker Client。Docker Daemon对于镜像下载job的执行,涉及的内容较多:首先解析job参数,获取Docker镜像的repository、tag、Docker Registry信息等;随后与Docker Registry建立session;然后通过session下载Docker镜像;接着将Docker镜像下载至本地并存储于graph;最后在TagStore标记该镜像。
Docker Daemon对于镜像下载job的执行主要依靠CmdPull函数。这个CmdPull函数与Docker Client的CmdPull函数完全不同,前者是为了代替用户发送镜像下载的请求至Docker Daemon,而Docker Daemon的CmdPull函数则是实现代替用户真正完全镜像下载的任务。调用CmdPull函数的对象类型为TagStore,其源码实现位于[./docker/graph/pull.go](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go)。
### 6.1 解析job参数
正如Docker Client与Docker Server,Docker Daemon执行镜像下载job时的第一个步骤也是解析参数。解析工作一方面确保传入参数无误,另一方面按需为job提供参数依据。表6.1罗列Docker Daemon解析的job参数,如下:
表6.1 Docker Daemon解析job参数列表
| 参数名称 | 参数描述 |
|---|---|
| localName | 代表镜像的repository信息,有可能携带Docker Registry信息 |
| tag | 代表镜像的标签信息,默认为latest |
| authConfig | 代表用户在指定Docker Registry上的认证信息 |
| metaHeaders | 代表请求中的header信息 |
| hostname | 代表Docker Registry信息,从localName解析获得,默认为Docker Hub地址 |
| remoteName | 代表Docker镜像的repository名称信息,不携带Docker Registry信息 |
| endpoint | 代表Docker Registry完整的URL,从hostname扩展获得 |
参数解析过程中,Docker Daemon还添加了一些精妙的设计。如:在TagStore类型中设计了pullingPool对象,用于保存正在被下载的Docker镜像,下载完毕之前禁止其他Docker Client发起相同镜像的下载请求,下载完毕之后pullingPool中的该记录被清除。Docker Daemon一旦解析出localName与tag两个参数信息,则立即检测pullingPool,实现源码位于[./docker/graph/pull.go#L36-L46](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L36-L46),如下:
~~~
c, err := s.poolAdd("pull", localName+":"+tag)
if err != nil {
if c != nil {
// Another pull of the same repository is already taking place; just wait for it to finish
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)
~~~
### 6.2 创建session对象
下载Docker镜像,Docker Daemon与Docker Registry需要建立通信。为了保障两者通信的可靠性,Docker Daemon采用了session机制。Docker Daemon每收到一个Docker Client的镜像下载请求,都会创建一个与相应Docker Registry的session,之后所有的网络数据传输都在该session上完成。包registry定义了session,位于[./docker/registry/registry.go](https://github.com/docker/docker/blob/v1.2.0/registry/registry.go),如下:
~~~
type Session struct {
authConfig *AuthConfig
reqFactory *utils.HTTPRequestFactory
indexEndpoint string
jar *cookiejar.Jar
timeout TimeoutType
}
~~~
CmdPull函数中创建session的源码实现如下:
~~~
r, err := registry.NewSession(authConfig, registry.HTTPRequestFactory
(metaHeaders), endpoint, true)
~~~
创建的session对象为r,在下一阶段的镜像下载过程中,多数与镜像相关的数据传输均在r这个seesion的基础上完成。
### 6.3 执行镜像下载
Docker Daemon之前所有的操作,都属于配置阶段,从解析job参数,到建立session对象,而并未与Docker Registry建立实际的连接,并且也还未真正传输过有关Docker镜像的内容。
完成所有的配置之后,Docker Daemon进入Docker镜像下载环节,实现Docker镜像下载的源码位于[./docker/graph/pull.go#L69-L71](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L69-L71),如下:
~~~
if err = s.pullRepository(r, job.Stdout, localName, remoteName, tag, sf,
job.GetenvBool("parallel")); err != nil {
return job.Error(err)
}
~~~
以上代码中pullRepository函数包含了镜像下载整个流程的林林总总,该流程可以参见图6.1:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-12_56443b0c19349.jpg)
图6.1 pullRepository流程图
关于上图的各个环节,下表给出简要的功能介绍:
表6.2 pullRepository各环节功能介绍表
| 函数名称 | 功能介绍 |
|---|---|
| r.GetRepositoryData() | 获取指定repository中所有image的id信息 |
| r.GetRemoteTags() | 获取指定repository中所有的tag信息 |
| r.pullImage() | 从Docker Registry下载Docker镜像 |
| r.GetRemoteHistory() | 获取指定image所有祖先image id信息 |
| r.GetRemoteImageJSON() | 获取指定image的json信息 |
| r.GetRemoteImageLayer() | 获取指定image的layer信息 |
| s.graph.Register() | 将下载的镜像在TagStore的graph中注册 |
| s.Set() | 在TagStore中添加新下载的镜像信息 |
分析pullRepository的整个流程之前,很有必要了解下pullRepository函数调用者的类型TagStore。TagStore是Docker镜像方面涵盖内容最多的数据结构:一方面TagStore管理Docker的Graph,另一方面TagStore还管理Docker的repository记录。除此之外,TagStore还管理着上文提到的对象pullingPool以及pushingPool,保证Docker Daemon在同一时刻,只为一个Docker Client执行同一镜像的下载或上传。TagStore结构体的定义位于[./docker/graph/tags.go#L20-L29](https://github.com/docker/docker/blob/v1.2.0/graph/tags.go#L20-L29),如下:
~~~
type TagStore struct {
path string
graph *Graph
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{}
}
~~~
以下将重点分析pullRepository的整个流程。
#### 6.3.1 GetRepositoryData
使用Docker下载镜像时,用户往往指定的是Docker镜像的名称,如:请求docker pull ubuntu:14.04中镜像名称为ubuntu。GetRepositoryData的作用则是获取镜像名称所在repository中所有image的 id信息。
GetRepositoryData的源码实现位于[./docker/registry/session.go#L255-L324](https://github.com/docker/docker/blob/v1.2.0/registry/session.go#L255-L324)。获取repository中image的ID信息的目标URL地址如以下源码:
~~~
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", indexEp, remote)
~~~
因此,docker pull ubuntu:14.04请求被执行时,repository的目标URL地址为[https://index.docker.io/v1/repositories/ubuntu/images](https://index.docker.io/v1/repositories/ubuntu/images),访问该URL可以获得有关ubuntu这个repository中所有image的 id信息,部分image的id信息如下:
~~~
[{"checksum": "", "id": "
2427658c75a1e3d0af0e7272317a8abfaee4c15729b6840e3c2fca342fe47bf1"},
{"checksum": "", "id":
"81fbd8fa918a14f4ebad9728df6785c537218279081c7a120d72399d3a5c94a5"
}, {"checksum": "", "id":
"ec69e8fd6b0236b67227869b6d6d119f033221dd0f01e0f569518edabef3b72c"
}, {"checksum": "", "id":
"9e8dc15b6d327eaac00e37de743865f45bee3e0ae763791a34b61e206dd5222e"
}, {"checksum": "", "id":
"78949b1e1cfdcd5db413c300023b178fc4b59c0e417221c0eb2ffbbd1a4725cc"
},……]
~~~
获取以上信息之后,Docker Daemon通过RepositoryData和ImgData类型对象来存储ubuntu这个repository中所有image的信息,RepositoryData和ImgData的数据结构关系如图6.2:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-12_56443b0c32e68.jpg)
图6.2 RepositoryData和ImgData的数据结构关系图
GetRepositoryData执行过程中,会为指定repository中的每一个image创建一个ImgData对象,并最终将所有ImgData存放在RepositoryData的ImgList属性中,ImgList的类型为map,key为image的ID,value指向ImgData对象。此时ImgData对象中只有属性ID与Checksum有内容。
#### 6.3.2 GetRemoteTags
使用Docker下载镜像时,用户除了指定Docker镜像的名称之外,一般还需要指定Docker镜像的tag,如:请求docker pull ubuntu:14.04中镜像名称为ubuntu,镜像tag为14.04,假设用户不显性指定tag,则默认tag为latest。GetRemoteTags的作用则是获取镜像名称所在repository中所有tag的信息。
GetRemoteTags的源码实现位于[./docker/registry/session.go#L195-234](https://github.com/docker/docker/blob/v1.2.0/registry/session.go#L255-L324)。获取repository中所有tag信息的目标URL地址如以下源码:
~~~
endpoint := fmt.Sprintf("%srepositories/%s/tags", host, repository)
~~~
获取指定repository中所有tag信息之后,Docker Daemon根据tag对应layer的ID,找到ImgData,并对填充ImgData中的Tag属性。此时,RepositoryData的ImgList属性中,有的ImgData对象有Tag内容,有的ImgData对象中没有Tag内容。这也和实际情况相符,如下载一个ubuntu:14.04镜像,该镜像的rootfs中只有最上层的layer才有tag信息,这一层layer的parent Image并不一定存在tag信息。
#### 6.3.3 pullImage
Docker Daemon下载Docker镜像时是通过image id来完成。GetRepositoryData和GetRemoteTags则成功完成了用户传入的repository和tag信息与image id的转换。如请求docker pull ubuntu:14.04中,repository为ubuntu,tag为14.04,则对应的image id为2d24f826。
Docker Daemon获得下载镜像的image id之后,首先查验pullingPool,判断是否有其他Docker Client同样发起了该镜像的下载请求,如果没有的话Docker Daemon才继续下载任务。
执行pullImage函数的源码实现位于[./docker/graph/pull.go#L159](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L159),如下:
~~~
s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf)
~~~
而pullImage函数的定义位于[./docker/graph/pull.go#L214-L301](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L214-L301)。图6.1中,可以看到pullImage函数的执行可以分为4个步骤:GetRemoteHistory、GetRemoteImageJson、GetRemoteImageLayer与s.graph.Register()。
GetRemoteHistory的作用很好理解,既然Docker Daemon已经通过GetRepositoryData和GetRemoteTags找出了指定tag的image id,那么Docker Daemon所需完成的工作为下载该image 及其所有的祖先image。GetRemoteHistory正是用于获取指定image及其所有祖先iamge的id。
GetRemoteHistory的源码实现位于[./docker/registry/session.go#L72-L101](https://github.com/docker/docker/blob/v1.2.0/registry/session.go#L72-L101)。
获取所有的image id之后,对于每一个image id,Docker Daemon都开始下载该image的全部内容。Docker Image的全部内容包括两个方面:image json信息以及image layer信息。Docker所有image的json信息都由函数GetRemoteImageJSON来完成。分析GetRemoteImageJSON之前,有必要阐述清楚什么是Docker Image的json信息。
Docker Image的json信息是一个非常重要的概念。这部分json唯一的标志了一个image,不仅标志了image的id,同时也标志了image所在layer对应的config配置信息。理解以上内容,可以举一个例子:docker build。命令docker build用以通过指定的Dockerfile来创建一个Docker镜像;对于Dockerfile中所有的命令,Docker Daemon都会为其创建一个新的image,如:RUN apt-get update, ENV path=/bin, WORKDIR /home等。对于命令RUN apt-get update,Docker Daemon需要执行apt-get update操作,对应的rootfs上必定会有内容更新,导致新建的image所代表的layer中有新添加的内容。而如ENV path=/bin, WORKDIR /home这样的命令,仅仅是配置了一些容器运行的参数,并没有镜像内容的更新,对于这种情况,Docker Daemon同样创建一层新的layer,并且这层新的layer中内容为空,而命令内容会在这层image的json信息中做更新。总结而言,可以认为Docker的image包含两部分内容:image的json信息、layer内容。当layer内容为空时,image的json信息被更新。
清楚了Docker image的json信息之后,理解GetRemoteImageJSON函数的作用就变得十分容易。GetRemoteImageJSON的执行代码位于[./docker/graph/pull.go#L243](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L243),如下:
~~~
imgJSON, imgSize, err = r.GetRemoteImageJSON(id, endpoint, token)
~~~
GetRemoteImageJSON返回的两个对象imgJSON代表image的json信息,imgSize代表镜像的大小。通过imgJSON对象,Docker Daemon立即创建一个image对象,创建image对象的源码实现位于[./docker/graph/pull.go#L251](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L251),如下:
~~~
img, err = image.NewImgJSON(imgJSON)
~~~
而NewImgJSON函数位于包image中,函数返回类型为一个Image对象,而Image类型的定义而下:
~~~
type Image struct {
ID string `json:"id"`
Parent string `json:"parent,omitempty"`
Comment string `json:"comment,omitempty"`
Created time.Time `json:"created"`
Container string `json:"container,omitempty"`
ContainerConfig runconfig.Config `json:"container_config,omitempty"`
DockerVersion string `json:"docker_version,omitempty"`
Author string `json:"author,omitempty"`
Config *runconfig.Config `json:"config,omitempty"`
Architecture string `json:"architecture,omitempty"`
OS string `json:"os,omitempty"`
Size int64
graph Graph
}
~~~
返回img对象,则说明关于该image的所有元数据已经保存完毕,由于还缺少image的layer中包含的内容,因此下一个步骤即为下载镜像layer的内容,调用函数为GetRemoteImageLayer,函数执行位于[./docker/graph/pull.go#L270](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L270),如下:
~~~
layer, err := r.GetRemoteImageLayer(img.ID, endpoint, token, int64(imgSize))
~~~
GetRemoteImageLayer函数返回当前image的layer内容。Image的layer内容指的是:该image在parent image之上做的文件系统内容更新,包括文件的增添、删除、修改等。至此,image的json信息以及layer内容均被Docker Daemon获取,意味着一个完整的image已经下载完毕。下载image完毕之后,并不意味着Docker Daemon关于Docker镜像下载的job就此结束,Docker Daemon仍然需要对下载的image进行存储管理,以便Docker Daemon在执行其他如创建容器等job时,能够方便使用这些image。
Docker Daemon在graph中注册image的源码实现位于[./docker/graph/pull.go#L283-L285](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L283-L285),如下:
~~~
err = s.graph.Register(imgJSON,utils.ProgressReader(layer, imgSize,
out, sf, false, utils.TruncateID(id), "Downloading"),img)
~~~
Docker Daemon通过graph存储image是一个很重要的环节。Docker在1.2.0版本中可以通过AUFS、DevMapper以及BTRFS来进行image的存储。在Linux 3.18-rc2版本中,OverlayFS已经被内核合并,故从1.4.0版本开始,Docker 的image支持OverlayFS的存储方式。
Docker镜像的存储在Docker中是较为独立且重要的内容,故将在《Docker源码分析》系列的第十一篇专文分析。
#### 6.3.4 配置TagStore
Docker镜像下载完毕之后,Docker Daemon需要在TagStore中指定的repository中添加相应的tag。每当用户查看本地镜像时,都可以从TagStore的repository中查看所有含有tag信息的image。
Docker Daemon配置TagStore的源码实现位于[./docker/graph/pull.go#L206](https://github.com/docker/docker/blob/v1.2.0/graph/pull.go#L206),如下:
~~~
if err := s.Set(localName, tag, id, true); err != nil {
return err
}
~~~
TagStore类型的Set函数定义位于[./docker/graph/tags.go#L174-L205](https://github.com/docker/docker/blob/v1.2.0/graph/tags.go#L174-L205)。Set函数的指定流程与简要介绍如图6.3:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-11-12_56443b0c4a715.jpg)
图6.3 TagStore中Set函数执行流程图
当Docker Daemon将已下载的Docker镜像信息同步到repository之后,Docker下载镜像的job就全部完成,Docker Daemon返回响应至Docker Server,Docker Server返回相应至Docker Client。注:本地的repository文件位于Docker的根目录,根目录一般为/var/lib/docker,如果使用aufs的graphdriver,则repository文件名为repositories-aufs。
## 7.总结
Docker镜像给Docker容器的运行带来了无限的可能性,诸如Docker Hub之类的Docker Registry又使得Docker镜像在全球的开发者之间共享。Docker镜像的下载,作为使用Docker的第一个步骤,Docker爱好者若能熟练掌握其中的原理,必定能对Docker的很多概念有更为清晰的认识,对Docker容器的运行、管理等均是有百利而无一害。
Docker镜像的下载需要Docker Client、Docker Server、Docker Daemon以及Docker Registry四者协同合作完成。本文从源码的角度分析了四者各自的扮演的角色,分析过程中还涉及多种Docker概念,如repository、tag、TagStore、session、image、layer、image json、graph等。
## 8.作者介绍
**孙宏亮**,[DaoCloud](http://www.daocloud.io/)初创团队成员,软件工程师,浙江大学VLIS实验室应届研究生。读研期间活跃在PaaS和Docker开源社区,对Cloud Foundry有深入研究和丰富实践,擅长底层平台代码分析,对分布式平台的架构有一定经验,撰写了大量有深度的技术博客。2014年末以合伙人身份加入DaoCloud团队,致力于传播以Docker为主的容器的技术,推动互联网应用的容器化步伐。邮箱:[allen.sun@daocloud.io](mailto:allen.sun@daocloud.io)
## 9.参考文献
[https://docs.docker.com/terms/image/](https://docs.docker.com/terms/image/)
[https://docs.docker.com/terms/layer](https://docs.docker.com/terms/layer)
[http://docs.studygolang.com/pkg/](http://docs.studygolang.com/pkg/)