Docker 源码分析(三):Docker Daemon 启动

阅读数:15927 2014 年 11 月 7 日

话题:语言 & 开发架构

【编者按】在《深入浅出 Docker》系列文章的基础上,InfoQ 推出了《Docker 源码分析》系列文章。《深入浅出 Docker》系列文章更多的是从使用角度出发,帮助读者了解 Docker 的来龙去脉,而《Docker 源码分析》系列文章通过分析解读 Docker 源码,来让读者了解 Docker 的内部实现,以更好的使用 Docker。总之,我们的目标是促进 Docker 在国内的发展以及传播。另外,欢迎加入 InfoQ Docker 技术交流群,QQ 群号:272489193。

1 前言

Docker 诞生以来,便引领了轻量级虚拟化容器领域的技术热潮。在这一潮流下,Google、IBM、Redhat 等业界翘楚纷纷加入 Docker 阵营。虽然目前 Docker 仍然主要基于 Linux 平台,但是 Microsoft 却多次宣布对 Docker 的支持,从先前宣布的 Azure 支持 Docker 与 Kubernetes,到如今宣布的下一代 Windows Server 原生态支持 Docker。Microsoft 的这一系列举措多少喻示着向 Linux 世界的妥协,当然这也不得不让世人对 Docker 的巨大影响力有重新的认识。

Docker 的影响力不言而喻,但如果需要深入学习 Docker 的内部实现,笔者认为最重要的是理解 Docker Daemon。在 Docker 架构中,Docker Client 通过特定的协议与 Docker Daemon 进行通信,而 Docker Daemon 主要承载了 Docker 运行过程中的大部分工作。本文即为《Docker 源码分析》系列的第三篇­——Docker Daemon 篇。

2 Docker Daemon 简介

Docker Daemon 是 Docker 架构中运行在后台的守护进程,大致可以分为 Docker Server、Engine 和 Job 三部分。Docker Daemon 可以认为是通过 Docker Server 模块接受 Docker Client 的请求,并在 Engine 中处理请求,然后根据请求类型,创建出指定的 Job 并运行,运行过程的作用有以下几种可能:向 Docker Registry 获取镜像,通过 graphdriver 执行容器镜像的本地化操作,通过 networkdriver 执行容器网络环境的配置,通过 execdriver 执行容器内部运行的执行工作等。

以下为 Docker Daemon 的架构示意图:

3 Docker Daemon 源码分析内容安排

本文从源码的角度,主要分析 Docker Daemon 的启动流程。由于 Docker Daemon 和 Docker Client 的启动流程有很大的相似之处,故在介绍启动流程之后,本文着重分析启动流程中最为重要的环节:创建 daemon 过程中 mainDaemon() 的实现。

4 Docker Daemon 的启动流程

由于 Docker Daemon 和 Docker Client 的启动都是通过可执行文件 docker 来完成的,因此两者的启动流程非常相似。Docker 可执行文件运行时,运行代码通过不同的命令行 flag 参数,区分两者,并最终运行两者各自相应的部分。

启动 Docker Daemon 时,一般可以使用以下命令:docker --daemon=true; docker –d; docker –d=true 等。接着由 docker 的 main() 函数来解析以上命令的相应 flag 参数,并最终完成 Docker Daemon 的启动。

首先,附上 Docker Daemon 的启动流程图:

由于《Docker 源码分析》系列之Docker Client 篇中,已经涉及了关于 Docker 中 main() 函数运行的很多前续工作(可参见Docker Client 篇),并且 Docker Daemon 的启动也会涉及这些工作,故本文略去相同部分,而主要针对后续仅和 Docker Daemon 相关的内容进行深入分析,即 mainDaemon() 的具体源码实现。

5 mainDaemon( ) 的具体实现

通过 Docker Daemon 的流程图,可以得出一个这样的结论:有关 Docker Daemon 的所有的工作,都被包含在 mainDaemon() 方法的实现中。

宏观来讲,mainDaemon() 完成创建一个 daemon 进程,并使其正常运行。

