Docker源碼分析(四):Docker Daemon之NewDaemon實現

1. 前言

Docker的生態系統日趨完善,開發者羣體也在日趨龐大,這讓業界對Docker持續抱有極其樂觀的態度。現在,對於廣大開發者而言,使用Docker這項技術已然不是門檻,享受Docker帶來的技術福利也再也不是困難。然而,如何探尋Docker適應的場景,如何發展Docker周邊的技術,以及如何彌合Docker新技術與傳統物理機或VM技術的鴻溝,已經佔據Docker研究者們的思考與實踐。html

本文爲《Docker源碼分析》第四篇——Docker Daemon之NewDaemon實現,力求幫助廣大Docker愛好者更多得理解Docker 的核心——Docker Daemon的實現。linux

2. NewDaemon做用簡介

在Docker架構中有不少重要的概念,如:graph,graphdriver,execdriver,networkdriver,volumes,Docker containers等。Docker在實現過程當中,須要將以上實體進行統一化管理,而Docker Daemon中的daemon實例就是設計用來完成這一任務的實體。git

從源碼的角度,NewDaemon函數的執行完成了Docker Daemon建立並加載daemon的任務,最終實現統一管理Docker Daemon的資源。github

3. NewDaemon源碼分析內容安排

本文從源碼角度,分析Docker Daemon加載過程當中NewDaemon的實現,整個分析過程以下圖:golang

圖3.1 Docker Daemon中NewDaemon執行流程圖sql

由上圖可見,Docker Daemon中NewDaemon的執行流程主要包含12個獨立的步驟:處理配置信息、檢測系統支持及用戶權限、配置工做路徑、加載並配置graphdriver、建立Docker Daemon網絡環境、建立並初始化graphdb、建立execdriver、建立daemon實例、檢測DNS配置、加載已有container、設置shutdown處理方法、以及返回daemon實例。docker

下文會在NewDaemon的具體實現中,以12節分別分析以上內容。數據庫

4. NewDaemon具體實現

在《Docker源碼分析》系列第三篇中,有一個重要的環節:使用goroutine加載daemon對象並運行。在加載並運行daemon對象時,所作的第一個工做即爲:數組

d, err := daemon.NewDaemon(daemonCfg, eng)

該部分代碼分析以下:安全

  • 函數名:NewDaemon;
  • 函數調用具體實現所處的包位置:./docker/daemon
  • 函數具體實現源文件:./docker/daemon/daemon.go
  • 函數傳入實參:daemonCfg,定義了Docker Daemon運行過程當中所需的衆多配置信息;eng,在mainDaemon中建立的Engine對象實例;
  • 函數返回類型:d,具體的Daemon對象實例;err,錯誤狀態。

進入./docker/daemon/daemon.go中NewDaemon的具體實現,代碼以下:

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(下文簡稱爲默認配置文件)中,初始化時Mtu屬性值爲0,故執行GetDefaultNetworkMtu。

GetDefaultNetworkMtu函數的具體實現位於./docker/daemon/config.go:

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,這兩個屬性屬於互斥類型,只能至多指定其中之一。而在默認配置文件中,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。而在默認配置文件中,這兩個屬性的值均爲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文件配置信息的代碼實現以下:

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文件。在默認配置文件中,Pidfile文件的初始值爲」 /var/run/docker.pid」。

以上即是關於配置信息處理的分析。

4.2. 檢測系統支持及用戶權限

初步處理完Docker的配置信息以後,Docker對自身運行的環境進行了一系列的檢測,主要包括三個方面:

  • 操做系統類型對Docker Daemon的支持;
  • 用戶權限的級別;
  • 內核版本與處理器的支持。

系統支持與用戶權限檢測的實現較爲簡單,實現代碼以下:

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。實現過程當中,第一個工做是:檢測程序運行所在的處理器架構是否爲「amd64」,而目前Docker運行時只能支持amd64的處理器架構。第二個工做是:檢測Linux內核版本是否知足要求,而目前Docker Daemon運行所需的內核版本若太低,則必須升級至3.8.0。

4.3. 配置工做路徑

配置Docker Daemon的工做路徑,主要是建立Docker Daemon運行中所在的工做目錄。實現過程當中,經過config中的Root屬性來完成。在默認配置文件中,Root屬性的值爲」/var/lib/docker」。

配置工做路徑的代碼實現中,步驟以下:

(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,具體細節分析以下:

graphdriver.DefaultDriver = config.GraphDriver
driver, err := graphdriver.New(config.Root, config.GraphOptions)

首先,爲graphdriver包中的DefaultDriver對象賦值,值爲config中的GraphDriver屬性,在默認配置文件中,GraphDriver屬性的值爲空;一樣的,屬性GraphOptions也爲空。而後經過graphDriver中的new函數實現加載graph的存儲驅動。

建立具體的graphdriver是至關重要的一個環節,實現細節由graphdriver包中的New函數來完成。進入./docker/daemon/graphdriver/driver.go中,實現步驟以下:

第一,遍歷數組選擇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,以及其餘三類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

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,實現過程當中返回的對象爲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,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屬性來判斷,在默認配置文件中,該屬性有過定義,卻沒有初始值。可是在應用配置信息中處理網絡功能配置的時候,將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,做用爲:

  • 獲取爲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),以及createBridgeIface(bridgeIface)

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,以下:

// 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,以下:

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,代碼實現以下:

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驅動類別,在默認配置文件中默認使用」native」,也能夠將這個值改成」lxc」,則使用lxc接口執行Docker container內部的操做;
  • config.Root:Docker運行時的root路徑,默認配置文件中爲」/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。若localCopy與sysyInitPath不相等,則說明當前系統中的dockerinit二進制文件,不在localCopy路徑下,須要將其拷貝至localCopy下,並對該文件設定權限。

設定完dockerinit二進制文件的位置以後,Docker Daemon建立sysinfo對象,記錄系統的功能屬性。SysInfo的定義,位於./docker/pkg/sysinfo/sysinfo.go,以下:

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,因爲選擇使用native做爲exec驅動,故執行如下的代碼,返回最終的execdriver,其中native.NewDriver實現位於./docker/daemon/execdriver/native/driver.go

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,代碼以下:

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. 參考文獻

http://docs.studygolang.com/pkg/

http://www.iptables.info/en/iptables-matches.html

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

http://crosbymichael.com/the-lost-packages-of-docker.html

相關文章
相關標籤/搜索