本節書摘來異步社群《容器技術系列》一書中的第3章 ,第3.3節,孫宏亮 著, 更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
docker daemon的啟動流程圖展示了dockerdaemon的從無到有。通過分析流程圖,我們可以得出一個這樣的結論:區分docker daemon與docker client的關鍵在于flag參數fldaemon的值。一旦*fldaemon的值為真,則代表docker二進制需要啟動的是docker daemon。有關docker daemon的所有的工作,都被包含在函數maindaemon()的具體實作中。
宏觀來講,maindaemon()的使命是:建立一個守護程序,并保證其正常運作。
從功能的角度來說,maindaemon()實作了兩部分内容:第一,建立docker運作環境;第二,服務于docker client,接收并處理相應請求(完成docker server的初始化)。
從實作細節來分析,maindaemon()的實作流程主要包含以下步驟:
1)daemon的配置初始化。這部分在init()函數中實作,即在maindaemon()運作前就執行,但由于這部分内容和maindaemon()的運作息息相關,可以認為是maindaemon()運作的先決條件。
2)指令行flag參數檢查。
3)建立engine對象。
4)設定engine的信号捕獲及處理方法。
5)加載builtins。
6)使用goroutine加載daemon對象并運作。
7)列印docker版本及驅動資訊。
8)serveapi的建立與運作。
對于以上内容,本章将一一深入分析。
maindaemon()的運作位于./docker/docker/docker/daemon.go,深入分析maindaemon()的實作之前,我們回到go 語言的特性,即變量與init函數的執行順序。在daemon.go中,docker定義了變量daemoncfg,以及init函數,通過go 語言的特性,變量的定義與init函數均會在maindaemon()之前運作。兩者的定義如下:
在函數installflags()的實作過程中,docker主要定義了衆多類型不一的flag參數,并将該參數的值綁定在daemoncfg變量的指定屬性上,如:
以上語句的含義為:
定義一個string類型的flag參數。
該flag的名稱為"p"或者"-pidfile"。
該flag的預設值為"/var/run/docker.pid",并将該值綁定在變量config.pidfile上。
該flag的描述資訊為"path to use for daemon pid file"。
至此,關于docker daemon所需要的配置資訊均聲明并初始化完畢。
從本小節開始,程式運作真正進入docker daemon的maindaemon(),下面對此流程進行深入分析。
maindaemon()運作的第一個步驟是指令行flag參數的檢查。具體而言,即當docker指令經過flag參數解析之後,docker判斷剩餘的參數是否為0。若為0,則說明docker daemon的啟動指令無誤,正常運作;若不為0,則說明在啟動docker daemon的時候,傳入了多餘的參數,此時docker會輸出錯誤提示,并退出運作程式。具體代碼如下:
在maindaemon()運作過程中,flag參數檢查完畢之後,docker随即建立engine對象,代碼如下:
engine是docker架構中的運作引擎,同時也是docker運作的核心子產品。engine扮演着docker container存儲倉庫的角色,并且通過job的形式管理docker運作中涉及的所有任務。
engine結構體的定義位于./docker/docker/engine/engine.go#l47-l60,具體代碼如下:
engine結構體中最為重要的是handlers屬性,handlers屬性為map類型,key的類型是string,value的類型是handler。其中handler類型的定義位于./docker/docker/engine/engine.go#l23,具體代碼如下:
type handler func(*job) status
可見,handler為一個定義的函數。該函數傳入的參數為job指針,傳回為status狀态。
了解完engine以及handler的基本知識之後,我們真正進入建立engine執行個體的部分,即new()函數的實作,具體代碼如下:
分析以上代碼,從傳回結果可以發現,new()函數最終傳回一個engine執行個體對象。而在代碼實作部分,大緻可以将其分為三個步驟:
1)建立一個engine結構體執行個體eng,并初始化部分屬性,如handlers、id、标準輸出stdout、日志屬性logging等。
2)向eng對象注冊名為commands的handler,其中handler為臨時定義的函數func(job *job) status{ },該函數的作用是通過job來列印所有已經注冊完畢的command名稱,最終傳回狀态statusok。
3)将變量globalhandlers中定義完畢的所有handler都複制到eng對象的handlers屬性中。
至此,一個基本的engine對象執行個體eng已經建立完畢,并實作部分屬性的初始化。docker daemon啟動的後續過程中,仍然會對engine對象執行個體進行額外的配置。
docker在包engine中執行完engine對象的建立與初始化之後,回到maindaemon()函數的運作,緊接着執行的代碼為:
signal.trap(eng.shutdown)
docker daemon作為linux作業系統上的一個背景程序,原則上應該具備處理信号的能力。信号處理能力的存在,能保障docker管理者可以通過向docker daemon發送信号的方式,管理docker daemon的運作。
再來看以上代碼則不難了解其中的含義:在docker daemon的運作中,設定捕獲特定信号後的處理方法,特定信号有sigint、sigterm以及sigquit;當程式捕獲sigint或者sigterm信号時,執行相應的善後操作,最後保證docker daemon程式退出。
該部分代碼的實作位于./docker/docker/pkg/signal/trap.go。實作的流程分為以下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/docker/engine/engine.go#l153-l199,主要完成的任務是:docker daemon關閉時,做一些必要的善後工作。
善後工作主要有以下4項:
在15秒時間内,若所有的handler執行完畢,則 shutdown()函數傳回,否則強制傳回。
由于在signal.trap( eng.shutdown )函數的具體實作中,一旦程式接收到相應的信号,則會執行eng.shutdown這個函數,在執行完eng.shutdown之後,随即執行os.exit(0),完成目前整個docker daemon程式的退出。源碼實作位于./docker/docker/pkg/signal/trap.go#l33-l47。
dockerdaemon設定完trap特定信号的處理方法(即eng.shutdown()函數)之後,docker daemon實作了builtins的加載。docker的builtins可以了解為:docker daemon運作過程中,注冊的一些任務(job),這部分任務一般與容器的運作無關,與docker daemon的運作時資訊有關。加載builtins的源碼實作如下:
加載builtins完成的具體工作是:向engine注冊多個handler,以便後續在執行相應任務時,運作指定的handler。這些handler包括:docker daemon主控端的網絡初始化、web api服務、事件查詢、版本檢視、docker registry的驗證與搜尋等。源碼實作位于./docker/docker/builtins/builtins.go#l16-l30,如下:
下面分析register函數實作過程中最為主要的5個部分:daemon(eng)、remote(eng)、events.new().install(eng)、eng.register("version",dockerversion)以及registry.newservice().install(eng)。
注冊網絡初始化處理方法
daemon(eng)的實作過程,主要為eng對象注冊了一個鍵為"init_networkdriver"的處理方法,此處理方法的值為bridge.initdriver函數,源碼如下:
需要注意的是,向eng對象注冊處理方法,并不代表處理方法的值函數會被立即調用執行,如注冊init_networkdrive時bridge.initdriver并不會直接運作,而是将bridge.initdriver的函數入口作為init_networkdriver的值,寫入eng的handlers屬性中。當docker daemon接收到名為init_networkdriver的job的執行請求時,bridge.initdriver才被docker daemon調用執行。
bridge.initdriver的具體實作位于./docker/docker/daemon/networkdriver/bridge/driver.go#79-l175,主要作用為:
擷取為docker服務的網絡裝置位址。
建立指定ip位址的網橋。
配置網絡iptables規則。
另外還為eng對象注冊了多個handler,如allocate_interface、release_interface、allocate_port以及link等。
本書将在第6章詳細分析docker daemon如何初始化主控端的網絡環境。
注冊api服務處理方法
remote(eng)的實作過程,主要為eng對象注冊了兩個handler,分别為serveapi與acceptconnections,源碼實作如下:
注冊的兩個處理方法名稱分别為serveapi與acceptconnections,相應的執行方法分别為apiserver.serveapi與apiserver.acceptconnections,具體實作位于./docker/docker/api/server/server.go。其中,serveapi執行時,通過循環多種指定協定,建立出goroutine協調來配置指定的http.server,最終為不同協定的請求服務;而acceptconnections的作用主要是:通知主控端上init守護程序docker daemon已經啟動完畢,可以讓docker daemon開始服務api請求。
注冊events事件處理方法
events.new().install(eng)的實作過程,為docker注冊了多個event事件,功能是給docker使用者提供api,使得使用者可以通過這些api檢視docker内部的events資訊,log資訊以及subscribers_count資訊。具體的源碼位于./docker/docker/events/events.go#l29-l42,如下所示:
注冊版本處理方法
eng.register("version",dockerversion)的實作過程,向eng對象注冊key為version,value為dockerversion執行方法的handler。dockerversion的執行過程中,會向名為version的job的标準輸出中寫入docker的版本、docker api的版本、git版本、go語言運作時版本,以及作業系統版本等資訊。dockerversion的源碼實作如下:
注冊registry處理方法
registry.newservice().install(eng)的實作過程位于./docker/docker/registry/service.go,功能是:在eng對象對外暴露的api資訊中添加docker registry的資訊。若registry.newservice()被成功安裝,則會有兩個相應的處理方法注冊至eng,docker daemon通過docker client提供的認證資訊向registry發起認證請求;search,在公有registry上搜尋指定的鏡像,目前公有的registry隻支援docker hub。
install的具體實作如下:
至此,docker daemon所有builtins的加載全部完成,實作了向eng對象注冊特定的處理方法。
docker執行完builtins的加載之後,再次回到maindaemon()的執行流程中。此時,docker通過一個goroutine協程加載daemon對象并開始運作docker server。這一環節的執行,主要包含以下三個步驟:
1)通過init函數中初始化的daemoncfg與eng對象,建立一個daemon對象d。
2)通過daemon對象的install函數,向eng對象中注冊衆多的處理方法。
3)在docker daemon啟動完畢之後,運作名為acceptconnections的job,主要工作為向init守護程序發送ready=1信号,以便docker server開始正常接收請求。
源碼實作位于./docker/docker/docker/daemon.go#l43-l56,如下所示:
下面詳細分析三個步驟所做的工作。
建立daemon對象
daemon.newdaemon(daemoncfg, eng)是建立daemon對象d的核心部分,主要作用是初始化docker daemon的基本環境,如處理config參數,驗證系統支援度,配置docker工作目錄,設定與加載多種驅動,建立graph環境,驗證dns配置等。
由于daemon.maindaemon(daemoncfg, eng)是加載docker daemon的核心部分,且篇幅過長,本書第4章将深入分析newdaemon的實作。
通過daemon對象為engine注冊handler
docker建立完daemon對象,goroutine立即執行d.install(eng),具體實作位于./docker/daemon/daemon.go,代碼如下所示:
以上代碼的實作同樣分為三部分:
向eng對象中注冊衆多的處理方法對象。
daemon.repositories().install(eng)實作了向eng對象注冊多個與docker鏡像相關的handler,install的實作位于./docker/docker/graph/service.go。
eng.hack_setglobalvar("httpapi.daemon", daemon)實作向eng對象中類型為map的hack對象中添加一條記錄,鍵為httpapi.daemon,值為daemon。
運作名為acceptconnections的job
docker在goroutine的最後環節運作名為acceptconnections的job,主要作用是通知init守護程序,使docker daemon開始接受請求。源碼位于./docke<code>`</code>javascript
r/docker/docker/daemon.go#l53-l55,如下所示:
// after the daemon is done setting up we can tell the api to start
// accepting connections
if err := eng.job("acceptconnections").run(); err != nil {
}
func (eng engine) job(name string, args ...string) job {
func acceptconnections(job *engine.job) engine.status {
log.printf("docker daemon: %s %s; execdriver: %s; graphdriver: %s",
)
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 {