从功能的角度来说,mainDaemon() 实现了两部分内容:第一,创建 Docker 运行环境;第二,服务于 Docker Client,接收并处理相应请求。

从实现细节来讲,mainDaemon() 的实现过程主要包含以下步骤:

  • daemon 的配置初始化(这部分在 init() 函数中实现,即在 mainDaemon() 运行前就执行,但由于这部分内容和 mainDaemon() 的运行息息相关,故可认为是 mainDaemon() 运行的先决条件);
  • 命令行 flag 参数检查;
  • 创建 engine 对象;
  • 设置 engine 的信号捕获及处理方法;
  • 加载 builtins;
  • 使用 goroutine 加载 daemon 对象并运行;
  • 打印 Docker 版本及驱动信息;
  • Job 之”serveapi”的创建与运行。

下文将一一深入分析以上步骤。

5.0 配置初始化

在 mainDaemon() 运行之前,关于 Docker Daemon 所需要的 config 配置信息均已经初始化完毕。具体实现如下,位于./docker/docker/daemon.go

var (
	daemonCfg = &daemon.Config{}
)
func init() {
	daemonCfg.InstallFlags()
}

首先,声明一个为 daemon 包中 Config 类型的变量,名为 daemonCfg。而 Config 对象,定义了 Docker Daemon 所需的配置信息。在 Docker Daemon 在启动时,daemonCfg 变量被传递至 Docker Daemon 并被使用。

Config 对象的定义如下(含部分属性的解释),位于./docker/daemon/config.go

type Config struct {
	Pidfile                  string   //Docker Daemon 所属进程的 PID 文件 
	Root                   string   //Docker 运行时所使用的 root 路径 
	AutoRestart             bool    // 已被启用,转而支持 docker run 时的重启 
	Dns                   []string  //Docker 使用的 DNS Server 地址 
	DnsSearch              []string  //Docker 使用的指定的 DNS 查找域名 
	Mirrors                 []string  // 指定的优先 Docker Registry 镜像 
	EnableIptables           bool    // 启用 Docker 的 iptables 功能 
	EnableIpForward         bool    // 启用 net.ipv4.ip_forward 功能 
	EnableIpMasq            bool      // 启用 IP 伪装技术 
	DefaultIp                net.IP     // 绑定容器端口时使用的默认 IP
	BridgeIface              string      // 添加容器网络至已有的网桥 
	BridgeIP                 string     // 创建网桥的 IP 地址 
	FixedCIDR               string     // 指定 IP 的 IPv4 子网,必须被网桥子网包含 
	InterContainerCommunication   bool  // 是否允许相同 host 上容器间的通信 
	GraphDriver             string      //Docker 运行时使用的特定存储驱动 
	GraphOptions            []string   // 可设置的存储驱动选项 
	ExecDriver               string    // Docker 运行时使用的特定 exec 驱动 
	Mtu                    int      // 设置容器网络的 MTU
	DisableNetwork          bool     // 有定义,之后未初始化 
	EnableSelinuxSupport      bool     // 启用 SELinux 功能的支持 
	Context                 map[string][]string   // 有定义,之后未初始化 
}

已经有声明的 daemonCfg 之后,init() 函数实现了 daemonCfg 变量中各属性的赋值,具体的实现为:daemonCfg.InstallFlags(),位于./docker/daemon/config.go,代码如下:

func (config *Config) InstallFlags() {
	flag.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, "/var/run/docker.pid",
 "Path to use for daemon PID file")
	flag.StringVar(&config.Root, []string{"g", "-graph"}, "/var/lib/docker",
"Path to use as the root of the Docker runtime")
	……
	opts.IPVar(&config.DefaultIp, []string{"#ip", "-ip"}, "0.0.0.0", "Default IP address to 
use when binding container ports")
	opts.ListVar(&config.GraphOptions, []string{"-storage-opt"}, "Set storage driver options")
	……
}

在 InstallFlags() 函数的实现过程中,主要是定义某种类型的 flag 参数,并将该参数的值绑定在 config 变量的指定属性上,如:

