[TOC=3]
## 1\. 前言
Docker的生態系統日趨完善,開發者群體也在日趨龐大,這讓業界對Docker持續抱有極其樂觀的態度。如今,對于廣大開發者而言,使用Docker這項技術已然不是門檻,享受Docker帶來的技術福利也不再是困難。然而,如何探尋Docker適應的場景,如何發展Docker周邊的技術,以及如何彌合Docker新技術與傳統物理機或VM技術的鴻溝,已經占據Docker研究者們的思考與實踐。
本文為《Docker源碼分析》第四篇——Docker Daemon之NewDaemon實現,力求幫助廣大Docker愛好者更多得理解Docker 的核心——Docker Daemon的實現。
## 2\. NewDaemon作用簡介
在Docker架構中有很多重要的概念,如:graph,graphdriver,execdriver,networkdriver,volumes,Docker containers等。Docker在實現過程中,需要將以上實體進行統一化管理,而Docker Daemon中的daemon實例就是設計用來完成這一任務的實體。
從源碼的角度,NewDaemon函數的執行完成了Docker Daemon創建并加載daemon的任務,最終實現統一管理Docker Daemon的資源。
## 3\. NewDaemon源碼分析內容安排
本文從源碼角度,分析Docker Daemon加載過程中NewDaemon的實現,整個分析過程如下圖:

圖3.1 Docker Daemon中NewDaemon執行流程圖
由上圖可見,Docker Daemon中NewDaemon的執行流程主要包含12個獨立的步驟:處理配置信息、檢測系統支持及用戶權限、配置工作路徑、加載并配置graphdriver、創建Docker Daemon網絡環境、創建并初始化graphdb、創建execdriver、創建daemon實例、檢測DNS配置、加載已有container、設置shutdown處理方法、以及返回daemon實例。
下文會在NewDaemon的具體實現中,以12節分別分析以上內容。
## 4\. NewDaemon具體實現
在《Docker源碼分析》系列第三篇中,有一個重要的環節:使用goroutine加載daemon對象并運行。在加載并運行daemon對象時,所做的第一個工作即為:
~~~
d, err := daemon.NewDaemon(daemonCfg, eng)
~~~
該部分代碼分析如下:
* 函數名:NewDaemon;
* 函數調用具體實現所處的包位置:[./docker/daemon](https://github.com/docker/docker/tree/v1.2.0/daemon);
* 函數具體實現源文件:[./docker/daemon/daemon.go](https://github.com/docker/docker/blob/v1.2.0/daemon/daemon.go#L665-L671);
* 函數傳入實參:daemonCfg,定義了Docker Daemon運行過程中所需的眾多配置信息;eng,在mainDaemon中創建的Engine對象實例;
* 函數返回類型:d,具體的Daemon對象實例;err,錯誤狀態。
進入./docker/daemon/daemon.go中[NewDaemon的具體實現](https://github.com/docker/docker/blob/v1.2.0/daemon/daemon.go#L665-L671),代碼如下:
~~~
func NewDaemon(config *Config, eng *engine.Engine) (*Daemon, error) {
daemon, err := NewDaemonFromDirectory(config, eng)
if err != nil {
return nil, err
}
return daemon, nil
}
~~~
可見,在實現NewDaemon的過程中,通過NewDaemonFromDirectory函數來實現創建Daemon的運行環境。該函數的實現,傳入參數以及返回類型與NewDaemon函數相同。下文將大篇幅分析NewDaemonFromDirectory的實現細節。
### 4.1\. 應用配置信息
在NewDaemonFromDirectory的實現過程中,第一個工作是:如何應用傳入的配置信息。這部分配置信息服務于Docker Daemon的運行,并在Docker Daemon啟動初期就初始化完畢。配置信息的主要功能是:供用戶自由配置Docker的可選功能,使得Docker的運行更貼近用戶期待的運行場景。
配置信息的處理包含4部分:
* 配置Docker容器的MTU;
* 檢測網橋配置信息;
* 查驗容器通信配置;
* 處理PID文件配置。
#### 4.1.1\. 配置Docker容器的MTU
config信息中的Mtu應用于容器網絡的最大傳輸單元(MTU)特性。有關MTU的源碼如下:
~~~
if config.Mtu == 0 {
config.Mtu = GetDefaultNetworkMtu()
~~~
可見,若config信息中Mtu的值為0的話,則通過GetDefaultNetworkMtu函數將Mtu設定為默認的值;否則,采用config中的Mtu值。由于在默認的配置文件[./docker/daemon/config.go](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L57)(下文簡稱為默認配置文件)中,初始化時Mtu屬性值為0,故執行GetDefaultNetworkMtu。
GetDefaultNetworkMtu函數的具體實現位于[./docker/daemon/config.go](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L65-L70):
~~~
func GetDefaultNetworkMtu() int {
if iface, err := networkdriver.GetDefaultRouteIface(); err == nil {
return iface.MTU
}
return defaultNetworkMtu
}
~~~
GetDefaultNetworkMtu的實現中,通過networkdriver包的GetDefaultRouteIface方法獲取具體的網絡設備,若該網絡設備存在,則返回該網絡設備的MTU屬性值;否則的話,返回默認的MTU值defaultNetworkMtu,值為1500。
#### 4.1.2\. 檢測網橋配置信息
處理完config中的Mtu屬性之后,馬上檢測config中BridgeIface和BridgeIP這兩個信息。BridgeIface和BridgeIP的作用是為創建網橋的任務”init_networkdriver”提供參數。代碼如下:
~~~
if config.BridgeIface != "" && config.BridgeIP != "" {
return nil, fmt.Errorf("You specified -b & --bip, mutually exclusive options.
Please specify only one.")
}
~~~
以上代碼的含義為:若config中BridgeIface和BridgeIP兩個屬性均不為空,則返回nil對象,并返回錯誤信息,錯誤信息內容為:用戶同時指定了BridgeIface和BridgeIP,這兩個屬性屬于互斥類型,只能至多指定其中之一。而在[默認配置文件](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L51-L52)中,BridgeIface和BridgeIP均為空。
#### 4.1.3\. 查驗容器通信配置
檢測容器的通信配置,主要是針對config中的EnableIptables和InterContainerCommunication這兩個屬性。EnableIptables屬性的作用是啟用Docker對iptables規則的添加功能;InterContainerCommunication的作用是啟用Docker container之間互相通信的功能。代碼如下:
~~~
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.")
}
~~~
代碼含義為:若EnableIptables和InterContainerCommunication兩個屬性的值均為false,則返回nil對象以及錯誤信息。其中錯誤信息為:用戶將以上兩屬性均置為false,container間通信需要iptables的支持,需設置至少其中之一為true。而在[默認配置文件](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L49-L53)中,這兩個屬性的值均為true。
#### 4.1.4\. 處理網絡功能配置
接著,處理config中的DisableNetwork屬性,以備后續在創建并執行創建Docker Daemon網絡環境時使用,即在名為”init_networkdriver”的job創建并運行中體現。
~~~
config.DisableNetwork = config.BridgeIface == DisableNetworkBridge
~~~
由于config中的BridgeIface屬性值為空,另外DisableNetworkBridge的值為字符串”none”,因此最終config中DisableNetwork的值為false。后續名為”init_networkdriver”的job在執行過程中需要使用該屬性。
#### 4.1.5\. 處理PID文件配置
處理PID文件配置,主要工作是:為Docker Daemon進程運行時的PID號創建一個PID文件,文件的路徑即為config中的Pidfile屬性。并且為Docker Daemon的shutdown操作添加一個刪除該Pidfile的函數,以便在Docker Daemon退出的時候,可以在第一時間刪除該Pidfile。[處理PID文件配置信息](https://github.com/docker/docker/blob/v1.2.0/daemon/daemon.go#L691-L699)的代碼實現如下:
~~~
if config.Pidfile != "" {
if err := utils.CreatePidFile(config.Pidfile); err != nil {
return nil, err
}
eng.OnShutdown(func() {
utils.RemovePidFile(config.Pidfile)
})
}
~~~
代碼執行過程中,首先檢測config中的Pidfile屬性是否為空,若為空,則跳過代碼塊繼續執行;若不為空,則首先在文件系統中創建具體的Pidfile,然后向eng的onShutdown屬性添加一個處理函數,函數具體完成的工作為utils.RemovePidFile(config.Pidfile),即在Docker Daemon進行shutdown操作的時候,刪除Pidfile文件。在[默認配置文件](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L46)中,Pidfile文件的初始值為” /var/run/docker.pid”。
以上便是關于配置信息處理的分析。
### 4.2\. 檢測系統支持及用戶權限
初步處理完Docker的配置信息之后,Docker對自身運行的環境進行了一系列的檢測,主要包括三個方面:
* 操作系統類型對Docker Daemon的支持;
* 用戶權限的級別;
* 內核版本與處理器的支持。
系統支持與用戶權限檢測的實現較為簡單,[實現代碼](https://github.com/docker/docker/blob/v1.2.0/daemon/daemon.go#L703-L711)如下:
~~~
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())
}
~~~
首先,通過runtime.GOOS,檢測操作系統的類型。runtime.GOOS返回運行程序所在操作系統的類型,可以是Linux,Darwin,FreeBSD等。結合具體代碼,可以發現,若操作系統不為Linux的話,將報出Fatal錯誤日志,內容為“Docker Daemon只能支持Linux操作系統”。
接著,通過os.Geteuid(),檢測程序用戶是否擁有足夠權限。os.Geteuid()返回調用者所在組的group id。結合具體代碼,也就是說,若返回不為0,則說明不是以root用戶的身份運行,報出Fatal日志。
最后,通過checkKernelAndArch(),檢測內核的版本以及主機處理器類型。checkKernelAndArch()的實現同樣位于[./docker/daemon/daemon.go](https://github.com/docker/docker/blob/v1.2.0/daemon/daemon.go#L1097-L1119)。實現過程中,第一個工作是:檢測程序運行所在的處理器架構是否為“amd64”,而目前Docker運行時只能支持amd64的處理器架構。第二個工作是:檢測Linux內核版本是否滿足要求,而目前Docker Daemon運行所需的內核版本若過低,則必須升級至3.8.0。
### 4.3\. 配置工作路徑
配置Docker Daemon的工作路徑,主要是創建Docker Daemon運行中所在的工作目錄。實現過程中,通過config中的Root屬性來完成。在[默認配置文件](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L47)中,Root屬性的值為”/var/lib/docker”。
在[配置工作路徑的代碼實現](https://github.com/docker/docker/blob/v1.2.0/daemon/daemon.go#L714-L741)中,步驟如下:
(1) 使用規范路徑創建一個TempDir,路徑名為tmp;
(2) 通過tmp,創建一個指向tmp的文件符號連接realTmp;
(3) 使用realTemp的值,創建并賦值給環境變量TMPDIR;
(4) 處理config的屬性EnableSelinuxSupport;
(5) 將realRoot重新賦值于config.Root,并創建Docker Daemon的工作根目錄。
### 4.4\. 加載并配置graphdriver
加載并配置存儲驅動graphdriver,目的在于:使得Docker Daemon創建Docker鏡像管理所需的驅動環境。Graphdriver用于完成Docker容器鏡像的管理,包括存儲與獲取。
#### 4.4.1\. 創建graphdriver
這部分內容的源碼位于[./docker/daemon/daemon.go#L743-L790](https://github.com/docker/docker/blob/v1.2.0/daemon/daemon.go#L743-L790),具體細節分析如下:
~~~
graphdriver.DefaultDriver = config.GraphDriver
driver, err := graphdriver.New(config.Root, config.GraphOptions)
~~~
首先,為graphdriver包中的DefaultDriver對象賦值,值為config中的GraphDriver屬性,在[默認配置文件](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L54)中,GraphDriver屬性的值為空;同樣的,屬性GraphOptions也為空。然后通過graphDriver中的new函數實現加載graph的存儲驅動。
創建具體的graphdriver是相當重要的一個環節,實現細節由graphdriver包中的New函數來完成。進入[./docker/daemon/graphdriver/driver.go](https://github.com/docker/docker/blob/v1.2.0/daemon/graphdriver/driver.go#L81-L111)中,實現步驟如下:
第一,遍歷數組選擇graphdriver,數組內容為os.Getenv(“DOCKER_DRIVER”)和DefaultDriver。若不為空,則通過GetDriver函數直接返回相應的Driver對象實例,若均為空,則繼續往下執行。這部分內容的作用是:讓graphdriver的加載,首先滿足用戶的自定義選擇,然后滿足默認值。代碼如下:
~~~
for _, name := range []string{os.Getenv("DOCKER_DRIVER"), DefaultDriver} {
if name != "" {
return GetDriver(name, root, options)
}
}
~~~
第二,遍歷優先級數組選擇graphdriver,優先級數組的內容為依次為”aufs”,”brtfs”,”devicemapper”和”vfs”。若依次驗證時,GetDriver成功,則直接返回相應的Driver對象實例,若均不成功,則繼續往下執行。這部分內容的作用是:在沒有指定以及默認的Driver時,從優先級數組中選擇Driver,目前優先級最高的為“aufs”。代碼如下:
~~~
for _, name := range priority {
driver, err = GetDriver(name, root, options)
if err != nil {
if err == ErrNotSupported || err == ErrPrerequisites || err == ErrIncompatibleFS {
continue
}
return nil, err
}
return driver, nil
}
~~~
第三,從已經注冊的drivers數組中選擇graphdriver。在”aufs”,”btrfs”,”devicemapper”和”vfs”四個不同類型driver的init函數中,它們均向graphdriver的drivers數組注冊了相應的初始化方法。分別位于[./docker/daemon/graphdriver/aufs/aufs.go](https://github.com/docker/docker/blob/v1.2.0/daemon/graphdriver/aufs/aufs.go#L49-L51),以及其他三類driver的相應位置。這部分內容的作用是:在沒有優先級drivers數組的時候,同樣可以通過注冊的driver來選擇具體的graphdriver。
#### 4.4.2\. 驗證btrfs與SELinux的兼容性
由于目前在btrfs文件系統上運行的Docker不兼容SELinux,因此當config中配置信息需要啟用SELinux的支持并且driver的類型為btrfs時,返回nil對象,并報出Fatal日志。代碼實現如下:
~~~
// 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!")
}
~~~
#### 4.4.3\. 創建容器倉庫目錄
Docker Daemon在創建Docker容器之后,需要將容器放置于某個倉庫目錄下,統一管理。而這個目錄即為daemonRepo,值為:/var/lib/docker/containers,并通過daemonRepo創建相應的目錄。代碼實現如下:
~~~
daemonRepo := path.Join(config.Root, "containers")
if err := os.MkdirAll(daemonRepo, 0700); err != nil && !os.IsExist(err) {
return nil, err
}
~~~
#### 4.4.4\. 遷移容器至aufs類型
當graphdriver的類型為aufs時,需要將現有graph的所有內容都遷移至aufs類型;若不為aufs,則繼續往下執行。實現代碼如下:
~~~
if err = migrateIfAufs(driver, config.Root); err != nil {
return nil, err
}
~~~
這部分的遷移內容主要包括Repositories,Images以及Containers,具體實現位于[./docker/daemon/graphdriver/aufs/migrate.go](https://github.com/docker/docker/blob/v1.2.0/daemon/graphdriver/aufs/migrate.go#L39-L50)。
~~~
func (a *Driver) Migrate(pth string, setupInit func(p string) error) error {
if pathExists(path.Join(pth, "graph")) {
if err := a.migrateRepositories(pth); err != nil {
return err
}
if err := a.migrateImages(path.Join(pth, "graph")); err != nil {
return err
}
return a.migrateContainers(path.Join(pth, "containers"), setupInit)
}
return nil
}
~~~
migrate repositories的功能是:在Docker Daemon的root工作目錄下創建repositories-aufs的文件,存儲所有與images相關的基本信息。
migrate images的主要功能是:將原有的image鏡像都遷移至aufs driver能識別并使用的類型,包括aufs所規定的layers,diff與mnt目錄內容。
migrate container的主要功能是:將container內部的環境使用aufs driver來進行配置,包括,創建container內部的初始層(init layer),以及創建原先container內部的其他layers。
#### 4.4.5\. 創建鏡像graph
創建鏡像graph的主要工作是:在文件系統中指定的root目錄下,實例化一個全新的graph對象,作用為:存儲所有標記的文件系統鏡像,并記錄鏡像之間的關系。實現代碼如下:
~~~
g, err := graph.NewGraph(path.Join(config.Root, "graph"), driver)
~~~
NewGraph的具體實現位于[./docker/graph/graph.go](https://github.com/docker/docker/blob/v1.2.0/graph/graph.go#L34-L53),實現過程中返回的對象為Graph類型,定義如下:
~~~
type Graph struct {
Root string
idIndex *truncindex.TruncIndex
driver graphdriver.Driver
}
~~~
其中Root表示graph的工作根目錄,一般為”/var/lib/docker/graph”;idIndex使得檢索字符串標識符時,允許使用任意一個該字符串唯一的前綴,在這里idIndex用于通過簡短有效的字符串前綴檢索鏡像與容器的ID;最后driver表示具體的graphdriver類型。
#### 4.4.6\. 創建volumesdriver以及volumes graph
在Docker中volume的概念是:可以從Docker宿主機上掛載到Docker容器內部的特定目錄。一個volume可以被多個Docker容器掛載,從而Docker容器可以實現互相共享數據等。在實現volumes時,Docker需要使用driver來管理它,又由于volumes的管理不會像容器文件系統管理那么復雜,故Docker采用vfs驅動實現volumes的管理。代碼實現如下:
~~~
volumesDriver, err := graphdriver.GetDriver("vfs", config.Root, config.GraphOptions)
volumes, err := graph.NewGraph(path.Join(config.Root, "volumes"), volumesDriver)
~~~
主要完成工作為:使用vfs創建volumesDriver;創建相應的volumes目錄,并返回volumes graph對象。
#### 4.4.7\. 創建TagStore
TagStore主要是用于存儲鏡像的倉庫列表(repository list)。代碼如下:
~~~
repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+driver.String()), g)
~~~
NewTagStore位于[./docker/graph/tags.go](https://github.com/docker/docker/blob/v1.2.0/graph/tags.go#L33-L54),TagStore的定義如下:
~~~
type TagStore struct {
path string
graph *Graph
Repositories map[string]Repository
sync.Mutex
pullingPool map[string]chan struct{}
pushingPool map[string]chan struct{}
}
~~~
需要闡述的是TagStore類型中的多個屬性的含義:
* path:TagStore中記錄鏡像倉庫的文件所在路徑;
* graph:相應的Graph實例對象;
* Repositories:記錄具體的鏡像倉庫的map數據結構;
* sync.Mutex:TagStore的互斥鎖
* pullingPool :記錄池,記錄有哪些鏡像正在被下載,若某一個鏡像正在被下載,則駁回其他Docker Client發起下載該鏡像的請求;
* pushingPool:記錄池,記錄有哪些鏡像正在被上傳,若某一個鏡像正在被上傳,則駁回其他Docker Client發起上傳該鏡像的請求;
### 4.5\. 創建Docker Daemon網絡環境
創建Docker Daemon運行環境的時候,創建網絡環境是極為重要的一個部分,這不僅關系著容器對外的通信,同樣也關系著容器間的通信。
在創建網絡時,Docker Daemon是通過運行名為”init_networkdriver”的job來完成的。代碼如下:
~~~
if !config.DisableNetwork {
job := eng.Job("init_networkdriver")
job.SetenvBool("EnableIptables", config.EnableIptables)
job.SetenvBool("InterContainerCommunication", config.InterContainerCommunication)
job.SetenvBool("EnableIpForward", config.EnableIpForward)
job.Setenv("BridgeIface", config.BridgeIface)
job.Setenv("BridgeIP", config.BridgeIP)
job.Setenv("DefaultBindingIP", config.DefaultIp.String())
if err := job.Run(); err != nil {
return nil, err
}
}
~~~
分析以上源碼可知,通過config中的DisableNetwork屬性來判斷,在[默認配置文件](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L36)中,該屬性有過定義,卻沒有初始值。但是在應用配置信息中處理網絡功能配置的時候,將DisableNetwork屬性賦值為false,故判斷語句結果為真,執行相應的代碼塊。
首先創建名為”init_networkdriver”的job,隨后為該job設置環境變量,環境變量的值如下:
* 環境變量EnableIptables,使用config.EnableIptables來賦值,為true;
* 環境變量InterContainerCommunication,使用config.InterContainerCommunication來賦值,為true;
* 環境變量EnableIpForward,使用config.EnableIpForward來賦值,值為true;
* 環境變量BridgeIface,使用config.BridgeIface來賦值,為空字符串””;
* 環境變量BridgeIP,使用config.BridgeIP來賦值,為空字符串””;
* 環境變量DefaultBindingIP,使用config.DefaultIp.String()來賦值,為”0.0.0.0”。
設置完環境變量之后,隨即運行該job,由于在eng中key為”init_networkdriver”的handler,value為bridge.InitDriver函數,故執行bridge.InitDriver函數,具體的實現位于[./docker/daemon/networkdriver/bridge/dirver.go](https://github.com/docker/docker/blob/v1.2.0/daemon/networkdriver/bridge/driver.go#L79),作用為:
* 獲取為Docker服務的網絡設備的地址;
* 創建指定IP地址的網橋;
* 啟用Iptables功能并配置;
* 另外還為eng實例注冊了4個Handler,如 ”allocate_interface”, ”release_interface”, ”allocate_port”,”link”。
#### 4.5.1\. 創建Docker網絡設備
創建Docker網絡設備,屬于Docker Daemon創建網絡環境的第一步,實際工作是創建名為“docker0”的網橋設備。
在InitDriver函數運行過程中,首先使用job的環境變量初始化內部變量;然后根據目前網絡環境,判斷是否創建docker0網橋,若Docker專屬網橋已存在,則繼續往下執行;否則的話,創建docker0網橋。具體實現為[createBridge(bridgeIP)](https://github.com/docker/docker/blob/v1.2.0/daemon/networkdriver/bridge/driver.go#L108),以及[createBridgeIface(bridgeIface)](https://github.com/docker/docker/blob/v1.2.0/daemon/networkdriver/bridge/driver.go#L285)。
createBridge的功能是:在host主機上啟動創建指定名稱網橋設備的任務,并為該網橋設備配置一個與其他設備不沖突的網絡地址。而createBridgeIface通過系統調用負責創建具體實際的網橋設備,并設置MAC地址,通過libcontainer中netlink包的CreateBridge來實現。
#### 4.5.2\. 啟用iptables功能
創建完網橋之后,Docker Daemon為容器以及host主機配置iptables,包括為container之間所需要的link操作提供支持,為host主機上所有的對外對內流量制定傳輸規則等。代碼位于[./docker/daemon/networkdriver/bridge/driver/driver.go#L133-L137](https://github.com/docker/docker/blob/v1.2.0/daemon/networkdriver/bridge/driver.go#L133-L137),如下:
~~~
// Configure iptables for link support
if enableIPTables {
if err := setupIPTables(addr, icc); err != nil {
return job.Error(err)
}
}
~~~
其中setupIPtables的調用過程中,addr地址為Docker網橋的網絡地址,icc為true,即為允許Docker容器間互相訪問。假設網橋設備名為docker0,網橋網絡地址為docker0_ip,設置iptables規則,操作步驟如下:
(1) 使用iptables工具開啟新建網橋的NAT功能,使用命令如下:
~~~
iptables -I POSTROUTING -t nat -s docker0_ip ! -o docker0 -j MASQUERADE
~~~
(2) 通過icc參數,決定是否允許container間通信,并制定相應iptables的Forward鏈。Container之間通信,說明數據包從container內發出后,經過docker0,并且還需要在docker0處發往docker0,最終轉向指定的container。換言之,從docker0出來的數據包,如果需要繼續發往docker0,則說明是container的通信數據包。命令使用如下:
~~~
iptables -I FORWARD -i docker0 -o docker0 -j ACCEPT
~~~
(3) 允許接受從container發出,且不是發往其他container數據包。換言之,允許所有從docker0發出且不是繼續發向docker0的數據包,使用命令如下:
~~~
iptables -I FORWARD -i docker0 ! -o docker0 -j ACCEPT
~~~
(4) 對于發往docker0,并且屬于已經建立的連接的數據包,Docker無條件接受這些數據包,使用命令如下:
~~~
iptables -I FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
~~~
#### 4.5.3\. 啟用系統數據包轉發功能
在Linux系統上,數據包轉發功能是被默認禁止的。數據包轉發,就是當host主機存在多塊網卡的時,如果其中一塊網卡接收到數據包,并需要將其轉發給另外的網卡。通過修改/proc/sys/net/ipv4/ip_forward的值,將其置為1,則可以保證系統內數據包可以實現轉發功能,代碼如下:
~~~
if ipForward {
// Enable IPv4 forwarding
if err := ioutil.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte{'1', '\n'}, 0644); err != nil {
job.Logf("WARNING: unable to enable IPv4 forwarding: %s\n", err)
}
}
~~~
#### 4.5.4\. 創建DOCKER鏈
在網橋設備上創建一條名為DOCKER的鏈,該鏈的作用是在創建Docker container并設置端口映射時使用。實現代碼位于[./docker/daemon/networkdriver/bridge/driver/driver.go](https://github.com/docker/docker/blob/v1.2.0/daemon/networkdriver/bridge/driver.go#L147-L157),如下:
~~~
if err := iptables.RemoveExistingChain("DOCKER"); err != nil {
return job.Error(err)
}
if enableIPTables {
chain, err := iptables.NewChain("DOCKER", bridgeIface)
if err != nil {
return job.Error(err)
}
portmapper.SetIptablesChain(chain)
}
~~~
#### 4.5.5\. 注冊Handler至Engine
在創建完網橋,并配置完基本的iptables規則之后,Docker Daemon在網絡方面還在Engine中注冊了4個Handler,這些Handler的名稱與作用如下:
* allocate_interface:為Docker container分配一個專屬網卡;
* realease_interface:釋放網絡設備資源;
* allocate_port:為Docker container分配一個端口;
* link:實現Docker container間的link操作。
由于在Docker架構中,網絡是極其重要的一部分,因此Docker網絡篇會安排在《Docker源碼分析》系列的第六篇。
### 4.6\. 創建graphdb并初始化
Graphdb是一個構建在SQLite之上的圖形數據庫,通常用來記錄節點命名以及節點之間的關聯。Docker Daemon使用graphdb來記錄鏡像之間的關聯。創建graphdb的代碼如下:
~~~
graphdbPath := path.Join(config.Root, "linkgraph.db")
graph, err := graphdb.NewSqliteConn(graphdbPath)
if err != nil {
return nil, err
}
~~~
以上代碼首先確定graphdb的目錄為/var/lib/docker/linkgraph.db;隨后通過graphdb包內的NewSqliteConn打開graphdb,使用的驅動為”sqlite3”,數據源的名稱為” /var/lib/docker/linkgraph.db”;最后通過NewDatabase函數初始化整個graphdb,為graphdb創建entity表,edge表,并在這兩個表中初始化部分數據。NewSqliteConn函數的實現位于[./docker/pkg/graphdb/conn_sqlite3.go](https://github.com/docker/docker/blob/v1.2.0/pkg/graphdb/conn_sqlite3.go),代碼實現如下:
~~~
func NewSqliteConn(root string) (*Database, error) {
……
conn, err := sql.Open("sqlite3", root)
……
return NewDatabase(conn, initDatabase)
}
~~~
### 4.7\. 創建execdriver
Execdriver是Docker中用來執行Docker container任務的驅動。創建并初始化graphdb之后,Docker Daemon隨即創建了execdriver,具體代碼如下:
~~~
ed, err := execdrivers.NewDriver(config.ExecDriver, config.Root, sysInitPath, sysInfo)
~~~
可見,在創建execdriver的時候,需要4部分的信息,以下簡要介紹這4部分信息:
* config.ExecDriver:Docker運行時中指定使用的exec驅動類別,在[默認配置文件](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L55)中默認使用”native”,也可以將這個值改為”lxc”,則使用lxc接口執行Docker container內部的操作;
* config.Root:Docker運行時的root路徑,[默認配置文件](https://github.com/docker/docker/blob/v1.2.0/daemon/config.go#L47)中為”/var/lib/docker”;
* sysInitPath:系統上存放dockerinit二進制文件的路徑,一般為”/var/lib/docker/init/dockerinit-1.2.0”;
* sysInfo:系統功能信息,包括:容器的內存限制功能,交換區內存限制功能,數據轉發功能,以及AppArmor安全功能等。
在執行execdrivers.NewDriver之前,首先通過以下代碼,獲取期望的目標dockerinit文件的路徑localPath,以及系統中dockerinit文件實際所在的路徑sysInitPath:
~~~
localCopy := path.Join(config.Root, "init", fmt.Sprintf("
dockerinit-%s", dockerversion.VERSION))
sysInitPath := utils.DockerInitPath(localCopy)
~~~
通過執行以上代碼,localCopy為”/var/lib/docker/init/dockerinit-1.2.0”,而sysyInitPath為當前Docker運行時中dockerinit-1.2.0實際所處的路徑,utils.DockerInitPath的實現位于 [./docker/utils/util.go](https://github.com/docker/docker/blob/v1.2.0/utils/utils.go#L120-L158)。若localCopy與sysyInitPath不相等,則說明當前系統中的dockerinit二進制文件,不在localCopy路徑下,需要將其拷貝至localCopy下,并對該文件設定權限。
設定完dockerinit二進制文件的位置之后,Docker Daemon創建sysinfo對象,記錄系統的功能屬性。SysInfo的定義,位于[./docker/pkg/sysinfo/sysinfo.go](https://github.com/docker/docker/blob/v1.2.0/pkg/sysinfo/sysinfo.go#L12-L17),如下:
~~~
type SysInfo struct {
MemoryLimit bool
SwapLimit bool
IPv4ForwardingDisabled bool
AppArmor bool
}
~~~
其中MemoryLimit通過判斷cgroups文件系統掛載路徑下是否均存在memory.limit_in_bytes和memory.soft_limit_in_bytes文件來賦值,若均存在,則置為true,否則置為false。SwapLimit通過判斷memory.memsw.limit_in_bytes文件來賦值,若該文件存在,則置為true,否則置為false。AppArmor通過host主機是否存在/sys/kernel/security/apparmor來判斷,若存在,則置為true,否則置為false。
執行execdrivers.NewDriver時,返回execdriver.Driver對象實例,具體代碼實現位于 [./docker/daemon/execdriver/execdrivers/execdrivers.go](https://github.com/docker/docker/blob/v1.2.0/daemon/execdriver/execdrivers/execdrivers.go),由于選擇使用native作為exec驅動,故執行以下的代碼,返回最終的execdriver,其中native.NewDriver實現位于[./docker/daemon/execdriver/native/driver.go](https://github.com/docker/docker/blob/v1.2.0/daemon/execdriver/native/driver.go#L45-L60):
return native.NewDriver(path.Join(root, "execdriver", "native"), initPath)
至此,已經創建完畢一個execdriver的實例ed。
### 4.8\. 創建daemon實例
Docker Daemon在經過以上諸多設置以及創建對象之后,整合眾多內容,創建最終的Daemon對象實例daemon,實現代碼如下:
~~~
daemon := &Daemon{
repository: daemonRepo,
containers: &contStore{s: make(map[string]*Container)},
graph: g,
repositories: repositories,
idIndex: truncindex.NewTruncIndex([]string{}),
sysInfo: sysInfo,
volumes: volumes,
config: config,
containerGraph: graph,
driver: driver,
sysInitPath: sysInitPath,
execDriver: ed,
eng: eng,
}
~~~
以下分析Daemon類型的屬性:
| 屬性名| 作用 |
|---|---|
| repository | 部署所有Docker容器的路徑 |
| containers | 用于存儲具體Docker容器信息的對象 |
| graph | 存儲Docker鏡像的graph對象 |
| repositories | 存儲Docker鏡像元數據的文件 |
| idIndex | 用于通過簡短有效的字符串前綴定位唯一的鏡像 |
| sysInfo | 系統功能信息 |
| volumes | 管理host主機上volumes內容的graphdriver,默認為vfs類型 |
| config | Config.go文件中的配置信息,以及執行產生的配置DisableNetwork |
| containerGraph | 存放Docker鏡像關系的graphdb |
| driver | 管理Docker鏡像的驅動graphdriver,默認為aufs類型 |
| sysInitPath | 系統dockerinit二進制文件所在的路徑 |
| execDriver | Docker Daemon的exec驅動,默認為native類型 |
| eng | Docker的執行引擎Engine類型實例 |
### 4.9\. 檢測DNS配置
創建完Daemon類型實例daemon之后,Docker Daemon使用daemon.checkLocaldns()檢測Docker運行環境中DNS的配置, checkLocaldns函數的定義位于[./docker/daemon/daemon.go](https://github.com/docker/docker/blob/v1.2.0/daemon/daemon.go#L1053-L1063),代碼如下:
~~~
func (daemon *Daemon) checkLocaldns() error {
resolvConf, err := resolvconf.Get()
if err != nil {
return err
}
if len(daemon.config.Dns) == 0 && utils.CheckLocalDns(resolvConf) {
log.Infof("Local (127.0.0.1) DNS resolver found in resolv.conf and
containers can't use it. Using default external servers : %v", DefaultDns)
daemon.config.Dns = DefaultDns
}
return nil
}
~~~
以上代碼首先通過resolvconf.Get()方法獲取/etc/resolv.conf中的DNS服務器信息。若本地DNS 文件中有127.0.0.1,而Docker container不能使用該地址,故采用默認外在DNS服務器,為8.8.8.8,8.8.4.4,并將其賦值給config文件中的Dns屬性。
### 4.10\. 啟動時加載已有Docker containers
當Docker Daemon啟動時,會去查看在daemon.repository,也就是在/var/lib/docker/containers中的內容。若有存在Docker container的話,則讓Docker Daemon加載這部分容器,將容器信息收集,并做相應的維護。
### 4.11\. 設置shutdown的處理方法
加載完已有Docker container之后,Docker Daemon設置了多項在shutdown操作中需要執行的handler。也就是說:當Docker Daemon接收到特定信號,需要執行shutdown操作時,先執行這些handler完成善后工作,最終再實現shutdown。實現代碼如下:
~~~
eng.OnShutdown(func() {
if err := daemon.shutdown(); err != nil {
log.Errorf("daemon.shutdown(): %s", err)
}
if err := portallocator.ReleaseAll(); err != nil {
log.Errorf("portallocator.ReleaseAll(): %s", err)
}
if err := daemon.driver.Cleanup(); err != nil {
log.Errorf("daemon.driver.Cleanup(): %s", err.Error())
}
if err := daemon.containerGraph.Close(); err != nil {
log.Errorf("daemon.containerGraph.Close(): %s", err.Error())
}
})
~~~
可知,eng對象shutdown操作執行時,需要執行以上作為參數的func(){……}函數。該函數中,主要完成4部分的操作:
* 運行daemon對象的shutdown函數,做daemon方面的善后工作;
* 通過portallocator.ReleaseAll(),釋放所有之前占用的端口資源;
* 通過daemon.driver.Cleanup(),通過graphdriver實現unmount所有layers中的掛載點;
* 通過daemon.containerGraph.Close()關閉graphdb的連接。
### 4.12\. 返回daemon對象實例
當所有的工作完成之后,Docker Daemon返回daemon實例,并最終返回至mainDaemon()中的加載daemon的goroutine中繼續執行。
## 5\. 總結
本文從源碼的角度深度分析了Docker Daemon啟動過程中daemon對象的創建與加載。在這一環節中涉及內容極多,本文歸納總結daemon實現的邏輯,一一深入,具體全面。
在Docker的架構中,Docker Daemon的內容是最為豐富以及全面的,而NewDaemon的實現而是涵蓋了Docker Daemon啟動過程中的絕大部分。可以認為NewDaemon是Docker Daemon實現過程中的精華所在。深入理解NewDaemon的實現,即掌握了Docker Daemon運行的來龍去脈。
## 6\. 作者簡介
孫宏亮,[DaoCloud](http://www.daocloud.io/)初創團隊成員,軟件工程師,浙江大學VLIS實驗室應屆研究生。讀研期間活躍在PaaS和Docker開源社區,對Cloud Foundry有深入研究和豐富實踐,擅長底層平臺代碼分析,對分布式平臺的架構有一定經驗,撰寫了大量有深度的技術博客。2014年末以合伙人身份加入DaoCloud團隊,致力于傳播以Docker為主的容器的技術,推動互聯網應用的容器化步伐。郵箱:[allen.sun@daocloud.io](mailto:allen.sun@daocloud.io)
## 7\. 參考文獻
[http://docs.studygolang.com/pkg/](http://docs.studygolang.com/pkg/)
[http://www.iptables.info/en/iptables-matches.html](http://www.iptables.info/en/iptables-matches.html)
[https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt](https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt)
[http://crosbymichael.com/the-lost-packages-of-docker.html](http://crosbymichael.com/the-lost-packages-of-docker.html)