您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  要资料 文章 文库 Lib 视频 Code iProcess 课程 认证 咨询 工具 火云堂 讲座吧   成长之路  
会员   
 
   
 
  
每天15篇文章
不仅获得谋生技能
更可以追随信仰
 
 
     
   
 订阅
  捐助
Docker源码分析(十):Docker镜像下载
 
作者 孙宏亮  来源:infoq 火龙果软件  发布于 2015-4-15
2364 次浏览     评价:      
 

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。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:


图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篇。

Docker Client执行pull请求相应的处理函数,源码位于./docker/api/client/command.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,如下:

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,源码如下:

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,如下:

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,如下:

hostname, _, err := registry.ResolveRepositoryName(remote)

Docker Client通过包registry中的函数ResolveRepositoryName来解析hostname参数,传入的实参为remote,即去tag化的remote参数。ResolveRepositoryName函数的实现位于./docker/registry/registry.go#L237-L259,如下:

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:


图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,如下:

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篇。

Docker Server接收到镜像下载请求之后,通过路由分发最终由具体的handler——postImagesCreate来处理。postImagesCreate的实现位于./docker/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启动篇。

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。

6.1 解析job参数

正如Docker Client与Docker Server,Docker Daemon执行镜像下载job时的第一个步骤也是解析参数。解析工作一方面确保传入参数无误,另一方面按需为job提供参数依据。表6.1罗列Docker Daemon解析的job参数,如下:

表6.1 Docker Daemon解析job参数列表

参数解析过程中,Docker Daemon还添加了一些精妙的设计。如:在TagStore类型中设计了pullingPool对象,用于保存正在被下载的Docker镜像,下载完毕之前禁止其他Docker Client发起相同镜像的下载请求,下载完毕之后pullingPool中的该记录被清除。Docker Daemon一旦解析出localName与tag两个参数信息,则立即检测pullingPool,实现源码位于./docker/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,如下:

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,如下:

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

以上代码中pullRepository函数包含了镜像下载整个流程的林林总总,该流程可以参见图6.1:


图6.1 pullRepository流程图

关于上图的各个环节,下表给出简要的功能介绍:

表6.2 pullRepository各环节功能介绍表

分析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,如下:

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。获取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,访问该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:


图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。获取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,如下:

s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf)

而pullImage函数的定义位于./docker/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。

获取所有的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,如下:

imgJSON, imgSize, err = r.GetRemoteImageJSON(id, endpoint, token)

GetRemoteImageJSON返回的两个对象imgJSON代表image的json信息,imgSize代表镜像的大小。通过imgJSON对象,Docker Daemon立即创建一个image对象,创建image对象的源码实现位于./docker/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,如下:

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,如下:

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,如下:

if err := s.Set(localName, tag, id, true); err != nil {
return err
}

TagStore类型的Set函数定义位于./docker/graph/tags.go#L174-L205。Set函数的指定流程与简要介绍如图6.3:


图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初创团队成员,软件工程师,浙江大学VLIS实验室应届研究生。读研期间活跃在PaaS和Docker开源社区,对Cloud Foundry有深入研究和丰富实践,擅长底层平台代码分析,对分布式平台的架构有一定经验,撰写了大量有深度的技术博客。2014年末以合伙人身份加入DaoCloud团队,致力于传播以Docker为主的容器的技术,推动互联网应用的容器化步伐。

   
 订阅
  捐助
相关文章

每日构建解决方案
如何制定有效的配置管理流程
配置管理主要活动及实现方法
构建管理入门
相关文档

配置管理流程
配置管理白皮书
CM09_C配置管理标准
使用SVN进行版本控制
相关课程

配置管理实践
配置管理方法、工具与应用
多层次集成配置管理
产品发布管理
 

软件配置管理的问题、目的
软件配置管理规范
CQWeb 7.1性能测试与调优指南
为什么需要使用ClearCase
ClearCase与RTC的集成
利用ClearQuest 进行测试管理
更多...   


产品发布管理
配置管理方法、实践、工具
多层次集成配置管理
使用CC与CQ进行项目实践
CVS与配置管理
Subversion管理员

相关咨询服务
SCM启动咨询
SCM流程规范咨询
SCM评估性咨询


配置管理实践(从组织级到项目级)
通号院 配置管理规范与应用
配置管理日构建及持续集成
丹佛斯 ClearCase与配置管理
中国移动 软件配置管理
中国银行 软件配置管理
天津华翼蓝天科技 配置管理与Pvcs
 
 
 
 
 
每天2个文档/视频
扫描微信二维码订阅
订阅技术月刊
获得每月300个技术资源
 
 

关于我们 | 联系我们 | 京ICP备10020922号 京公海网安备110108001071号