flag.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, " /var/run/docker.pid", "Path to use for daemon PID file")

以上语句的含义为:

  • 定义一个为 String 类型的 flag 参数;
  • 该 flag 的名称为”p”或者”-pidfile”;
  • 该 flag 的值为” /var/run/docker.pid”, 并将该值绑定在变量 config.Pidfile 上;
  • 该 flag 的描述信息为"Path to use for daemon PID file"。

至此,关于 Docker Daemon 所需要的配置信息均声明并初始化完毕。

5.1 flag 参数检查

从这一节开始,真正进入 Docker Daemon 的 mainDaemon() 运行分析。

第一个步骤即 flag 参数的检查。具体而言,即当 docker 命令经过 flag 参数解析之后,判断剩余的参数是否为 0。若为 0,则说明 Docker Daemon 的启动命令无误,正常运行;若不为 0,则说明在启动 Docker Daemon 的时候,传入了多余的参数,此时会输出错误提示,并退出运行程序。具体代码如下:

if flag.NArg() != 0 {
	flag.Usage()
	return
}

5.2 创建 engine 对象

在 mainDaemon() 运行过程中,flag 参数检查完毕之后,随即创建 engine 对象,代码如下:

eng := engine.New()

Engine 是 Docker 架构中的运行引擎,同时也是 Docker 运行的核心模块。Engine 扮演着 Docker container 存储仓库的角色,并且通过 job 的形式来管理这些容器。

./docker/engine/engine.go中,Engine 结构体的定义如下:

type Engine struct {
	handlers   map[string]Handler
	catchall   Handler
	hack       Hack // data for temporary hackery (see hack.go)
	id         string
	Stdout     io.Writer
	Stderr     io.Writer
	Stdin      io.Reader
	Logging    bool
	tasks      sync.WaitGroup
	l          sync.RWMutex // lock for shutdown
	shutdown   bool
	onShutdown []func() // shutdown handlers
}

其中,Engine 结构体中最为重要的即为 handlers 属性。该 handlers 属性为 map 类型,key 为 string 类型,value 为 Handler 类型。其中 Handler 类型的定义如下:

type Handler func(*Job) Status

可见,Handler 为一个定义的函数。该函数传入的参数为 Job 指针,返回为 Status 状态。

介绍完 Engine 以及 Handler,现在真正进入 New() 函数的实现中:

func New() *Engine {
	eng := &Engine{
		handlers: make(map[string]Handler),
		id:       utils.RandomString(),
		Stdout:   os.Stdout,
		Stderr:   os.Stderr,
		Stdin:    os.Stdin,
		Logging:  true,
	}
	eng.Register("commands", func(job *Job) Status {
		for _, name := range eng.commands() {
			job.Printf("%s\n", name)
		}
		return StatusOK
	})
	// Copy existing global handlers
	for k, v := range globalHandlers {
		eng.handlers[k] = v
	}
	return eng
}

分析以上代码,可以知道 New() 函数最终返回一个 Engine 对象。而在代码实现部分,第一个工作即为创建一个 Engine 结构体实例 eng;第二个工作是向 eng 对象注册名为 commands 的 Handler,其中 Handler 为临时定义的函数 func(job *Job) Status{ } , 该函数的作用是通过 job 来打印所有已经注册完毕的 command 名称,最终返回状态 StatusOK;第三个工作是:将已定义的变量 globalHandlers 中的所有的 Handler,都复制到 eng 对象的 handlers 属性中。最后成功返回 eng 对象。

5.3 设置 engine 的信号捕获

回到 mainDaemon() 函数的运行中,执行后续代码:

signal.Trap(eng.Shutdown)

该部分代码的作用是:在 Docker Daemon 的运行中,设置 Trap 特定信号的处理方法,特定信号有 SIGINT,SIGTERM 以及 SIGQUIT;当程序捕获到 SIGINT 或者 SIGTERM 信号时,执行相应的善后操作,最后保证 Docker Daemon 程序退出。

