首页 > 代码库 > Docker源码分析之——Docker启动

Docker源码分析之——Docker启动

        在上文中,笔者通过分析Docker的架构,初步作了Docker的架构图。架构图本身更多的出于笔者的理解,为了便于理解,对于Docker代码本身做了一些抽象,例如Server的运行都是以一个Job的形式存在的,而架构图中并未明显的表明这一点。



Docker模块简介

       

       本文将从源码的角度分析Docker的启动,主要是作为一个daemon进程的启动。在这之前,需要先清晰Docker内部最主要的几个概念:Daemon,Engine以及Job。


Daemon

       

        Daemon可以认为是Docker守护进程的载体。从源码的视角来看,Daemon可以认为是Daemon结构体,以及Daemon package中定义的一系列方法的总和。同时Daemon也是Docker内部的一个结构体,从结构体的定义,可以看出Daemon关联了Docker的绝大部分的内容,Daemon结构的定义如下:

type Daemon struct {
	repository     string
	sysInitPath    string
	containers     *contStore
	graph          *graph.Graph
	repositories   *graph.TagStore
	idIndex        *truncindex.TruncIndex
	sysInfo        *sysinfo.SysInfo
	volumes        *graph.Graph
	eng            *engine.Engine
	config         *Config
	containerGraph *graphdb.Database
	driver         graphdriver.Driver
	execDriver     execdriver.Driver
}

       以下简要介绍结构体内部每个对象:

  • repository            所有container的目录的父目录
  • sysInitPath           sysInit的路径
  • containers            所有container的存储记录
  • graph                   关于image的graph信息
  • repositories         Graph的存储库
  • idIndex        
  • sysInfo                 Docker所处host的系统信息
  • volumes               宿主机上且在容器根目录外的一些目录,可以挂载至容器内部
  • eng                       Docker内部所有Job的执行引擎
  • config                   Docker所需要的配置信息
  • containerGraph    GraphDB对象,用于graph信息的存取
  • driver                    有关image的存储的graph驱动
  • execDriver           有关容器运行与管理的操作驱动
        由于介绍繁杂的Docker内属性不是本文的目的,故不再赘述。

Engine

       
       在源代码中关于Engine的介绍非常确切:Engine是整个Docker的核心部分,它扮演所有Docker Container的存储仓库的角色,并且通过执行Job来实现操纵这些容器。

       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
}
       其中需要特别注意的就是handlers属性,该属性为一个map类型的对象,存储的都是关于某个特定handler的处理方法,之后会详细分析handler。

Job


        关于Job的定义,源码中注释如此说道:在Docker的engine中,Job是最基本基本工作单位。Docker可以做的所有工作,最终都必须表示成一个Job。例如:在容器内执行一个进程,这是一个Job;创建一个新容器,这是一个Job;从Internet上下载一份文档,这是一个Job;服务于HTTP的API,这也是一个Job,等等。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
}

        同时,Job的API设计得很像一个unix的进程:比如说,Job有一个名称,有参数,有环境变量,有标准的输入输出,有错误处理,有返回状态,其中返回0表示执行成功,返回其他数字表示错误。


Docker的启动


        Docker的启动可以认为是通过Docker的可执行文件,启动一个Docker的守护进程,这个守护进程在启动过程中完成了启动所需要的所有工作,并且最终作为一个server可以为docker client发送来的众多请求服务。


        以下从源码的角度分析Docker的启动。


        首先,Docker的main函数位于./docker/docker.go中。执行流程如下。


reexec、flag解析与判断

        

        在main函数中,首先执行了以下内容:

	if reexec.Init() {
		return
	}
	flag.Parse()
	// FIXME: validate daemon flags here

	if *flVersion {
		showVersion()
		return
	}
	if *flDebug {
		os.Setenv("DEBUG", "1")
	}
        首先判断reexec.Init()的返回值,若为真,直接返回;否则进行flag.Parse()方法,该方法主要解析了flag参数,并为之后的flag参数判断做准备。众多的flag参数位于./docker/flag.go中,并且在main函数执行之前就完成var的定义以及init函数的执行。解析玩flag参数之后,随即判断众参数,若*flVersion为真的话,直接通过showVersion()方法显示Docker的版本号,随后返回;若*flVersion不为真,则继续往下判断,若*flDebug为真,对于DEBUG环境变量设置值为1,继续往下执行。


