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

1. 前言

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

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

2. NewDaemon做用简介

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

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

3. NewDaemon源码分析内容安排

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

图3.1 Docker Daemon中NewDaemon执行流程图sql

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

下文会在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. 参考文献

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

相关文章
相关标签/搜索