该部分的代码的实现位于./docker/pkg/signal/trap.go。实现的流程分为以下 4 个步骤:

  • 创建并设置一个 channel,用于发送信号通知;
  • 定义 signals 数组变量,初始值为 os.SIGINT, os.SIGTERM; 若环境变量 DEBUG 为空的话,则添加 os.SIGQUIT 至 signals 数组;
  • 通过 gosignal.Notify(c, signals...) 中 Notify 函数来实现将接收到的 signal 信号传递给 c。需要注意的是只有 signals 中被罗列出的信号才会被传递给 c,其余信号会被直接忽略;
  • 创建一个 goroutine 来处理具体的 signal 信号,当信号类型为 os.Interrupt 或者 syscall.SIGTERM 时,执行传入 Trap 函数的具体执行方法,形参为 cleanup(), 实参为 eng.Shutdown。

Shutdown() 函数的定义位于./docker/engine/engine.go,主要做的工作是为 Docker Daemon 的关闭做一些善后工作。

善后工作如下:

  • Docker Daemon 不再接收任何新的 Job;
  • Docker Daemon 等待所有存活的 Job 执行完毕;
  • Docker Daemon 调用所有 shutdown 的处理方法;
  • 当所有的 handler 执行完毕,或者 15 秒之后,Shutdown() 函数返回。

由于在 signal.Trap( eng.Shutdown ) 函数的具体实现中执行 eng.Shutdown,在执行完 eng.Shutdown 之后,随即执行os.Exit(0),完成当前程序的立即退出。

5.4 加载 builtins

为 eng 设置完 Trap 特定信号的处理方法之后,Docker Daemon 实现了 builtins 的加载。代码实现如下:

if err := builtins.Register(eng); err != nil {
	log.Fatal(err)
}

加载 builtins 的主要工作是为:为 engine 注册多个 Handler,以便后续在执行相应任务时,运行指定的 Handler。这些 Handler 包括:网络初始化、web API 服务、事件查询、版本查看、Docker Registry 验证与搜索。代码实现位于./docker/builtins/builtins.go, 如下:

func Register(eng *engine.Engine) error {
	if err := daemon(eng); err != nil {
		return err
	}
	if err := remote(eng); err != nil {
		return err
	}
	if err := events.New().Install(eng); err != nil {
		return err
	}
	if err := eng.Register("version", dockerVersion); err != nil {
		return err
	}
	return registry.NewService().Install(eng)
}

以下分析实现过程中最为主要的 5 个部分:daemon(eng)、remote(eng)、events.New().Install(eng)、eng.Register(“version”,dockerVersion) 以及 registry.NewService().Install(eng)。

5.4.1 注册初始化网络驱动的 Handler

daemon(eng) 的实现过程,主要为 eng 对象注册了一个 key 为”init_networkdriver”的 Handler,该 Handler 的值为 bridge.InitDriver 函数,代码如下:

func daemon(eng *engine.Engine) error {
	return eng.Register("init_networkdriver", bridge.InitDriver)
}

需要注意的是,向 eng 对象注册 Handler,并不代表 Handler 的值函数会被直接运行,如 bridge.InitDriver,并不会直接运行,而是将 bridge.InitDriver 的函数入口,写入 eng 的 handlers 属性中。

Bridge.InitDriver 的具体实现位于./docker/daemon/networkdriver/bridge/driver.go ,主要作用为:

  • 获取为 Docker 服务的网络设备的地址;
  • 创建指定 IP 地址的网桥;
  • 配置网络 iptables 规则;
  • 另外还为 eng 对象注册了多个 Handler, 如 ”allocate_interface”, ”release_interface”, ”allocate_port”,”link”。

5.4.2 注册 API 服务的 Handler

remote(eng) 的实现过程,主要为 eng 对象注册了两个 Handler,分别为”serveapi”与”acceptconnections”。代码实现如下:

func remote(eng *engine.Engine) error {
	if err := eng.Register("serveapi", apiserver.ServeApi); err != nil {
		return err
	}
	return eng.Register("acceptconnections", apiserver.AcceptConnections)
}

