Hang

docker 源码分析(3) -- daemon 启动流程

2015-02-05

本篇的主要内容是关于docker daemon的启动流程。其主要内容均包含在
github.com/docker/docker/docker/daemon.go文件中的mainDaemon函数中,本文即按
其执行流程分析源码。因为所涉源码较多,所以所涉部分多是点到为止,详细分析会在后续
分专篇讲述。

Engine#

mainDaemon的开始处,在确认参数解析无误后,首先便生成了一个Engine的实例:

eng := engine.New()
signal.Trap(eng.Shutdown)

Engine可以说是docker的核心。它用来执行docker的各种操作(统一为job的形式),
管理container的存储。其结构定义为 :

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
}

大部分均可见名知意,下面对部分字段进行详细解析。

Handler#

Engine结构体中中最关键的便是handlers映射表,docker daemon启动时会向其中注册各种功
能的handler,比如关于网络设置的、web server、版本等等,然后就可以通过名字调用进行
初始化:

type Handler func(*Job) Status

各个模块在初始化时只要设置好相应环境变量并注册一个job即可。统一的函数接口能够
docker内部各组件在代码结构和执行流程上更加清晰一致。

Job#

jobEngine最为基本的执行单元。所有的docker操作,比如启动一个
container,在container内部执行一个程序,从网络pull一个镜像等等,都可以用
job来表示。

type Job struct {
Eng *Engine
Name string
Args []string
env *Env
Stdout *Output
Stderr *Output
Stdin *Input
handler Handler
status Status
end time.Time
closeIO bool
}
const (
StatusOK Status = 0
StatusErr Status = 1
StatusNotFound Status = 127
)

从结构体的定义来看,jobunix上的进程的结构表示非常类似:名字、参数、环境变
量、标准输入输出、退出状态(0 表示成功,其他表示错误)……我们完全可以将其当作像进程一样的概念
来看待。

Initializes#

New函数用来初始化一个Engine,基本上只是对各变量进行简单的初始化:

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
}

注意点:

  1. Engine id是一个随机字符串
  2. 注册了一个commandshandler,用来返回Engine所支持的commands(handlers表中的key)列表。
  3. 如果已经有预定义好的globalHandlers,也添加到Enginehandlers表中.

Shutdown#

Engine关闭的流程大概如下:

  1. 不再接受新的执行job的请求
  2. 等待所有正在执行中的job结束
  3. 并发调用已经注册的各个shutdown handlers
  4. 所有handlers结束或者等待 15 秒后返回

具体可参考github.com/docker/docker/engine/engine.go#Shutdown()

上面mainDaemonTrap的设置可以让Engine像大多数unix程序一样在接收到信号时做
一些指定的操作:

  • SIGINT 或者 SIGTERM, 直接调用eng.Shutdown,然后程序结束
  • SIGINT 或者 SIGTERMeng.Shutdown执行完成之前重复了 3 次,那么就直接停止执行并且直接结束程序
  • 如果DEBUG环境变量被设置,SIGQUIT会直接让程序退出而不调用eng.Shutdown

Builtins#

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

builtins包主要用来给Engine注册一些内部使用的handlers : 网络设置、
apiserverEvents设置和version信息 :

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 nil
}

因为只是注册,具体的执行还在后面,所以这里暂时不深入探讨各个handler的详细内容,
等分析到实际执行的时候再结合运行时信息详细探讨,理解起来应该更容易一些。这里只列
出注册的handlers映射信息:

Name Handler
init_networkdriver bridge.InitDriver
serveapi apiserver.ServeApi
acceptconnections apiserver.AcceptConnections
version dockerVersion

Version#

因为dockerVersion的实现比较简单,所以就直接写在了
github.com/docker/docker/builtins/builtins.go里面:

func dockerVersion(job *engine.Job) engine.Status {
v := &engine.Env{}
v.SetJson("Version", dockerversion.VERSION)
v.SetJson("ApiVersion", api.APIVERSION)
v.SetJson("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
}

我们可以直接通过执行docker version命令来查看其大概效果:

Events#

我们可以先通过docker events命令来看看dockerEvents是干嘛用的。如图,启动一个
container

在另一个窗口的docker events命令显示结果:

可见Events是类似于 log 的一种东西,不过是一种结构化的记录方式,而且只记录特定的
运行时信息。

const eventsLimit = 64

type listener chan<- *utils.JSONMessage

type Events struct {
mu sync.RWMutex
events []*utils.JSONMessage
subscribers []listener
}

func New() *Events {
return &Events{
events: make([]*utils.JSONMessage, 0, eventsLimit),
}
}

而前面提到的events.New().Install(eng)也是向Engine注册了一些handlers:

func (e *Events) Install(eng *engine.Engine) error {
// Here you should describe public interface
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
}

具体的函数实现则不再赘述。

Registry#

if err := registry.NewService(daemonCfg.InsecureRegistries).Install(eng); err != nil {
log.Fatal(err)
}

registry主要是给Engine提供认证和搜索官方(dockerhub)镜像的能力:

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

如代码所示,Registry注册了authsearch两个handler

Daemon#

经过前面的那么多设置,Engine算是配置的差不多了,下面就是对daemon进行各项配置
的时候了:

go func() {
d, err := daemon.NewDaemon(daemonCfg, eng)
if err != nil {
log.Fatal(err)
}

log.Infof("docker daemon: %s %s; execdriver: %s; graphdriver: %s",
dockerversion.VERSION,
dockerversion.GITCOMMIT,
d.ExecutionDriver().Name(),
d.GraphDriver().String(),
)

if err := d.Install(eng); err != nil {
log.Fatal(err)
}

b := &builder.BuilderJob{eng, d}
b.Install()

if err := eng.Job("acceptconnections").Run(); err != nil {
log.Fatal(err)
}
}()

主要内容如下:

  1. daemon各个模块的设置,创建daemon。这部分内容非常长,下面将详述。
  2. 打印一些关键日志信息。如下图所示:

  3. Engine注册daemon所提供的各种handlers,主要就是docker client各种命令
    的后台实现:

  4. docker build的后台handler实现。因为这个命令实现比较复杂,所以单列。

  5. daemon设置完成后即启动api server准备接受请求。

Config#

Config定义了docker daemon的各项配置:

type Config struct {
Pidfile string
Root string
AutoRestart bool
Dns []string
DnsSearch []string
Mirrors []string
EnableIptables bool
EnableIpForward bool
EnableIpMasq bool
DefaultIp net.IP
BridgeIface string
BridgeIP string
FixedCIDR string
InsecureRegistries []string
InterContainerCommunication bool
GraphDriver string
GraphOptions []string
ExecDriver string
Mtu int
DisableNetwork bool
EnableSelinuxSupport bool
Context map[string][]string
TrustKeyPath string
Labels []string
}
type Daemon struct {
ID string
repository string
sysInitPath string
containers *contStore
execCommands *execStore
graph *graph.Graph
repositories *graph.TagStore
idIndex *truncindex.TruncIndex
sysInfo *sysinfo.SysInfo
volumes *volumes.Repository
eng *engine.Engine
config *Config
containerGraph *graphdb.Database
driver graphdriver.Driver
execDriver execdriver.Driver
trustStore *trust.TrustStore
}

从这些配置项也可以看出,很多都是与docker启动时的参数一一对应的。NewDaemon
数即通过这些参数来进行daemon的各项设置:

Settings#

从上面ConfigDaemon的定义也可以看出,二者包含了docker运行时需要关注
的绝大部分内容及组件。而具体的设置由
github.com/docker/docker/daemon/daemon.go#NewDaemonFromDirectory完成,因为比较
琐碎,所以将其归为以下几类介绍:

network args#

因为网络参数比较多,有的还有冲突,所有还要进行一定的检测。关于网络的设置主要由以
下几项:

  1. MTU,容器网络的最大传输单元。未指定则使用默认值: 1500。如果网络环境的自定义程
    度较高,则MTU需要小心设置,不然可能因为额外的封包解包过程导致包大小超过MTU
    被丢弃。
  2. --bridge--bip 参数不能同时指定。因为bridge是用来创建自定义的
    bridge网络,而--bip是用来给默认的docker0指定其他地址和掩码的。
  3. --iptables=false--icc=false不能同时指定。因为ICC依赖于iptables

system#

  • pidfile的创建和管理
if config.Pidfile != "" {
if err := utils.CreatePidFile(config.Pidfile); err != nil {
return nil, err
}
eng.OnShutdown(func() {
// Always release the pidfile last, just in case
utils.RemovePidFile(config.Pidfile)
})
}

使用pid文件可以说是linux上大多数daemon服务的一种通用模式了: 没有则创建,
并且在程序退出时删除(通过shutdown handler来处理)。

  • 操作系统及内核版本检测,要求 linux 3.8 以上的 kernel.
if runtime.GOOS != "linux" {
return nil, fmt.Errorf("The Docker daemon is only supported on linux")
}

if err := checkKernelAndArch(); err != nil {
return nil, err
}
  • 权限检测,docker需要root权限运行
if os.Geteuid() != 0 {
return nil, fmt.Errorf("The Docker daemon needs to be run as root")
}
  • TempDir设置

这里的TempDir是相对于docker的目录而言的,并不是指系统的/tmp目录。从参数设置
上可以看到默认的根目录为/var/lib/docker:

flag.StringVar(&config.Root, []string{"g", "-graph"}, "/var/lib/docker", "Path to use as the root of the Docker runtime")

如果使用默认值,则TempDir/var/lib/docker/tmp

func TempDir(rootDir string) (string, error) {
var tmpDir string
if tmpDir = os.Getenv("DOCKER_TMPDIR"); tmpDir == "" {
tmpDir = filepath.Join(rootDir, "tmp")
}
err := os.MkdirAll(tmpDir, 0700)
return tmpDir, err
}

  • SELinux

检测是否开启SELinux支持。SELinuxApparmor是 docker 支持的两种安全机制,SELinux功能强大,架构也比较复
杂,AppArmor则相反。

if !config.EnableSelinuxSupport {
selinuxSetDisabled()
}
  • Docker root directory

Docker所有文件存储的根目录,默认为/var/lib/docker

graphdriver#

graph driver是主要用来管理容器文件系统及镜像存储的组件,与宿主机对各文件系统的支持
相关。比如ubuntu上默认使用的是AUFS,Centos上是devicemapper,Coreos上则是btrfs
graph driver定义了一个统一的、抽象的接口,以一种可扩展的方式对各文件系统提供了支持。


// Set the default driver
graphdriver.DefaultDriver = config.GraphDriver

// Load storage driver
driver, err := graphdriver.New(config.Root, config.GraphOptions)
if err != nil {
return nil, err
}
log.Debugf("Using graph driver %s", driver)

因为config.GraphDriver并没有设置(没有供用户指定的参数选项),所以graphDriver会从其支持的文件系统列表中
一个一个检测系统是否支持,找到一个支持的即设为要用的 driver :

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
}