flHost信息的获取


        接着的代码执行如下:

	if len(flHosts) == 0 {
		defaultHost := os.Getenv("DOCKER_HOST")
		if defaultHost == "" || *flDaemon {
			// If we do not have a host, default to unix socket
			defaultHost = fmt.Sprintf("unix://%s", api.DEFAULTUNIXSOCKET)
		}
		defaultHost, err := api.ValidateHost(defaultHost)
		if err != nil {
			log.Fatal(err)
		}
		flHosts = append(flHosts, defaultHost)
	}


        以上代码的功能主要是查找host地址,作为之后server的监听地址。如果在flag的定义以及初始化之后,flHost的长度依旧为0的话,则说明配置中没有设定Host地址,需要程序自行查找。首先,通过宿主机环境变量中的DOCKER_HOST来给默认host变量defaultHost赋值,如果仍然为空,或者*flDaemon为真的话,通过api定义的DEFAULTDAEMON属性来初始化defaultHost,默认为一个unix socket。经过验证该defaultHost之后,将defaultHost添加至flHost末尾。


        以上可以认为是为Docker daemon的运行做了充足的准备工作,以下的代码真正在做Docker Daemon的启动。

	if *flDaemon {
		mainDaemon()
		return
	}


        也就是说若*flDaemon为真,则直接运行mainDaemon()方法。以下将大篇幅分析介绍mainDaemon()所做的工作。



mainDaemon()


        mainDaemon()的实现位于文件./docker/daemon.go中。


flag.Narg()


        首先,Daemon执行flag.NArg(),当flag参数被处理后,已经没有其他的参数时,继续往下执行。


创建engine并捕获shutdown


        Daemon创建一个engine,并随时捕获engine的shutdown信号。


加载builtins


        加载builtins,代码为builtins.Register(eng),进入./docker/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)
}

        1. daemon(eng) : 所做工作是为engine注册一个handler,具体的handler名称为“init_networkdriver”。具体的功能是初始化Docker环境的docker0网桥,处理方法的实现位于./daemon/networkdriver/bridge/driver.go中的InitDriver.

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

        2.remote(eng)  : 所做的工作是为engine注册两个handler,第一个handler的名称为“serveapi”,具体的功能是使得daemon提供RESTful的API,保证daemon可以与外界建立通信,处理方法的实现为./api/server/server.go中的ServeApi;第二个handler的名称为“acceptconnection”,具体的功能是使得初始化完毕的daemon可以接收请求,处理方法的实现为./api/server/server.go中的AcceptConnections。代码如下:

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

        3.events.New().Install(eng): Docker的event事件的实现,功能是让外界知道Docker内部的events,内部的log以及内部的subscribers_count,具体的job名称分别为“events”,“logs”,subscribers_count“,处理方法的实现为./events/events.go中的Get,Log,SubscribersCount 。
// Install installs events public api in docker engine
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
}

         4.eng.Register("version",dockerVersion): Docker的engine注册一个名称为“ version”的handler,处理方法的实现为当前builtins.go文件中的dockerVersion。


         5.registry.NewService().Install(eng):方法实现位于./registry/service.go,首先先获取Service对象,随后通过Install方法来注册两个handler,第一个的名称为“auth”,实现在公有registry中的认证;第二个的名称为“search”,实现在公有registry中查找image的功能。

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


加载Daemon

 

        以上分析大部分builtins.Register(eng)的实现。回到mainDaemon方法中,即进入一个goroutine,如下:

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

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

	// after the daemon is done setting up we can tell the api to start
	// accepting connections
	if err := eng.Job("acceptconnections").Run(); err != nil {
		log.Fatal(err)
	}
}()

        以上使用一个goroutine来加载daemon,以保证与此同时,可以尽快的运行serveapi的job,以致有些connection来临时不会因为daemon正在加载而得不到相应。