注册的两个 Handler 名称分别为”serveapi”与”acceptconnections”, 相应的执行方法分别为 apiserver.ServeApi 与 apiserver.AcceptConnections,具体实现位于./docker/api/server/server.go。其中,ServeApi 执行时,通过循环多种协议,创建出 goroutine 来配置指定的 http.Server,最终为不同的协议请求服务;而 AcceptConnections 的实现主要是为了通知 init 守护进程,Docker Daemon 已经启动完毕,可以让 Docker Daemon 进程接受请求。

5.4.3 注册 events 事件的 Handler

events.New().Install(eng) 的实现过程,为 Docker 注册了多个 event 事件,功能是给 Docker 用户提供 API,使得用户可以通过这些 API 查看 Docker 内部的 events 信息,log 信息以及 subscribers_count 信息。具体的代码位于./docker/events/events.go,如下:

func (e *Events) Install(eng *engine.Engine) error {
	jobs := map[string]engine.Handler{
		"events":            e.Get,
		"log":               e.Log,
		"subscribers_count": e.SubscribersCount,
	}
	for name, job := range jobs {
		if err := eng.Register(name, job); err != nil {
			return err
		}
	}
	return nil
}

5.4.4 注册版本的 Handler

eng.Register(“version”,dockerVersion) 的实现过程,向 eng 对象注册 key 为”version”,value 为”dockerVersion”执行方法的 Handler,dockerVersion 的执行过程中,会向名为 version 的 job 的标准输出中写入 Docker 的版本,Docker API 的版本,git 版本,Go 语言运行时版本以及操作系统等版本信息。dockerVersion 的具体实现如下:

func dockerVersion(job *engine.Job) engine.Status {
	v := &engine.Env{}
	v.SetJson("Version", dockerversion.VERSION)
	v.SetJson("ApiVersion", api.APIVERSION)
	v.Set("GitCommit", dockerversion.GITCOMMIT)
	v.Set("GoVersion", runtime.Version())
	v.Set("Os", runtime.GOOS)
	v.Set("Arch", runtime.GOARCH)
	if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
		v.Set("KernelVersion", kernelVersion.String())
	}
	if _, err := v.WriteTo(job.Stdout); err != nil {
		return job.Error(err)
	}
	return engine.StatusOK
}

5.4.5 注册 registry 的 Handler

registry.NewService().Install(eng) 的实现过程位于./docker/registry/service.go,在 eng 对象对外暴露的 API 信息中添加 docker registry 的信息。当 registry.NewService() 成功被 Install 安装完毕的话,则有两个调用能够被 eng 使用:”auth”,向公有 registry 进行认证;”search”,在公有 registry 上搜索指定的镜像。

Install 的具体实现如下:

func (s *Service) Install(eng *engine.Engine) error {
	eng.Register("auth", s.Auth)
	eng.Register("search", s.Search)
	return nil
}

至此,所有 builtins 的加载全部完成,实现了向 eng 对象注册特定的 Handler。

5.5 使用 goroutine 加载 daemon 对象并运行

执行完 builtins 的加载,回到 mainDaemon() 的执行,通过一个 goroutine 来加载 daemon 对象并开始运行。这一环节的执行,主要包含三个步骤:

  • 通过 init 函数中初始化的 daemonCfg 与 eng 对象来创建一个 daemon 对象 d;
  • 通过 daemon 对象的 Install 函数,向 eng 对象中注册众多的 Handler;
  • 在 Docker Daemon 启动完毕之后,运行名为”acceptconnections”的 job,主要工作为向 init 守护进程发送”READY=1”信号,以便开始正常接受请求。

代码实现如下:

go func() {
	d, err := daemon.MainDaemon(daemonCfg, eng)
	if err != nil {
		log.Fatal(err)
	}
	if err := d.Install(eng); err != nil {
		log.Fatal(err)
	}
	if err := eng.Job("acceptconnections").Run(); err != nil {
		log.Fatal(err)
	}
}()

以下分别分析三个步骤所做的工作。

5.5.1 创建 daemon 对象