priority列表为:

priority = []string{
"aufs",
"btrfs",
"devicemapper",
"vfs",
// experimental, has to be enabled manually for now
"overlay",
}

如果使用的是btrfs,因为其与SELinux的不兼容,所以还要进行一些检测:

if selinuxEnabled() && config.EnableSelinuxSupport && driver.String() == "btrfs" {
return nil, fmt.Errorf("SELinux is not supported with the BTRFS graph driver!")
}

之后检测/var/lib/docker/containers目录是否存在,不存在则创建。我们来看看
containers目录下的内容:

每个container创建的时候,与网络有关的配置文件
(/etc/hosts,/etc/resolv.conf等)与其他文件的处理是不同的,他们是通过挂载的方式
container使用的,有点类似于docker container本身的存储方式: 一个只读的层,
加上一些可写的层。containers目录就是用来存储这些信息的。

graph#

g, err := graph.NewGraph(path.Join(config.Root, "graph"), driver)
if err != nil {
return nil, err
}

Graph是用来存储标记的文件系统镜像以及他们之间的关系的组件:

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

其中idIndex的作用是使我们可以使用长 id 的前缀来检索镜像,RootGraph的根目录,一般为/var/lib/docker/graphNewGraph即是用此目
录下的文件来重建镜像索引。我们可以查看此目录下的目录的结构:

每一个镜像一个目录,下面包含一个描述镜像信息的 json 文件,也包含了记录镜像大小的
layersize文件。我们用一些实例来对比查看一下,下图是docker images --tree的部分
结果:

我们选取487e08镜像来对照,json 文件记录了其parent image的 id、创建时间、大小等
等信息。这个大小与 layersize 文件中的相一致。

volumes#

Volumes是一种特殊的目录,其数据可以被一个或多个container共享,它和创建它的
container的生命周期分离开来,在container被删去之后能继续存在。在实现上,使用
的依然是只读层和读写层结合(union file system)的方式。

volumesDriver, err := graphdriver.GetDriver("vfs", config.Root, config.GraphOptions)
if err != nil {
return nil, err
}

volumes, err := volumes.NewRepository(path.Join(config.Root, "volumes"), volumesDriver)
if err != nil {
return nil, err
}

VFS是一个中间层,下面是个各种文件系统实现,对外提供的则是统一的访问接口,这非常
类似我们之前提到的GraphDriver的机制。刚开始看这段代码很难知道它是干嘛用的,但我们还可以
仿照之前Graph部分先对/var/lib/docker/volumes目录进行一番探究。

我们先用官方的例子创建一个包含Volumescontainer

然后通过docker inspect查看与Volumes相关的信息:

到获取到的目录去看下:

里面什么也没有。我们进到 container 内部在/webapp目录下创建一个文件看看:

可以确定,/var/lib/docker/vfs目录下的目录是用来存储Volumes中实际数据的。我们
再来看看/var/lib/docker/volumes目录下的内容:

可以看到,这个目录只用来存储关于Volumes的关键信息的。

明白了这些之后,就会发现上面的代码和之前的与Graph有关的代码是非常类似的 : 初始
driver,然后从相应目录里读取原有的关于image或者container的信息并加载。

repository#

repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g, config.Mirrors, config.InsecureRegistries)
if err != nil {
return nil, fmt.Errorf("Couldn't create Tag store: %s", err)
}

我们依然先来查看下相关的文件: /var/lib/docker/repositories-aufs :

整个 json 文件记录了所有的镜像的不同的 tag 及其对应的 id.从函数名NewTagStore上也可
以看出,tag信息的记录是其主要功能之一。

type TagStore struct {
path string
graph *Graph
mirrors []string
insecureRegistries []string
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{}
}

pullingPool记录有哪些镜像正在被下载,若某一个镜像正在被下载,则驳回其他Docker Client发起下载该镜像的请求。pushingPool记录有哪些镜像正在被上传,若某一个镜像
正在被上传,则驳回其他Docker Client发起上传该镜像的请求;

trust#

trustDir := path.Join(config.Root, "trust")
if err := os.MkdirAll(trustDir, 0700); err != nil && !os.IsExist(err) {
return nil, err
}
t, err := trust.NewTrustStore(trustDir)
if err != nil {
return nil, fmt.Errorf("could not create trust store: %s", err)
}

还是先看/var/lib/docker/trust下的内容:

跟认证签名有关的一些信息。这个文件是从下面这个地方获取到的:

var baseEndpoints = map[string]string{"official": "https://dvjy3tqbc323p.cloudfront.net/trust/official.json"}

然后用其中的内容来初始化TrustStore:

type TrustStore struct {
path string
caPool *x509.CertPool
graph trustgraph.TrustGraph
expiration time.Time
fetcher *time.Timer
fetchTime time.Duration
autofetch bool
httpClient *http.Client
baseEndpoints map[string]*url.URL

sync.RWMutex
}

init_networkdriver#

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

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

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