NewDaemon()


        首先执行的是d, err := daemon.NewDaemon(daemonCfg, eng),作用为创建一个daemon对象,代码实现位于./daemon/daemon.go的NewDaemon方法。在NewDaemon的实现过程中,可以发现具体调用的方法为daemon, err := NewDaemonFromDirectory(config, eng)。在这里,我们可以先来看看该config参数的来历。在加载daemon的goroutine中,NewDaemon的实参为daemonCfg。在./docker/daemon.go中,有daemonCfg = &daemon.Config{},而在该文件中的init()方法中实现了daemonCfg.InstallFlags(),而InstallFlags()的实现位于./docker/daemon/config.go,实现过程中加载了很多需要的配置项,几乎Docker所需要的所有配置信息都在该放啊中实现初始化。


         这里涉及到了Golang的一个特性,即init()方法的执行。在golang中init()方法的特性如下:

  1. init方法用于程序执行前包的初始化工作,比如初始化变量等;
  2. 每个包可以有多个init方法;
  3. 包的每一个源文件也可以有多个init方法;
  4. 同一个包内的init方法的执行顺序没有明确的定义;
  5. 不同包的init方法按照包导入的依赖关系决定初始化的顺序;
  6. init方法不能内调用,而是在main()函数调用前自动被调用。


        了解完config的来历,进入NewDaemonFromDirectory的实现。该方法的实现,可以简易的认为提供以下功能。


        1.验证或配置config参数

// Apply configuration defaults
if config.Mtu == 0 {
	// FIXME: GetDefaultNetwork Mtu doesn't need to be public anymore
	config.Mtu = GetDefaultNetworkMtu()
}
// Check for mutually incompatible config options
if config.BridgeIface != "" && config.BridgeIP != "" {
	return nil, fmt.Errorf("You specified -b & --bip, mutually exclusive options. Please specify only one.")
}
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.")
}
// FIXME: DisableNetworkBidge doesn't need to be public anymore
config.DisableNetwork = config.BridgeIface == DisableNetworkBridge

// Claim the pidfile first, to avoid any and all unexpected race conditions.
// Some of the init doesn't need a pidfile lock - but let's not try to be smart.
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)
	})
}


        2.验证系统支持度以及执行用户的权限

// Check that the system is supported and we have sufficient privileges
// FIXME: return errors instead of calling Fatal
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())
}

 

       3.配置或创建Docker所需要的工作路径

// set up the TempDir to use a canonical path
tmp, err := utils.TempDir(config.Root)
if err != nil {
	log.Fatalf("Unable to get the TempDir under %s: %s", config.Root, err)
}
realTmp, err := utils.ReadSymlinkedDirectory(tmp)
if err != nil {
	log.Fatalf("Unable to get the full path to the TempDir (%s): %s", tmp, err)
}
os.Setenv("TMPDIR", realTmp)
if !config.EnableSelinuxSupport {
	selinuxSetDisabled()
}
// get the canonical path to the Docker root directory
var realRoot string
if _, err := os.Stat(config.Root); err != nil && os.IsNotExist(err) {
	realRoot = config.Root
} else {
	realRoot, err = utils.ReadSymlinkedDirectory(config.Root)
	if err != nil {
		log.Fatalf("Unable to get the full path to root (%s): %s", config.Root, err)
	}
}
config.Root = realRoot
// Create the root directory if it doesn't exists
if err := os.MkdirAll(config.Root, 0700); err != nil && !os.IsExist(err) {
	return nil, err
}


        4.设置以及加载多种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)
// 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!")
}


        5.创建Docker Image所需要的graph,graphdb,volumns等

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