daemon.MainDaemon(daemonCfg, eng) 是创建 daemon 对象 d 的核心部分。主要作用为初始化 Docker Daemon 的基本环境,如处理 config 参数,验证系统支持度,配置 Docker 工作目录,设置与加载多种 driver,创建 graph 环境等,验证 DNS 配置等。

由于 daemon.MainDaemon(daemonCfg, eng) 是加载 Docker Daemon 的核心部分,且篇幅过长,故安排《Docker 源码分析》系列的第四篇专文分析这部分。

5.5.2 通过 daemon 对象为 engine 注册 Handler

当创建完 daemon 对象,goroutine 执行 d.Install(eng),具体实现位于./docker/daemon/daemon.go:

func (daemon *Daemon) Install(eng *engine.Engine) error {
	for name, method := range map[string]engine.Handler{
		"attach":            daemon.ContainerAttach,
		……
		"image_delete":      daemon.ImageDelete, 
	} {
		if err := eng.Register(name, method); err != nil {
			return err
		}
	}
	if err := daemon.Repositories().Install(eng); err != nil {
		return err
	}
	eng.Hack_SetGlobalVar("httpapi.daemon", daemon)
	return nil
}

以上代码的实现分为三部分:

  • 向 eng 对象中注册众多的 Handler 对象;
  • daemon.Repositories().Install(eng) 实现了向 eng 对象注册多个与 image 相关的 Handler,Install 的实现位于./docker/graph/service.go
  • eng.Hack_SetGlobalVar("httpapi.daemon", daemon) 实现向 eng 对象中 map 类型的 hack 对象中添加一条记录,key 为”httpapi.daemon”,value 为 daemon。

5.5.3 运行 acceptconnections 的 job

在 goroutine 内部最后运行名为”acceptconnections”的 job,主要作用是通知 init 守护进程,Docker Daemon 可以开始接受请求了。

这是源码分析系列中第一次涉及具体 Job 的运行,以下简单分析”acceptconnections”这个 job 的运行。

可以看到首先执行 eng.Job("acceptconnections"),返回一个 Job,随后再执行 eng.Job("acceptconnections").Run(),也就是该执行 Job 的 run 函数。

eng.Job(“acceptconnections”) 的实现位于./docker/engine/engine.go,如下:

func (eng *Engine) Job(name string, args ...string) *Job {
	job := &Job{
		Eng:    eng,
		Name:   name,
		Args:   args,
		Stdin:  NewInput(),
		Stdout: NewOutput(),
		Stderr: NewOutput(),
		env:    &Env{},
	}
	if eng.Logging {
		job.Stderr.Add(utils.NopWriteCloser(eng.Stderr))
	}
	if handler, exists := eng.handlers[name]; exists {
		job.handler = handler
	} else if eng.catchall != nil && name != "" {
		job.handler = eng.catchall
	}
	return job
}

由以上代码可知,首先创建一个类型为 Job 的 job 对象,该对象中 Eng 属性为函数的调用者 eng,Name 属性为”acceptconnections”,没有参数传入。另外在 eng 对象所有的 handlers 属性中寻找键为”acceptconnections”记录的值,由于在加载 builtins 操作中的 remote(eng) 中已经向 eng 注册过这样的一条记录,key 为”acceptconnections”,value 为 apiserver.AcceptConnections。因此 job 对象的 handler 为 apiserver.AcceptConnections。最后返回已经初始化完毕的对象 job。

创建完 job 对象之后,随即执行该 job 对象的 run() 函数。Run() 函数的实现位于./docker/engine/job.go,该函数执行指定的 job,并在 job 执行完成前一直阻塞。对于名为”acceptconnections”的 job 对象,运行代码为job.status = job.handler(job),由于 job.handler 值为 apiserver.AcceptConnections,故真正执行的是 job.status = apiserver.AcceptConnections(job)。

进入 AcceptConnections 的具体实现,位于./docker/api/server/server.go, 如下:

func AcceptConnections(job *engine.Job) engine.Status {
	// Tell the init daemon we are accepting requests
	go  systemd.SdNotify("READY=1")
	if activationLock != nil {
		close(activationLock)
	}
	return engine.StatusOK
}

