在上文中,笔者通过分析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()方法的特性如下:
- init方法用于程序执行前包的初始化工作,比如初始化变量等;
- 每个包可以有多个init方法;
- 包的每一个源文件也可以有多个init方法;
- 同一个包内的init方法的执行顺序没有明确的定义;
- 不同包的init方法按照包导入的依赖关系决定初始化的顺序;
- 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]
新浪微博:@莲子弗如清