Docker Daemon启动
Docker Daemon 是Docker架构中运行在后台的守护进程,可分为Docker Server、Engine和Job三部分。
Docker Daemon 是通过Docker Server模块接受Docker Client的请求,并在Engine中处理请求,然后根据请求类型,创建出指定的Job并运行,运行过程的几种可能:向Docker Registry获取镜像,通过graphdriver执行容器镜像的本地化操作,通过networkdriver执行容器网络环境的配置,通过execdriver执行容器内部运行的执行工作等。
Docker Daemon架构示意图:
Docker Daemon启动流程图
通过流程图可以看出,有关DockerDaemon的所有工作都在mainDaemon()方法中实现。
mainDaemon()方法:
功能:1)创建Docker运行环境;2)服务于Docker Client,接受并处理请求。
细节:
1) daemon的配置初始化;(在init()函数中实现,即mainDaemon()运行前就执行)
var ( daemonCfg = &daemon.Config{} ) funcinit() { daemonCfg.InstallFlags() }
首先,声明一个为daemon包中Config类型的变量,名为daemonCfg。而Config对象,定义了Docker Daemon所需的配置信息。在Docker Daemon在启动时,daemonCfg变量被传递至Docker Daemon并被使用。
Config对象的定义如下:
typeConfig struct { Pidfile string //Docker Daemon所属进程的PID文件 Root string //Docker运行时所使用的root路径 AutoRestart bool //已被启用,转而支持docker run时的重启 Dns []string //Docker使用的DNS Server地址 DnsSearch []string //Docker使用的指定的DNS查找域名 Mirrors []string //指定的优先Docker Registry镜像 EnableIptables bool //启用Docker的iptables功能 EnableIpForward bool //启用net.ipv4.ip_forward功能 EnableIpMasq bool //启用IP伪装技术 DefaultIp net.IP //绑定容器端口时使用的默认IP BridgeIface string //添加容器网络至已有的网桥 BridgeIP string //创建网桥的IP地址 FixedCIDR string //指定IP的IPv4子网,必须被网桥子网包含 InterContainerCommunication bool //是否允许相同host上容器间的通信 GraphDriver string //Docker运行时使用的特定存储驱动 GraphOptions []string //可设置的存储驱动选项 ExecDriver string // Docker运行时使用的特定exec驱动 Mtu int //设置容器网络的MTU DisableNetwork bool //有定义,之后未初始化 EnableSelinuxSupport bool //启用SELinux功能的支持 Context map[string][]string //有定义,之后未初始化 }
init()函数实现了daemonCfg变量中各属性的赋值,具体的实现为:daemonCfg.InstallFlags():
func(config *Config) InstallFlags() { flag.StringVar(&config.Pidfile,[]string{"p", "-pidfile"}, "/var/run/docker.pid", "Path to use for daemon PID file") flag.StringVar(&config.Root,[]string{"g", "-graph"}, "/var/lib/docker", "Pathto use as the root of the Docker runtime") …… opts.IPVar(&config.DefaultIp,[]string{"#ip", "-ip"}, "0.0.0.0", "DefaultIP address to use whenbinding container ports") opts.ListVar(&config.GraphOptions,[]string{"-storage-opt"}, "Set storage driver options") …… }
在InstallFlags()函数的实现过程中,主要是定义某种类型的flag参数,并将该参数的值绑定在config变量的指定属性上。
2) 命令行flag参数检查;
当docker命令经过flag参数解析之后,判断剩余的参数是否为0。若为0,则说明Docker Daemon的启动命令无误,正常运行;若不为0,则说明在启动DockerDaemon的时候,传入了多余的参数,此时会输出错误提示,并退出运行程序。具体代码如下:
ifflag.NArg() != 0 { flag.Usage() return }
3) 创建engine对象;
mainDaemon()运行过程中,flag参数检查完毕之后,随即创建engine对象,代码如下:
eng :=engine.New()
Engine是Docker架构中的运行引擎,同时也是Docker运行的核心模块。Engine扮演着Docker container存储仓库的角色,并且通过job的形式来管理这些容器。
在./docker/engine/engine.go中,Engine结构体的定义如下:
type Enginestruct { 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 }
之后,进入New()函数的实现中:
func New()*Engine { //创建Engine结构体实例eng eng := &Engine{ handlers: make(map[string]Handler), id: utils.RandomString(), Stdout: os.Stdout, Stderr: os.Stderr, Stdin: os.Stdin, Logging: true, } //向eng对象注册名为commands的Handler eng.Register("commands", func(job*Job) Status { for _, name := range eng.commands() { job.Printf("%s\n",name) } return StatusOK }) // Copy existing global handlers将已定义的变量globalHandlers中的所有的Handler,都 //复制到eng对象的handlers属性中 for k, v := range globalHandlers { eng.handlers[k] = v } return eng }
New()函数最终返回一个Engine对象。在代码实现部分,第一个工作即为创建一个Engine结构体实例eng;第二个工作是向eng对象注册名为commands的Handler,其中Handler为临时定义的函数func(job *Job) Status{ } , 该函数的作用是通过job来打印所有已经注册完毕的command名称,最终返回状态StatusOK;第三个工作是:将已定义的变量globalHandlers中的所有的Handler,都复制到eng对象的handlers属性中。最后成功返回eng对象。
4) 设置engine的信号捕获及处理方法;
执行后续代码:
signal.Trap(eng.Shutdown)
该部分代码的作用是:在Docker Daemon的运行中,设置Trap特定信号的处理方法,特定信号有SIGINT,SIGTERM以及SIGQUIT;当程序捕获到SIGINT或者SIGTERM信号时,执行相应的善后操作,最后保证Docker Daemon程序退出。
Trap()函数的代码如下:
funcTrap(cleanup func()) { c:= make(chan os.Signal, 1) signals:= []os.Signal{os.Interrupt, syscall.SIGTERM} ifos.Getenv("DEBUG") == "" { signals= append(signals, syscall.SIGQUIT) } gosignal.Notify(c,signals...) gofunc() { interruptCount:= uint32(0) forsig := range c { gofunc(sig os.Signal) { log.Printf("Received signal ‘%v‘, starting shutdown ofdocker...\n", sig) switch sig { case os.Interrupt, syscall.SIGTERM: // If the user really wants to interrupt,let him do so. if atomic.LoadUint32(&interruptCount)< 3 { atomic.AddUint32(&interruptCount, 1) // Initiate the cleanup only once if atomic.LoadUint32(&interruptCount)== 1 { // Call cleanup handler cleanup() os.Exit(0) } else { return } } else { log.Printf("Force shutdown ofdocker, interrupting cleanup\n") } case syscall.SIGQUIT: } os.Exit(128 + int(sig.(syscall.Signal))) }(sig) } }() }
实现流程分为4个步骤:
1)创建并设置一个channel,用于发送信号通知;
2)定义signals数组变量,初始值为os.SIGINT, os.SIGTERM;若环境变量DEBUG为空的话,则添加os.SIGQUIT至signals数组;
3)通过gosignal.Notify(c, signals...)中Notify函数来实现将接收到的signal信号传递给c。需要注意的是只有signals中被罗列出的信号才会被传递给c,其余信号会被直接忽略;
4)创建一个goroutine来处理具体的signal信号,当信号类型为os.Interrupt或者syscall.SIGTERM时,执行传入Trap函数的具体执行方法,形参为cleanup(),实参为eng.Shutdown。
Shutdown()函数的工作是为Docker Daemon的关闭做一些善后工作。
善后工作如下:
1)Docker Daemon不再接收任何新的Job;
2)Docker Daemon等待所有存活的Job执行完毕;
3)Docker Daemon调用所有shutdown的处理方法;
4)当所有的handler执行完毕,或者15秒之后,Shutdown()函数返回。
5) 加载builtins;
为eng设置完Trap特定信号的处理方法之后,Docker Daemon实现了builtins的加载。代码实现如下:
if err :=builtins.Register(eng); err != nil { log.Fatal(err) }
加载builtins的主要工作是为:为engine注册多个Handler,以便后续在执行相应任务时,运行指定的Handler。这些Handler包括:网络初始化、web API服务、事件查询、版本查看、Docker Registry验证与搜索。代码实现位于./docker/builtins/builtins.go,如下:
funcRegister(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) }
以上代码实现主要有5个部分:daemon(eng)、remote(eng)、events.New().Install(eng)、eng.Register(“version”,dockerVersion)以及registry.NewService().Install(eng)。
daemon(eng):注册初始化网络驱动的Handler
remote(eng):注册API服务的Handler
events.New().Install(eng):注册events事件的Handler
eng.Register(“version”,dockerVersion):注册版本的Handler
registry.NewService().Install(eng):注册registry的Handler
6) 使用goroutine加载daemon对象并运行;
执行完builtins的加载,回到mainDaemon()的执行,通过一个goroutine来加载daemon对象并开始运行。这一环节的执行,主要包含三个步骤:
1)通过init函数中初始化的daemonCfg与eng对象来创建一个daemon对象d;
2)通过daemon对象的Install函数,向eng对象中注册众多的Handler;
3)在Docker Daemon启动完毕之后,运行名为”acceptconnections”的job,主要工作为向init守护进程发送”READY=1”信号,以便开始正常接受请求。
代码实现如下:
go func() { d, err := daemon.MainDaemon(daemonCfg, eng) if err != nil { log.Fatal(err) } if err := d.Install(eng); err != nil { log.Fatal(err) } if err :=eng.Job("acceptconnections").Run(); err != nil { log.Fatal(err) } }()
三个步骤详解:
1)创建daemon对象:daemon.MainDaemon(daemonCfg, eng)是创建daemon对象d的核心部分。主要作用为初始化Docker Daemon的基本环境,如处理config参数,验证系统支持度,配置Docker工作目录,设置与加载多种driver,创建graph环境等,验证DNS配置等。
2)通过daemon对象为engine注册Handler:
创建完daemon对象,goroutine执行d.Install(eng),具体实现位于./docker/daemon/daemon.go:
func (daemon*Daemon) Install(eng *engine.Engine) error { for name, method := rangemap[string]engine.Handler{ "attach": daemon.ContainerAttach, …… "image_delete": daemon.ImageDelete, } { if err := eng.Register(name, method);err != nil { return err } } if err := daemon.Repositories().Install(eng);err != nil { return err } eng.Hack_SetGlobalVar("httpapi.daemon",daemon) return nil }
以上代码的实现分为三部分:
向eng对象中注册众多的Handler对象;
daemon.Repositories().Install(eng)实现了向eng对象注册多个与image相关的Handler,Install的实现位于./docker/graph/service.go;
eng.Hack_SetGlobalVar("httpapi.daemon", daemon)实现向eng对象中map类型的hack对象中添加一条记录,key为”httpapi.daemon”,value为daemon。
3)运行acceptconnection的job:
在goroutine内部最后运行名为”acceptconnections”的job,主要作用是通知init守护进程,Docker Daemon可以开始接受请求了。
7) 打印Docker版本信息及驱动信息;
回到mainDaemon()的运行流程中,在goroutine的执行之时,mainDaemon()函数内部其它代码也会并发执行。
显示docker的版本信息,以及ExecDriver和GraphDriver这两个驱动的具体信息,代码如下:
logrus.WithFields(logrus.Fields{ "version": dockerversion.VERSION, "commit": dockerversion.GITCOMMIT, "execdriver": d.ExecutionDriver().Name(), "graphdriver":d.GraphDriver().String(), }).Info("Docker daemon")
8) Job之“serveapi”的创建与运行。
打印部分Docker具体信息之后,DockerDaemon立即创建并运行名为”serveapi”的job,主要作用为让Docker Daemon提供API访问服务。实现代码位于./docker/docker/daemon.go:
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) }
实现过程中,首先创建一个名为”serveapi”的job,并将flHosts的值赋给job.Args。flHost的作用主要是为Docker Daemon提供使用的协议与监听的地址。随后,Docker Daemon为该job设置了众多的环境变量,如安全传输层协议的环境变量等。最后通过job.Run()运行该serveapi的job。
至此,可以认为DockerDaemon已经完成了serveapi这个job的初始化工作。一旦acceptconnections这个job运行完毕,则会通知init进程Docker Daemon启动完毕,可以开始提供API服务。本文从源码的角度简单分析了Docker Daemon的启动,着重分析了mainDaemon()的实现。
原文地址:http://9320314.blog.51cto.com/9310314/1632738