Docker 源码分析(四):Docker Daemon 之 NewDaemon 实现

阅读数:6646 2014 年 11 月 25 日

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

1. 前言

Docker 的生态系统日趋完善,开发者群体也在日趋庞大,这让业界对 Docker 持续抱有极其乐观的态度。如今,对于广大开发者而言,使用 Docker 这项技术已然不是门槛,享受 Docker 带来的技术福利也不再是困难。然而,如何探寻 Docker 适应的场景,如何发展 Docker 周边的技术,以及如何弥合 Docker 新技术与传统物理机或 VM 技术的鸿沟,已经占据 Docker 研究者们的思考与实践。

本文为《Docker 源码分析》第四篇——Docker Daemon 之 NewDaemon 实现,力求帮助广大 Docker 爱好者更多得理解 Docker 的核心——Docker Daemon 的实现。

2. NewDaemon 作用简介

在 Docker 架构中有很多重要的概念,如:graph,graphdriver,execdriver,networkdriver,volumes,Docker containers 等。Docker 在实现过程中,需要将以上实体进行统一化管理,而 Docker Daemon 中的 daemon 实例就是设计用来完成这一任务的实体。

从源码的角度,NewDaemon 函数的执行完成了 Docker Daemon 创建并加载 daemon 的任务,最终实现统一管理 Docker Daemon 的资源。

3. NewDaemon 源码分析内容安排

本文从源码角度,分析 Docker Daemon 加载过程中 NewDaemon 的实现,整个分析过程如下图:

图 3.1 Docker Daemon 中 NewDaemon 执行流程图

由上图可见,Docker Daemon 中 NewDaemon 的执行流程主要包含 12 个独立的步骤:处理配置信息、检测系统支持及用户权限、配置工作路径、加载并配置 graphdriver、创建 Docker Daemon 网络环境、创建并初始化 graphdb、创建 execdriver、创建 daemon 实例、检测 DNS 配置、加载已有 container、设置 shutdown 处理方法、以及返回 daemon 实例。

下文会在 NewDaemon 的具体实现中,以 12 节分别分析以上内容。

4. NewDaemon 具体实现

在《Docker 源码分析》系列第三篇中,有一个重要的环节:使用 goroutine 加载 daemon 对象并运行。在加载并运行 daemon 对象时,所做的第一个工作即为:

d, err := daemon.NewDaemon(daemonCfg, eng)

该部分代码分析如下:

  • 函数名:NewDaemon;
  • 函数调用具体实现所处的包位置:./docker/daemon
  • 函数具体实现源文件:./docker/daemon/daemon.go
  • 函数传入实参:daemonCfg,定义了 Docker Daemon 运行过程中所需的众多配置信息;eng,在 mainDaemon 中创建的 Engine 对象实例;
  • 函数返回类型:d,具体的 Daemon 对象实例;err,错误状态。

进入./docker/daemon/daemon.go 中NewDaemon 的具体实现,代码如下:

func NewDaemon(config *Config, eng *engine.Engine) (*Daemon, error) {
	daemon, err := NewDaemonFromDirectory(config, eng)
	if err != nil {
		return nil, err
	}
	return daemon, nil
}

可见,在实现 NewDaemon 的过程中,通过 NewDaemonFromDirectory 函数来实现创建 Daemon 的运行环境。该函数的实现,传入参数以及返回类型与 NewDaemon 函数相同。下文将大篇幅分析 NewDaemonFromDirectory 的实现细节。

4.1. 应用配置信息

在 NewDaemonFromDirectory 的实现过程中,第一个工作是:如何应用传入的配置信息。这部分配置信息服务于 Docker Daemon 的运行,并在 Docker Daemon 启动初期就初始化完毕。配置信息的主要功能是:供用户自由配置 Docker 的可选功能,使得 Docker 的运行更贴近用户期待的运行场景。

配置信息的处理包含 4 部分:

  • 配置 Docker 容器的 MTU;
  • 检测网桥配置信息;
  • 查验容器通信配置;
  • 处理 PID 文件配置。

4.1.1. 配置 Docker 容器的 MTU

config 信息中的 Mtu 应用于容器网络的最大传输单元(MTU)特性。有关 MTU 的源码如下:

if config.Mtu == 0 {
config.Mtu = GetDefaultNetworkMtu()

可见,若 config 信息中 Mtu 的值为 0 的话,则通过 GetDefaultNetworkMtu 函数将 Mtu 设定为默认的值;否则,采用 config 中的 Mtu 值。由于在默认的配置文件./docker/daemon/config.go(下文简称为默认配置文件)中,初始化时 Mtu 属性值为 0,故执行 GetDefaultNetworkMtu。

GetDefaultNetworkMtu 函数的具体实现位于./docker/daemon/config.go:

func GetDefaultNetworkMtu() int {
	if iface, err := networkdriver.GetDefaultRouteIface(); err == nil {
		return iface.MTU
	}
	return defaultNetworkMtu
}

GetDefaultNetworkMtu 的实现中,通过 networkdriver 包的 GetDefaultRouteIface 方法获取具体的网络设备,若该网络设备存在,则返回该网络设备的 MTU 属性值;否则的话,返回默认的 MTU 值 defaultNetworkMtu,值为 1500。

4.1.2. 检测网桥配置信息

处理完 config 中的 Mtu 属性之后,马上检测 config 中 BridgeIface 和 BridgeIP 这两个信息。BridgeIface 和 BridgeIP 的作用是为创建网桥的任务”init_networkdriver”提供参数。代码如下:

if config.BridgeIface != "" && config.BridgeIP != "" {
	return nil, fmt.Errorf("You specified -b & --bip, mutually exclusive options. 
Please specify only one.")
}

以上代码的含义为:若 config 中 BridgeIface 和 BridgeIP 两个属性均不为空,则返回 nil 对象,并返回错误信息,错误信息内容为:用户同时指定了 BridgeIface 和 BridgeIP,这两个属性属于互斥类型,只能至多指定其中之一。而在默认配置文件中,BridgeIface 和 BridgeIP 均为空。

4.1.3. 查验容器通信配置

检测容器的通信配置,主要是针对 config 中的 EnableIptables 和 InterContainerCommunication 这两个属性。EnableIptables 属性的作用是启用 Docker 对 iptables 规则的添加功能;InterContainerCommunication 的作用是启用 Docker container 之间互相通信的功能。代码如下:

if !config.EnableIptables && !config.InterContainerCommunication {
	return nil, fmt.Errorf("You specified --iptables=false with --icc=
false. ICC uses iptables to function. Please set --icc or --iptables to true.")
}

代码含义为:若 EnableIptables 和 InterContainerCommunication 两个属性的值均为 false,则返回 nil 对象以及错误信息。其中错误信息为:用户将以上两属性均置为 false,container 间通信需要 iptables 的支持,需设置至少其中之一为 true。而在默认配置文件中,这两个属性的值均为 true。

4.1.4. 处理网络功能配置

接着,处理 config 中的 DisableNetwork 属性,以备后续在创建并执行创建 Docker Daemon 网络环境时使用,即在名为”init_networkdriver”的 job 创建并运行中体现。

config.DisableNetwork = config.BridgeIface == DisableNetworkBridge

由于 config 中的 BridgeIface 属性值为空,另外 DisableNetworkBridge 的值为字符串”none”,因此最终 config 中 DisableNetwork 的值为 false。后续名为”init_networkdriver”的 job 在执行过程中需要使用该属性。

4.1.5. 处理 PID 文件配置

处理 PID 文件配置,主要工作是:为 Docker Daemon 进程运行时的 PID 号创建一个 PID 文件,文件的路径即为 config 中的 Pidfile 属性。并且为 Docker Daemon 的 shutdown 操作添加一个删除该 Pidfile 的函数,以便在 Docker Daemon 退出的时候,可以在第一时间删除该 Pidfile。处理 PID 文件配置信息的代码实现如下:

if config.Pidfile != "" {
	if err := utils.CreatePidFile(config.Pidfile); err != nil {
		return nil, err
	}
	eng.OnShutdown(func() {
		utils.RemovePidFile(config.Pidfile)
	})
}

代码执行过程中,首先检测 config 中的 Pidfile 属性是否为空,若为空,则跳过代码块继续执行;若不为空,则首先在文件系统中创建具体的 Pidfile,然后向 eng 的 onShutdown 属性添加一个处理函数,函数具体完成的工作为 utils.RemovePidFile(config.Pidfile),即在 Docker Daemon 进行 shutdown 操作的时候,删除 Pidfile 文件。在默认配置文件中,Pidfile 文件的初始值为” /var/run/docker.pid”。

以上便是关于配置信息处理的分析。

4.2. 检测系统支持及用户权限

初步处理完 Docker 的配置信息之后,Docker 对自身运行的环境进行了一系列的检测,主要包括三个方面:

  • 操作系统类型对 Docker Daemon 的支持;
  • 用户权限的级别;
  • 内核版本与处理器的支持。

系统支持与用户权限检测的实现较为简单,实现代码如下:

if runtime.GOOS != "linux" {
	log.Fatalf("The Docker daemon is only supported on linux")
}
if os.Geteuid() != 0 {
	log.Fatalf("The Docker daemon needs to be run as root")
}
if err := checkKernelAndArch(); err != nil {
	log.Fatalf(err.Error())
}

首先,通过 runtime.GOOS,检测操作系统的类型。runtime.GOOS 返回运行程序所在操作系统的类型,可以是 Linux,Darwin,FreeBSD 等。结合具体代码,可以发现,若操作系统不为 Linux 的话,将报出 Fatal 错误日志,内容为“Docker Daemon 只能支持 Linux 操作系统”。

接着,通过 os.Geteuid(),检测程序用户是否拥有足够权限。os.Geteuid() 返回调用者所在组的 group id。结合具体代码,也就是说,若返回不为 0,则说明不是以 root 用户的身份运行,报出 Fatal 日志。

最后,通过 checkKernelAndArch(),检测内核的版本以及主机处理器类型。checkKernelAndArch() 的实现同样位于./docker/daemon/daemon.go。实现过程中,第一个工作是:检测程序运行所在的处理器架构是否为“amd64”,而目前 Docker 运行时只能支持 amd64 的处理器架构。第二个工作是:检测 Linux 内核版本是否满足要求,而目前 Docker Daemon 运行所需的内核版本若过低,则必须升级至 3.8.0。

4.3. 配置工作路径

配置 Docker Daemon 的工作路径,主要是创建 Docker Daemon 运行中所在的工作目录。实现过程中,通过 config 中的 Root 属性来完成。在默认配置文件中,Root 属性的值为”/var/lib/docker”。

配置工作路径的代码实现中,步骤如下:

(1) 使用规范路径创建一个 TempDir,路径名为 tmp;

(2) 通过 tmp,创建一个指向 tmp 的文件符号连接 realTmp;

(3) 使用 realTemp 的值,创建并赋值给环境变量 TMPDIR;

(4) 处理 config 的属性 EnableSelinuxSupport;

(5) 将 realRoot 重新赋值于 config.Root,并创建 Docker Daemon 的工作根目录。

4.4. 加载并配置 graphdriver

加载并配置存储驱动 graphdriver,目的在于:使得 Docker Daemon 创建 Docker 镜像管理所需的驱动环境。Graphdriver 用于完成 Docker 容器镜像的管理,包括存储与获取。

4.4.1. 创建 graphdriver

这部分内容的源码位于./docker/daemon/daemon.go#L743-L790,具体细节分析如下:

graphdriver.DefaultDriver = config.GraphDriver
driver, err := graphdriver.New(config.Root, config.GraphOptions)

首先,为 graphdriver 包中的 DefaultDriver 对象赋值,值为 config 中的 GraphDriver 属性,在默认配置文件中,GraphDriver 属性的值为空;同样的,属性 GraphOptions 也为空。然后通过 graphDriver 中的 new 函数实现加载 graph 的存储驱动。

创建具体的 graphdriver 是相当重要的一个环节,实现细节由 graphdriver 包中的 New 函数来完成。进入./docker/daemon/graphdriver/driver.go中,实现步骤如下:

第一,遍历数组选择 graphdriver,数组内容为 os.Getenv(“DOCKER_DRIVER”) 和 DefaultDriver。若不为空,则通过 GetDriver 函数直接返回相应的 Driver 对象实例,若均为空,则继续往下执行。这部分内容的作用是:让 graphdriver 的加载,首先满足用户的自定义选择,然后满足默认值。代码如下:

for _, name := range []string{os.Getenv("DOCKER_DRIVER"), DefaultDriver} {
	if name != "" {
		return GetDriver(name, root, options)
	}
}

第二,遍历优先级数组选择 graphdriver,优先级数组的内容为依次为”aufs”,”brtfs”,”devicemapper”和”vfs”。若依次验证时,GetDriver 成功,则直接返回相应的 Driver 对象实例,若均不成功,则继续往下执行。这部分内容的作用是:在没有指定以及默认的 Driver 时,从优先级数组中选择 Driver,目前优先级最高的为“aufs”。代码如下:

for _, name := range priority {
	driver, err = GetDriver(name, root, options)
	if err != nil {
		if err == ErrNotSupported || err == ErrPrerequisites || err == ErrIncompatibleFS {
			continue
		}
		return nil, err
	}
	return driver, nil
}

第三,从已经注册的 drivers 数组中选择 graphdriver。在”aufs”,”btrfs”,”devicemapper”和”vfs”四个不同类型 driver 的 init 函数中,它们均向 graphdriver 的 drivers 数组注册了相应的初始化方法。分别位于./docker/daemon/graphdriver/aufs/aufs.go,以及其他三类 driver 的相应位置。这部分内容的作用是:在没有优先级 drivers 数组的时候,同样可以通过注册的 driver 来选择具体的 graphdriver。

4.4.2. 验证 btrfs 与 SELinux 的兼容性

由于目前在 btrfs 文件系统上运行的 Docker 不兼容 SELinux,因此当 config 中配置信息需要启用 SELinux 的支持并且 driver 的类型为 btrfs 时,返回 nil 对象,并报出 Fatal 日志。代码实现如下:

// As Docker on btrfs and SELinux are incompatible at present, error on both being enabled
if config.EnableSelinuxSupport && driver.String() == "btrfs" {
return nil, fmt.Errorf("SELinux is not supported with the BTRFS graph driver!")
}

4.4.3. 创建容器仓库目录

Docker Daemon 在创建 Docker 容器之后,需要将容器放置于某个仓库目录下,统一管理。而这个目录即为 daemonRepo,值为:/var/lib/docker/containers,并通过 daemonRepo 创建相应的目录。代码实现如下:

daemonRepo := path.Join(config.Root, "containers")
if err := os.MkdirAll(daemonRepo, 0700); err != nil && !os.IsExist(err) {
	return nil, err
}

4.4.4. 迁移容器至 aufs 类型

当 graphdriver 的类型为 aufs 时,需要将现有 graph 的所有内容都迁移至 aufs 类型;若不为 aufs,则继续往下执行。实现代码如下:

if err = migrateIfAufs(driver, config.Root); err != nil {
return nil, err
}

这部分的迁移内容主要包括 Repositories,Images 以及 Containers,具体实现位于./docker/daemon/graphdriver/aufs/migrate.go

func (a *Driver) Migrate(pth string, setupInit func(p string) error) error {
	if pathExists(path.Join(pth, "graph")) {
		if err := a.migrateRepositories(pth); err != nil {
			return err
		}
		if err := a.migrateImages(path.Join(pth, "graph")); err != nil {
			return err
		}
		return a.migrateContainers(path.Join(pth, "containers"), setupInit)
	}
	return nil
}

migrate repositories 的功能是:在 Docker Daemon 的 root 工作目录下创建 repositories-aufs 的文件,存储所有与 images 相关的基本信息。

migrate images 的主要功能是:将原有的 image 镜像都迁移至 aufs driver 能识别并使用的类型,包括 aufs 所规定的 layers,diff 与 mnt 目录内容。

migrate container 的主要功能是:将 container 内部的环境使用 aufs driver 来进行配置,包括,创建 container 内部的初始层(init layer),以及创建原先 container 内部的其他 layers。

4.4.5. 创建镜像 graph

创建镜像 graph 的主要工作是:在文件系统中指定的 root 目录下,实例化一个全新的 graph 对象,作用为:存储所有标记的文件系统镜像,并记录镜像之间的关系。实现代码如下:

g, err := graph.NewGraph(path.Join(config.Root, "graph"), driver)

NewGraph 的具体实现位于./docker/graph/graph.go,实现过程中返回的对象为 Graph 类型,定义如下:

type Graph struct {
	Root    string
	idIndex *truncindex.TruncIndex
	driver  graphdriver.Driver
}

其中 Root 表示 graph 的工作根目录,一般为”/var/lib/docker/graph”;idIndex 使得检索字符串标识符时,允许使用任意一个该字符串唯一的前缀,在这里 idIndex 用于通过简短有效的字符串前缀检索镜像与容器的 ID;最后 driver 表示具体的 graphdriver 类型。

4.4.6. 创建 volumesdriver 以及 volumes graph

在 Docker 中 volume 的概念是:可以从 Docker 宿主机上挂载到 Docker 容器内部的特定目录。一个 volume 可以被多个 Docker 容器挂载,从而 Docker 容器可以实现互相共享数据等。在实现 volumes 时,Docker 需要使用 driver 来管理它,又由于 volumes 的管理不会像容器文件系统管理那么复杂,故 Docker 采用 vfs 驱动实现 volumes 的管理。代码实现如下:

volumesDriver, err := graphdriver.GetDriver("vfs", config.Root, config.GraphOptions)
volumes, err := graph.NewGraph(path.Join(config.Root, "volumes"), volumesDriver)

主要完成工作为:使用 vfs 创建 volumesDriver;创建相应的 volumes 目录,并返回 volumes graph 对象。

4.4.7. 创建 TagStore

TagStore 主要是用于存储镜像的仓库列表(repository list)。代码如下:

repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g)

NewTagStore 位于./docker/graph/tags.go,TagStore 的定义如下:

type TagStore struct {
	path         string
	graph        *Graph
	Repositories   map[string]Repository
	sync.Mutex
	pullingPool    map[string]chan struct{}
	pushingPool   map[string]chan struct{}
}

需要阐述的是 TagStore 类型中的多个属性的含义:

  • path:TagStore 中记录镜像仓库的文件所在路径;
  • graph:相应的 Graph 实例对象;
  • Repositories:记录具体的镜像仓库的 map 数据结构;
  • sync.Mutex:TagStore 的互斥锁
  • pullingPool :记录池,记录有哪些镜像正在被下载,若某一个镜像正在被下载,则驳回其他 Docker Client 发起下载该镜像的请求;
  • pushingPool:记录池,记录有哪些镜像正在被上传,若某一个镜像正在被上传,则驳回其他 Docker Client 发起上传该镜像的请求;

4.5. 创建 Docker Daemon 网络环境

创建 Docker Daemon 运行环境的时候,创建网络环境是极为重要的一个部分,这不仅关系着容器对外的通信,同样也关系着容器间的通信。

在创建网络时,Docker Daemon 是通过运行名为”init_networkdriver”的 job 来完成的。代码如下:

if !config.DisableNetwork {
	job := eng.Job("init_networkdriver")

	job.SetenvBool("EnableIptables", config.EnableIptables)
	job.SetenvBool("InterContainerCommunication", config.InterContainerCommunication)
	job.SetenvBool("EnableIpForward", config.EnableIpForward)
	job.Setenv("BridgeIface", config.BridgeIface)
	job.Setenv("BridgeIP", config.BridgeIP)
	job.Setenv("DefaultBindingIP", config.DefaultIp.String())

	if err := job.Run(); err != nil {
		return nil, err
	}
}

分析以上源码可知,通过 config 中的 DisableNetwork 属性来判断,在默认配置文件中,该属性有过定义,却没有初始值。但是在应用配置信息中处理网络功能配置的时候,将 DisableNetwork 属性赋值为 false,故判断语句结果为真,执行相应的代码块。

首先创建名为”init_networkdriver”的 job,随后为该 job 设置环境变量,环境变量的值如下:

  • 环境变量 EnableIptables,使用 config.EnableIptables 来赋值,为 true;
  • 环境变量 InterContainerCommunication,使用 config.InterContainerCommunication 来赋值,为 true;
  • 环境变量 EnableIpForward,使用 config.EnableIpForward 来赋值,值为 true;
  • 环境变量 BridgeIface,使用 config.BridgeIface 来赋值,为空字符串””;
  • 环境变量 BridgeIP,使用 config.BridgeIP 来赋值,为空字符串””;
  • 环境变量 DefaultBindingIP,使用 config.DefaultIp.String() 来赋值,为”0.0.0.0”。

设置完环境变量之后,随即运行该 job,由于在 eng 中 key 为”init_networkdriver”的 handler,value 为 bridge.InitDriver 函数,故执行 bridge.InitDriver 函数,具体的实现位于./docker/daemon/networkdriver/bridge/dirver.go,作用为:

  • 获取为 Docker 服务的网络设备的地址;
  • 创建指定 IP 地址的网桥;
  • 启用 Iptables 功能并配置;
  • 另外还为 eng 实例注册了 4 个 Handler, 如 ”allocate_interface”, ”release_interface”, ”allocate_port”,”link”。

4.5.1. 创建 Docker 网络设备

创建 Docker 网络设备,属于 Docker Daemon 创建网络环境的第一步,实际工作是创建名为“docker0”的网桥设备。

在 InitDriver 函数运行过程中,首先使用 job 的环境变量初始化内部变量;然后根据目前网络环境,判断是否创建 docker0 网桥,若 Docker 专属网桥已存在,则继续往下执行;否则的话,创建 docker0 网桥。具体实现为createBridge(bridgeIP),以及createBridgeIface(bridgeIface)

createBridge 的功能是:在 host 主机上启动创建指定名称网桥设备的任务,并为该网桥设备配置一个与其他设备不冲突的网络地址。而 createBridgeIface 通过系统调用负责创建具体实际的网桥设备,并设置 MAC 地址,通过 libcontainer 中 netlink 包的 CreateBridge 来实现。

4.5.2. 启用 iptables 功能

创建完网桥之后,Docker Daemon 为容器以及 host 主机配置 iptables,包括为 container 之间所需要的 link 操作提供支持,为 host 主机上所有的对外对内流量制定传输规则等。代码位于./docker/daemon/networkdriver/bridge/driver/driver.go#L133-L137, 如下:

// Configure iptables for link support
if enableIPTables {
	if err := setupIPTables(addr, icc); err != nil {
		return job.Error(err)
	}
}

其中 setupIPtables 的调用过程中,addr 地址为 Docker 网桥的网络地址,icc 为 true,即为允许 Docker 容器间互相访问。假设网桥设备名为 docker0,网桥网络地址为 docker0_ip, 设置 iptables 规则,操作步骤如下:

(1) 使用 iptables 工具开启新建网桥的 NAT 功能,使用命令如下:

iptables -I POSTROUTING -t nat -s docker0_ip ! -o docker0 -j MASQUERADE

(2) 通过 icc 参数,决定是否允许 container 间通信,并制定相应 iptables 的 Forward 链。Container 之间通信,说明数据包从 container 内发出后,经过 docker0,并且还需要在 docker0 处发往 docker0,最终转向指定的 container。换言之,从 docker0 出来的数据包,如果需要继续发往 docker0,则说明是 container 的通信数据包。命令使用如下:

iptables -I FORWARD -i docker0 -o docker0 -j ACCEPT

(3) 允许接受从 container 发出,且不是发往其他 container 数据包。换言之,允许所有从 docker0 发出且不是继续发向 docker0 的数据包,使用命令如下:

iptables -I FORWARD -i docker0 ! -o docker0 -j ACCEPT

(4) 对于发往 docker0,并且属于已经建立的连接的数据包,Docker 无条件接受这些数据包,使用命令如下:

iptables -I FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

4.5.3. 启用系统数据包转发功能

在 Linux 系统上,数据包转发功能是被默认禁止的。数据包转发,就是当 host 主机存在多块网卡的时,如果其中一块网卡接收到数据包,并需要将其转发给另外的网卡。通过修改 /proc/sys/net/ipv4/ip_forward 的值,将其置为 1,则可以保证系统内数据包可以实现转发功能,代码如下:

if ipForward {
	// Enable IPv4 forwarding
	if err := ioutil.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte{'1', '\n'}, 0644); err != nil {
		job.Logf("WARNING: unable to enable IPv4 forwarding: %s\n", err)
	}
}

4.5.4. 创建 DOCKER 链

在网桥设备上创建一条名为 DOCKER 的链,该链的作用是在创建 Docker container 并设置端口映射时使用。实现代码位于./docker/daemon/networkdriver/bridge/driver/driver.go,如下:

if err := iptables.RemoveExistingChain("DOCKER"); err != nil {
	return job.Error(err)
}
if enableIPTables {
	chain, err := iptables.NewChain("DOCKER", bridgeIface)
	if err != nil {
		return job.Error(err)
	}
		portmapper.SetIptablesChain(chain)
}

4.5.5. 注册 Handler 至 Engine

在创建完网桥,并配置完基本的 iptables 规则之后,Docker Daemon 在网络方面还在 Engine 中注册了 4 个 Handler,这些 Handler 的名称与作用如下:

  • allocate_interface:为 Docker container 分配一个专属网卡;
  • realease_interface:释放网络设备资源;
  • allocate_port:为 Docker container 分配一个端口;
  • link:实现 Docker container 间的 link 操作。

由于在 Docker 架构中,网络是极其重要的一部分,因此 Docker 网络篇会安排在《Docker 源码分析》系列的第六篇。

4.6. 创建 graphdb 并初始化

Graphdb 是一个构建在 SQLite 之上的图形数据库,通常用来记录节点命名以及节点之间的关联。Docker Daemon 使用 graphdb 来记录镜像之间的关联。创建 graphdb 的代码如下:

graphdbPath := path.Join(config.Root, "linkgraph.db")
graph, err := graphdb.NewSqliteConn(graphdbPath)
if err != nil {
	return nil, err
}

以上代码首先确定 graphdb 的目录为 /var/lib/docker/linkgraph.db;随后通过 graphdb 包内的 NewSqliteConn 打开 graphdb,使用的驱动为”sqlite3”,数据源的名称为” /var/lib/docker/linkgraph.db”;最后通过 NewDatabase 函数初始化整个 graphdb,为 graphdb 创建 entity 表,edge 表,并在这两个表中初始化部分数据。NewSqliteConn 函数的实现位于./docker/pkg/graphdb/conn_sqlite3.go,代码实现如下:

func NewSqliteConn(root string) (*Database, error) {
	……
	conn, err := sql.Open("sqlite3", root)
	……
	return NewDatabase(conn, initDatabase)
}

4.7. 创建 execdriver

Execdriver 是 Docker 中用来执行 Docker container 任务的驱动。创建并初始化 graphdb 之后,Docker Daemon 随即创建了 execdriver,具体代码如下:

ed, err := execdrivers.NewDriver(config.ExecDriver, config.Root, sysInitPath, sysInfo)

可见,在创建 execdriver 的时候,需要 4 部分的信息,以下简要介绍这 4 部分信息:

  • config.ExecDriver:Docker 运行时中指定使用的 exec 驱动类别,在默认配置文件中默认使用”native”, 也可以将这个值改为”lxc”,则使用 lxc 接口执行 Docker container 内部的操作 ;
  • config.Root:Docker 运行时的 root 路径,默认配置文件中为”/var/lib/docker”;
  • sysInitPath: 系统上存放 dockerinit 二进制文件的路径,一般为”/var/lib/docker/init/dockerinit-1.2.0”;
  • sysInfo: 系统功能信息,包括:容器的内存限制功能,交换区内存限制功能,数据转发功能,以及 AppArmor 安全功能等。

在执行 execdrivers.NewDriver 之前,首先通过以下代码,获取期望的目标 dockerinit 文件的路径 localPath,以及系统中 dockerinit 文件实际所在的路径 sysInitPath:

localCopy := path.Join(config.Root, "init", fmt.Sprintf("
dockerinit-%s", dockerversion.VERSION))
sysInitPath := utils.DockerInitPath(localCopy)

通过执行以上代码,localCopy 为”/var/lib/docker/init/dockerinit-1.2.0”,而 sysyInitPath 为当前 Docker 运行时中 dockerinit-1.2.0 实际所处的路径,utils.DockerInitPath 的实现位于 ./docker/utils/util.go。若 localCopy 与 sysyInitPath 不相等,则说明当前系统中的 dockerinit 二进制文件,不在 localCopy 路径下,需要将其拷贝至 localCopy 下,并对该文件设定权限。

设定完 dockerinit 二进制文件的位置之后,Docker Daemon 创建 sysinfo 对象,记录系统的功能属性。SysInfo 的定义,位于./docker/pkg/sysinfo/sysinfo.go,如下:

type SysInfo struct {
	MemoryLimit            bool
	SwapLimit              bool
	IPv4ForwardingDisabled bool
	AppArmor               bool
}

其中 MemoryLimit 通过判断 cgroups 文件系统挂载路径下是否均存在 memory.limit_in_bytes 和 memory.soft_limit_in_bytes 文件来赋值,若均存在,则置为 true,否则置为 false。SwapLimit 通过判断 memory.memsw.limit_in_bytes 文件来赋值,若该文件存在,则置为 true,否则置为 false。AppArmor 通过 host 主机是否存在 /sys/kernel/security/apparmor 来判断,若存在,则置为 true,否则置为 false。

执行 execdrivers.NewDriver 时,返回 execdriver.Driver 对象实例,具体代码实现位于 ./docker/daemon/execdriver/execdrivers/execdrivers.go,由于选择使用 native 作为 exec 驱动,故执行以下的代码,返回最终的 execdriver,其中 native.NewDriver 实现位于./docker/daemon/execdriver/native/driver.go

return native.NewDriver(path.Join(root, "execdriver", "native"), initPath)

至此,已经创建完毕一个 execdriver 的实例 ed。

4.8. 创建 daemon 实例

Docker Daemon 在经过以上诸多设置以及创建对象之后,整合众多内容,创建最终的 Daemon 对象实例 daemon,实现代码如下:

daemon := &Daemon{
	repository:     daemonRepo,
	containers:     &contStore{s: make(map[string]*Container)},
	graph:          g,
	repositories:   repositories,
	idIndex:        truncindex.NewTruncIndex([]string{}),
	sysInfo:        sysInfo,
	volumes:        volumes,
	config:         config,
	containerGraph: graph,
	driver:         driver,
	sysInitPath:    sysInitPath,
	execDriver:     ed,
	eng:            eng,
}

以下分析 Daemon 类型的属性:

属性名

作用

repository

部署所有 Docker 容器的路径

containers

用于存储具体 Docker 容器信息的对象

graph

存储 Docker 镜像的 graph 对象

repositories

存储 Docker 镜像元数据的文件

idIndex

用于通过简短有效的字符串前缀定位唯一的镜像

sysInfo

系统功能信息

volumes

管理 host 主机上 volumes 内容的 graphdriver,默认为 vfs 类型

config

Config.go 文件中的配置信息,以及执行产生的配置 DisableNetwork

containerGraph

存放 Docker 镜像关系的 graphdb

driver

管理 Docker 镜像的驱动 graphdriver,默认为 aufs 类型

sysInitPath

系统 dockerinit 二进制文件所在的路径

execDriver

Docker Daemon 的 exec 驱动,默认为 native 类型

eng

Docker 的执行引擎 Engine 类型实例

4.9. 检测 DNS 配置

创建完 Daemon 类型实例 daemon 之后,Docker Daemon 使用 daemon.checkLocaldns() 检测 Docker 运行环境中 DNS 的配置, checkLocaldns 函数的定义位于./docker/daemon/daemon.go,代码如下:

func (daemon *Daemon) checkLocaldns() error {
	resolvConf, err := resolvconf.Get()
	if err != nil {
		return err
	}
	if len(daemon.config.Dns) == 0 && utils.CheckLocalDns(resolvConf) {
		log.Infof("Local (127.0.0.1) DNS resolver found in resolv.conf and 
containers can't use it. Using default external servers : %v", DefaultDns)
		daemon.config.Dns = DefaultDns
	}
	return nil
}

以上代码首先通过 resolvconf.Get() 方法获取 /etc/resolv.conf 中的 DNS 服务器信息。若本地 DNS 文件中有 127.0.0.1,而 Docker container 不能使用该地址,故采用默认外在 DNS 服务器,为 8.8.8.8,8.8.4.4,并将其赋值给 config 文件中的 Dns 属性。

4.10. 启动时加载已有 Docker containers

当 Docker Daemon 启动时,会去查看在 daemon.repository,也就是在 /var/lib/docker/containers 中的内容。若有存在 Docker container 的话,则让 Docker Daemon 加载这部分容器,将容器信息收集,并做相应的维护。

4.11. 设置 shutdown 的处理方法

加载完已有 Docker container 之后,Docker Daemon 设置了多项在 shutdown 操作中需要执行的 handler。也就是说:当 Docker Daemon 接收到特定信号,需要执行 shutdown 操作时,先执行这些 handler 完成善后工作,最终再实现 shutdown。实现代码如下:

eng.OnShutdown(func() {
	if err := daemon.shutdown(); err != nil {
		log.Errorf("daemon.shutdown(): %s", err)
	}
	if err := portallocator.ReleaseAll(); err != nil {
		log.Errorf("portallocator.ReleaseAll(): %s", err)
	}
	if err := daemon.driver.Cleanup(); err != nil {
		log.Errorf("daemon.driver.Cleanup(): %s", err.Error())
	}
	if err := daemon.containerGraph.Close(); err != nil {
		log.Errorf("daemon.containerGraph.Close(): %s", err.Error())
	}
})

可知,eng 对象 shutdown 操作执行时,需要执行以上作为参数的 func(){……}函数。该函数中,主要完成 4 部分的操作:

  • 运行 daemon 对象的 shutdown 函数,做 daemon 方面的善后工作;
  • 通过 portallocator.ReleaseAll(),释放所有之前占用的端口资源;
  • 通过 daemon.driver.Cleanup(),通过 graphdriver 实现 unmount 所有 layers 中的挂载点;
  • 通过 daemon.containerGraph.Close() 关闭 graphdb 的连接。

4.12. 返回 daemon 对象实例

当所有的工作完成之后,Docker Daemon 返回 daemon 实例,并最终返回至 mainDaemon() 中的加载 daemon 的 goroutine 中继续执行。

5. 总结

本文从源码的角度深度分析了 Docker Daemon 启动过程中 daemon 对象的创建与加载。在这一环节中涉及内容极多,本文归纳总结 daemon 实现的逻辑,一一深入,具体全面。

在 Docker 的架构中,Docker Daemon 的内容是最为丰富以及全面的,而 NewDaemon 的实现而是涵盖了 Docker Daemon 启动过程中的绝大部分。可以认为 NewDaemon 是 Docker Daemon 实现过程中的精华所在。深入理解 NewDaemon 的实现,即掌握了 Docker Daemon 运行的来龙去脉。

6. 作者简介

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

7. 参考文献

http://docs.studygolang.com/pkg/

http://www.iptables.info/en/iptables-matches.html

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

http://crosbymichael.com/the-lost-packages-of-docker.html


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

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

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论