// We don't want to use a complex driver like aufs or devmapper
// for volumes, just a plain filesystem
volumesDriver, err := graphdriver.GetDriver("vfs", config.Root, config.GraphOptions)
if err != nil {
	return nil, err
}
log.Debugf("Creating volumes graph")
volumes, err := graph.NewGraph(path.Join(config.Root, "volumes"), volumesDriver)
if err != nil {
	return nil, err
}
log.Debugf("Creating repository list")
repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g)
if err != nil {
	return nil, fmt.Errorf("Couldn't create Tag store: %s", err)
……
graphdbPath := path.Join(config.Root, "linkgraph.db")
    graph, err := graphdb.NewSqliteConn(graphdbPath)
    if err != nil {
        return nil, err
} 


        6.关于dockerinit的一系列操作

localCopy := path.Join(config.Root, "init", fmt.Sprintf("dockerinit-%s", dockerversion.VERSION))
sysInitPath := utils.DockerInitPath(localCopy)
if sysInitPath == "" 
	return nil, fmt.Errorf("Could not locate dockerinit: This usually means docker was built incorrectly. See http://docs.docker.com/contributing/devenvironment for official build instructions.")
}


        7.验证DNS,判断docker container是否可以使用host的resolv.conf文件,若不能的话,使用默认的外界DNS server:“8.8.8.8”和“8.8.4.4”;

if err := daemon.checkLocaldns(); err != nil {
	return nil, err
}

        8.重新加载之前的docker container。例如,当Docker进程重启后,会restore之前运行着的docker container。

if err := daemon.restore(); err != nil {
	return nil, err
}


        9.最终返回daemon对象

return daemon, nil


       以上的9个步骤执行完NewDaemonFromDirectory之后,在goroutine之间执行d.Install(eng),该方法的实现位于./daemon/daemon.go中的Install方法,功能是为engine注册众多的handler,handler的actions位于./daemon/下的众多go文件中。例如有以下{"create":            daemon.ContainerCreate}handler,则当job的名称为create时,运行时的action为daemon.ContainerCreate, 位于./daemon/create.go。



builderJob.Install()


        随后执行代码为:

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

      

        这部分内容的功能为注册build的handler,位于./builder/job.go文件中,job的名称为“build”,处理方法为CmdBuild具体实现如下:

func (b *BuilderJob) Install() {
	b.Engine.Register("build", b.CmdBuild)
}


eng.Job("acceptconnections").Run()


        goroutine的最后一个步骤就是开始执行接收请求,即执行名称为“acceptconnections”的job,处理方法为./api/server/server.go中的AcceptConnections。


        以上的部分,即表示goroutine的运行流程,即加载daemon的运行流程。


    

eng.Job("serveapi", flHosts...).Run()


        在goroutine运行的同时,mainDaemon同时还在执行名称为“serveapi“的job,代码如下:

	// Serve api
	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)
	}

        在创建job的同时,使用到了参数flHost,也就是在mainDaemon之前的获取的flHost。由于在./builtins/builtins.go中注册了名称为“serveapi”的handler,所以只要运行相应的处理方法即可,为./api/server/server.go中的ServeApi方法。



总结


        以上的分析都是假设flDaemon为真,那样的话Docker的启动流程就结束了。


        由于Docker中所有关于container以及image等工作都必须暴露为一个job,因此Docker启动的完毕标志,可以认为是Docker完成server的启动,并最终为通过api来访问的请求进行服务。通过server来代理请求,并最终分发到相应的job上来执行。


        在Docker整个启动过程中,笔者认为最为重要,最为核心的部分为NewDaemonFromDirectory的实现,该部分配置了众多Daemon结构内部的属性,而这些属性在之后,都会涉及到很多实际操作container以及graph的工作,换言之,daemon保留了其他模块的访问接口。


        因此,在Docker内部,运行靠engine,执行靠job,访问driver等靠daemon。



转载请注明出处。

本文更多出于我本人的理解,肯定在一些地方存在不足和错误。希望本文能够对接触Docker的人有些帮助,如果你对这方面感兴趣,并有更好的想法和建议,也请联系我。

我的邮箱:shlallen@zju.edu.cn
新浪微博:@莲子弗如清

Docker源码分析之——Docker启动