重点为 go systemd.SdNotify("READY=1") 的实现,位于./docker/pkg/system/sd_notify.go,主要作用是通知 init 守护进程 Docker Daemon 的启动已经全部完成,潜在的功能是使得 Docker Daemon 开始接受 Docker Client 发送来的 API 请求。

至此,已经完成通过 goroutine 来加载 daemon 对象并运行。

5.6 打印 Docker 版本及驱动信息

回到 mainDaemon() 的运行流程中,在 goroutine 的执行之时,mainDaemon() 函数内部其它代码也会并发执行。

第一个执行的即为显示 docker 的版本信息,以及 ExecDriver 和 GraphDriver 这两个驱动的具体信息,代码如下:

log.Printf("docker daemon: %s %s; execdriver: %s; graphdriver: %s",
	dockerversion.VERSION,
	dockerversion.GITCOMMIT,
	daemonCfg.ExecDriver,
	daemonCfg.GraphDriver,
)

5.7 Job 之 serveapi 的创建与运行

打印部分 Docker 具体信息之后,Docker Daemon 立即创建并运行名为”serveapi”的 job,主要作用为让 Docker Daemon 提供 API 访问服务。实现代码位于./docker/docker/daemon.go#L66,如下:

job := eng.Job("serveapi", flHosts...)
job.SetenvBool("Logging", true)
job.SetenvBool("EnableCors", *flEnableCors)
job.Setenv("Version", dockerversion.VERSION)
job.Setenv("SocketGroup", *flSocketGroup)

job.SetenvBool("Tls", *flTls)
job.SetenvBool("TlsVerify", *flTlsVerify)
job.Setenv("TlsCa", *flCa)
job.Setenv("TlsCert", *flCert)
job.Setenv("TlsKey", *flKey)
job.SetenvBool("BufferRequests", true)
if err := job.Run(); err != nil {
	log.Fatal(err)
}

实现过程中,首先创建一个名为”serveapi”的 job,并将 flHosts 的值赋给 job.Args。flHost 的作用主要是为 Docker Daemon 提供使用的协议与监听的地址。随后,Docker Daemon 为该 job 设置了众多的环境变量,如安全传输层协议的环境变量等。最后通过 job.Run() 运行该 serveapi 的 job。

由于在 eng 中 key 为”serveapi”的 handler,value 为 apiserver.ServeApi,故该 job 运行时,执行 apiserver.ServeApi 函数,位于./docker/api/server/server.go。ServeApi 函数的作用主要是对于用户定义的所有支持协议,Docker Daemon 均创建一个 goroutine 来启动相应的 http.Server,分别为不同的协议服务。

由于创建并启动 http.Server 为 Docker 架构中有关 Docker Server 的重要内容,《Docker 源码分析》系列会在第五篇专文进行分析。

至此,可以认为 Docker Daemon 已经完成了 serveapi 这个 job 的初始化工作。一旦 acceptconnections 这个 job 运行完毕,则会通知 init 进程 Docker Daemon 启动完毕,可以开始提供 API 服务。

6 总结

本文从源码的角度分析了 Docker Daemon 的启动,着重分析了 mainDaemon() 的实现。

Docker Daemon 作为 Docker 架构中的主干部分,负责了 Docker 内部几乎所有操作的管理。学习 Docker Daemon 的具体实现,可以对 Docker 架构有一个较为全面的认识。总结而言,Docker 的运行,载体为 daemon,调度管理由 engine,任务执行靠 job。

7 作者简介

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

8 参考文献

  1. http://www.infoq.com/cn/news/2014/10/windows-server-docker
  2. http://www.freedesktop.org/software/systemd/man/sd_notify.html
  3. http://www.ibm.com/developerworks/cn/linux/1407_liuming_init3/index.html
  4. http://docs.studygolang.com/pkg/os/
  5. https://docs.docker.com/reference/commandline/cli/

感谢郭蕾对本文的策划和审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。