前面提到在Builtins里注册了这个handler,这里就利用启动参数进行了相关环境变
量的设置并真正开始启动这个 Job。主要内容如下:

  1. bridge及其 ip 设置,一般都是使用默认的docker0

  1. iptablesipforward设置。

  2. fixed cidr 设置。这个可以用来限制contaienrdocker0获取到的ip地址的范
    围。

  3. 注册了一些供以后进行各个容器的网络设置的handlers:

    for name, f := range map[string]engine.Handler{
    "allocate_interface": Allocate,
    "release_interface": Release,
    "allocate_port": AllocatePort,
    "link": LinkContainers,
    } {
    if err := job.Eng.Register(name, f); err != nil {
    return job.Error(err)
    }
    }

linkgraph.db#

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

/var/lib/docker/linkgraph.db是一个 SQLITE3 的数据库文件。里面有两个表: edge
entity(两个图理论中常用的概念)。查看其内容:

edge里存储了容器的名字和 id,entity只存储了容器的 id。daemon通过这个数据库来重
建容器名称与 id 的关联。

execdriver#

sysInfo := sysinfo.New(false)
ed, err := execdrivers.NewDriver(config.ExecDriver, config.Root, sysInitPath, sysInfo)
if err != nil {
return nil, err
}

docker最开始使用的是linuxlxc作为其底层的容器执行引擎,后来自己开发了
libcontainer,用来替代lxc,所以我们现在看到docker info里显示的Excution Drivernative:

sysinfocgroup相关的一些系统信息,lxc exec driver初始化时需要从其中获取关于系统中apparmor的一些信息,但native exec driver不需要。

type SysInfo struct {
MemoryLimit bool
SwapLimit bool
IPv4ForwardingDisabled bool
AppArmor bool
}
func NewDriver(name, root, initPath string, sysInfo *sysinfo.SysInfo) (execdriver.Driver, error) {
switch name {
case "lxc":
return lxc.NewDriver(root, initPath, sysInfo.AppArmor)
case "native":
return native.NewDriver(path.Join(root, "execdriver", "native"), initPath)
}
return nil, fmt.Errorf("unknown exec driver %s", name)
}

看看代码中提到的目录/var/lib/docker/execdriver/native:

又是一堆container或者镜像的id,既然是执行引擎了,多半是关于container的一些
运行时信息,挑一个进去查看一下:

state.json主要描述了此container所在cgroup的相关目录,网络状态,以及主进程的
pid及启动时间。container.json包含信息较多,部分截图如下:

  1. 各个设备的访问权限,主要是/dev下面那些
  2. 一些特殊文件的信息。比如/etc/hosts,Volumes,/etc/resolv.conf等等
  3. 网络详细信息
  4. capabilites
  5. namespaces
  6. 环境变量

Restore#

经过前面各个组件的设置及初始化,终于到了daemon的创建了:

daemon := &Daemon{
ID: trustKey.PublicKey().KeyID(),
repository: daemonRepo,
containers: &contStore{s: make(map[string]*Container)},
execCommands: newExecStore(),
graph: g,
repositories: repositories,
idIndex: truncindex.NewTruncIndex([]string{}),
sysInfo: sysInfo,
volumes: volumes,
config: config,
containerGraph: graph,
driver: driver,
sysInitPath: sysInitPath,
execDriver: ed,
eng: eng,
trustStore: t,
}
if err := daemon.restore(); err != nil {
return nil, err
}

基本上用到了我们前面设置好的各个组件。之后的restore便开始加载原有的container
将设为自启动的container启动。

Shutdown#

前面提到过Engine在关闭时会调用各个注册好的handlers,这里便是一个:

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())
}
})

主要进行daemon自身的清理工作,端口的释放,挂载点的卸载,与graphdb连接的关闭。

ServeApi#

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)
}

查看之前在Builtins中注册的handlers表,可知serveapi对应的是
apiserver.ServeApi函数。ServeApi即开始监听参数中指定的各种协议和端口,并准备
开始处理http请求了(docker clientdaemon 的交互都是通过REST API来进行
的)。

参考链接#

  1. VFS
  2. How Docker container volumes work even when they aren’t running?
  3. Advanced Docker Volumes
  4. Network Configuration
  5. Docker 源码分析(四):Docker Daemon 之 NewDaemon 实现
Tags: docker