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的人有些帮助,如果你对这方面感兴趣,并有更好的想法和建议,也请联系我。

我的邮箱:[email protected]

新浪微博:@莲子弗如清

时间: 2024-10-10 07:48:21

Docker源码分析之——Docker启动的相关文章

Docker源码分析之——Docker Client的启动与命令执行

在上文Docker源码分析之--Docker Daemon的启动 中,介绍了Docker Daemon进程的启动.Docker Daemon可以认为是一个Docker作为Server的运行载体,而真正发送关于docker container操作的请求的载体,在于Docker Client.本文从Docker源码的角度,分析Docker Client启动与执行请求的过程. Docker Client启动的流程与Docker Daemon启动的过程相仿.首先执行reexec.Init():随后解析f

Docker源码分析之——Docker架构

Docker是PaaS圈内开源的基于LXC的应用容器引擎,基于Go语言开发,遵从Apache2.0协议. 最近一年来,Docker在云计算方面的热度持续升温,社区等活跃度也持续走高,使得大家对于Docker普遍持有积极态度. 笔者在研究生期间,主要从事PaaS方面的研究与实践工作,具体的研究平台为开源的Cloud Foundry.最近Docker的火热,更是让自己处于对Docker的学习过程中,不能自拔. 从功能的角度,Docker于Cloud Foundry的warden相仿.然而从产品的定位

docker 源码分析 一(基于1.8.2版本),docker daemon启动过程;

最近在研究golang,也学习一下比较火的开源项目docker的源代码,国内比较出名的docker源码分析是孙宏亮大牛写的一系列文章,但是基于的docker版本有点老:索性自己就git 了一下最新的代码研读: docker是c/s的架构,分为docker client 和 docker daemon,client端发送命令,daemon端负责完成client发送过来的命令(如获取和存储镜像.管理容器等).两者之间可以通过TCP,HTTP和UNIX SOCKET来进行通信: docker的启动入口

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

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

docker 源码分析 四(基于1.8.2版本),Docker镜像的获取和存储

前段时间一直忙些其他事情,docker源码分析的事情耽搁了,今天接着写,上一章了解了docker client 和 docker daemon(会启动一个http server)是C/S的结构,client端发出的命令由docker daemon接收并处理. 我们在运行docker的时候,可能会使用到docker run命令(当然通过Dockerfile运行docker build命令也是一样的)时,如果本地没有你需要的镜像,docker daemon首先会去下载你需要的docker镜像,然后存

Docker源码分析(一):Docker架构

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

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

1. 前言 Docker的生态系统日趋完善,开发者群体也在日趋庞大,这让业界对Docker持续抱有极其乐观的态度.如今,对于广大开发者而言,使用Docker这项技术已然不是门槛,享受Docker带来的技术福利也不再是困难.然而,如何探寻Docker适应的场景,如何发展Docker周边的技术,以及如何弥合Docker新技术与传统物理机或VM技术的鸿沟,已经占据Docker研究者们的思考与实践. 本文为<Docker源码分析>第四篇——Docker Daemon之NewDaemon实现,力求帮助广

Docker源码分析(二):Docker Client创建与命令执行

1. 前言 如今,Docker作为业界领先的轻量级虚拟化容器管理引擎,给全球开发者提供了一种新颖.便捷的软件集成测试与部署之道.在团队开发软件时,Docker可以提供可复用的运行环境.灵活的资源配置.便捷的集成测试方法以及一键式的部署方式.可以说,Docker的优势在简化持续集成.运维部署方面体现得淋漓尽致,它完全让开发者从持续集成.运维部署方面中解放出来,把精力真正地倾注在开发上. 然而,把Docker的功能发挥到极致,并非一件易事.在深刻理解Docker架构的情况下,熟练掌握Docker C

Docker源码分析(五):Docker Server的创建

1.Docker Server简介 Docker架构中,Docker Server是Docker Daemon的重要组成部分.Docker Server最主要的功能是:接受用户通过Docker Client发送的请求,并按照相应的路由规则实现路由分发. 同时,Docker Server具备十分优秀的用户友好性,多种通信协议的支持大大降低Docker用户使用Docker的门槛.除此之外,Docker Server设计实现了详尽清晰的API接口,以供Docker用户选择使用.通信安全方面,Docke