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()的實現。