【郵箱:summer_mushroom@163.com】
linux
【原創,轉載請註明出處】git
要了解Libcontainer首先要了解linux container所用到的一些基本技術。linux container是一種內核虛擬化技術,能夠提供輕量級的虛擬化,以便隔離進程和資源。而這正是docker容器技術和核心,docker正是linux container的一種實現。linux container所用到的基本技術包括namespace、cgroup、chroot、veth、union FS、iptables和netfilter、TC、quota、setrlimit,下面對這些基本技術作一個簡要的歸納:github
1. Namespace:用來作資源隔離以實現輕量級虛擬化,包括六種namespace,UTS namespace提供了主機名和域名之間的隔離;IPC namespace提供了進程間通訊的隔離;Network namespace提供了網絡的隔離包括網絡設備、網絡棧、端口等;Mount namespace提供了文件系統的隔離;User namespace提供了用戶權限間的隔離。docker
2. Cgroups:實現資源限制,能夠限制、記錄任務組所使用的物理資源。還可用於優先級分配,經過分配的CPU時間片數量及磁盤IO寬帶大小控制任務運行的優先級。用於資源統計,統計系統的資源使用量,如CPU使用時長、內存用量等。用於任務控制,能夠對任務執行掛起、控制等操做。json
3. Chroot:更改root目錄,用於在container裏查看到的文件系統。他有三大優勢:增長系統的安全性,限制了用戶的權利;創建一個與原系統隔離的系統目錄,這一點對容器極爲重要;切換系統的根目錄位置。 安全
4. Veth:把一個從網絡用戶空間(network namespace )發出的數據包轉發到另外一個用戶空間。即實現容器和宿主機之間的通訊。網絡
5. Union FS:疊加的文件系統,其中包括aufs一種支持聯合掛載的文件系統。函數
6. Iptables,netfilter:主要用來作ip數據包的過濾。源碼分析
7. TC:主要用來作流量隔離,帶寬的限制。測試
8. Quota:用來作磁盤讀寫大小的限制,用來限制用戶可用空間的大小。
9. Setrlimit:能夠限制container中打開的進程數,限制打開的文件個數等。
基於上文對linux container相關技術,docker基本是實現了前五個的技術,用libcontainer作了一層封裝。也就是說docker經過libcontainer封裝了linux container的部分技術,這樣使得Docker具備持續部署與測試、跨雲平臺支持、環境標準化和版本控制、高資源利用率與隔離、容器跨平臺性與鏡像、易於理解且易用以及具備應用鏡像倉庫等優勢。Libcontainer本質上是Docker中用於容器管理的包,基於Go語言實現,經過管理namespace、Cgroups、capabilities以及文件系統等來進行容器控制。Libcontainer可用於建立容器並對容器進行生命週期管理。提到Libcontainer就要提到execdriver,execdriver封裝了對namespace、cgroups等對OS資源進行操做的全部方法,而Libcontainer是execdriver的默認實現。execdriver經過獲得的command信息加載生成容器配置container,而後調用libcontainer加載容器配置container,建立真正的docker容器,完成容器的建立並對容器的生命週期進行管理。
Execdriver的工做流程如圖2.1所示:
圖2.1 execdriver的工做流程
Execdriver首先獲得Docker daemon提交的command信息,提交過來的command信息包含namespace、cgroup等配置容器所需的重要信息。相對應的command結構體源碼如圖2.2,其中包含了生成容器所需的基本配置,有namespace相關好比UTS可提主機名和域名之間的隔離;IPC提供了進程間通訊的隔離;Network提供了網絡的隔離包括網絡設備、網絡棧、端口等;Mount提供了文件系統的隔離。Resource包含了cgroup相關的信息,ProcessConfig表示容器中運行的進程的信息。
對圖2.2中的部分參數作簡要解釋,其中:ContainerPid表示容器中進程的pid;ID是容器ID,表明容器的惟一標識,很是重要;Mount是namespace的一種用於文件系統的隔離;Network也是namespace的一種用於進行網絡的隔離;ProcessConfig描述了容器中運行的進程的信息;Resource提供了cgroup相關的信息,後面會對Resource結構體展開作詳細的分析;Rootfs是容器的根目錄系統;WorkingDir顧名思義是容器的工做路徑;TmpDir是用來存儲docker臨時文件的目錄;
圖2.2 command結構體
Cgroups用於實現資源限制,能夠限制、記錄任務組所使用的物理資源。cgroups相關信息包含在resource裏,resource包含了對driver配置的全部資源的信息,resource結構體相關定義如圖2.3,其中:memory表示所使用的存儲容量,還定義了CPU用量等cgroup所需的信息。
圖2.3 resource結構體
ProcessConfig中包含了表示容器中運行的進程的信息,ProcessConfig結構體相關定義源碼如圖2.4,
圖2.4ProcessConfig結構體
圖2.1中所示的工做流程相對應的源碼在deamon/execdriver/native/driver.go的run函數中,run函數部分截圖如圖2.5所示,其中container, err := d.createContainer(c, hooks)語句的做用是調用createContainer函數建立容器配置。函數傳入的參數c表示execdriver.Command,即上文提到的command結構體,也就是說createContainer函數根據command參數建立相關的容器配置。
圖2.5 Run函數部分函數體
上文說到createContainer函數根據command參數建立相關的容器配置,下面咱們看一下createContainer函數的內部結構,如圖2.6爲createContainer函數的部分結構。其中container = execdriver.InitContainer(c)能夠看到調用InitContainer函數經過傳入的execdriver.Command參數生成容器配置container。其中一系列的createXXX()方法根據InitContainer函數獲得的container填充模板,配置IPC、Pid、network等所需字段。其中createIpc()表示配置Ipc提供提供了進程間通訊的隔離;createPid()表示配置Pid;createUTS()表示配置UTS提供主機名和域名之間的隔離;createNetwork()配置Network提供了網絡的隔離包括網絡設備、網絡棧、端口等。
圖2.6 createContainer函數的部分函數體
由createContainer函數的源碼的內部結構能夠看到在createContainer函數中首先調用InitContainer函數生成了一個叫作container的變量,InitContainer函數經過傳入的execdriver.Command參數生成容器配置container,如圖2.7是execdriver.InitContainer函數的內部結構。在InitContainer函數中根據command配置container的hostname主機名、cgroup、devices、rootfs等信息,最後返回一個容器配置container,這時候的返回的container實際上是一個Config對象,表示容器配置。後面再由createContainer函數中的createXXX()方法根據InitContainer函數返回的container容器配置,配置相應IPC、Pid、network等所需字段。
圖2.7 InitContainer函數的部分函數體
至此咱們已經分析完了deamon/execdriver/native/driver.go的run函數中container, err := d.createContainer(c, hooks)語句,簡單的說該語句的結果就是生成了一份container容器配置。接下來在run函數中execdriver調用libcontainer加載已經生成好的容器配置container,建立真正的Docker容器。
在deamon/execdriver/native/driver.go的run函數中,成功生成container容器配置之後,工做就交由libcontainer。libcontainer的主要工做爲:
1. 建立libcontainer構建容器所須要使用的進程對象,即Process。對應源碼如圖3.1所示。Process指定了容器內進程對象的配置和IO,其中有指定若干參數,並對參數賦值。Args表示將要運行的一系列指令;Env指定該進程對象的環境變量;Cwd將進程的工做目錄改至容器的rootfs中;User將爲容器中的正在運行的進程設置UID和GID。
圖3.1 構建Process
2.接下來在run函數中err := setupPipes(container, &c.ProcessConfig, p, pipes);語句調用setupPipes函數設置容器的輸出管道。而setupPipes函數即爲設置容器輸出管道函數,其函數體定義在deamon/execdriver/native/driver.go的setupPipes函數中。setupPipes函數主要經過execdriver.Pipes配置容器的輸出管道,其主要做用是將容器的輸出成標準輸入、標準輸出和標準錯誤。
3. 使用Factory工廠類,用容器ID和容器配置container建立邏輯容器Container,在run函數中對應的源碼爲:d.factory.Create(c.ID, container),其中c爲execdriver.Command,c.ID爲容器ID,container即爲以前屢次提到的容器配置。在生成邏輯容器的過程當中,容器配置container的各項會填充到邏輯容器Container對像的配置項config裏。
4.接下來用啓動容器,啓動容器對應的語句爲cont.Start(p),其中cont爲d.factory.Create(c.ID, container)函數生成的Container邏輯容器,而參數p爲以前生成的容器所須要使用的進程對象Process。
5. 下面的代碼p.Wait()即爲process.Wait(),表示等待以前Process的全部工做都完成,直到物理容器建立成功。Processd的Wait函數所對應的源碼爲圖3.2所示。
圖3.2 Process的Wait()函數
6. 最後的cont.Destroy()表示Container.Destory(),即在須要的狀況下能夠銷燬容器。
經過上述對libcontainer主要工做分析,咱們發現libcontainer的重點正是Process、Container、Factory這3個邏輯實體的實現。其中Factory用於建立一個邏輯上的容器對象;Container是包含容器配置信息的邏輯容器;Process用於物理容器中進程的配置和IO管理。下面咱們libcontainer中這三個邏輯實體進行詳細的解析。
Factory的做用是用給定的容器ID建立一個新的容器,並在該容器中啓動初始進程。而且接受的容器ID爲只包含字母、數字、下劃線組成的字符創,且長度必須在1到1024之間。容器ID不能與已經存在的容器的ID重合,使用同一路徑(和文件系統)的Factory建立的容器必須有不一樣的標識。最後用一個正在運行的進程返回一個新的容器。
在這個過程當中可能出現的錯誤有:IdInUse表示容器ID已經被其餘容器佔用;InvalidIdFormat表示容器ID的格式不正確;ConfigInvalid表示配置信息無用;Systemerror表示系統錯誤。一但發生錯誤,那麼任何已經建立的容器部分都會被清除,保證了容器建立的原子性,要不所有建立成功,不然所有不建立。
Factory對象中包含三個函數,他們分別爲:
1. Create()函數:其傳入參數爲一個容器ID和一份Config類型的配置參數,而且接受的容器ID爲只包含字母、數字、下劃線組成的字符創,且長度必須在1到1024之間。容器ID不能與已經存在的容器的ID重合,使用同一路徑(和文件系統)的Factory建立的容器必須有不一樣的標識。根據傳入的這兩個參數建立並返回一個Container類,其中包括容器ID、容器工做目錄、容器配置、初始化指令和參數、以及Cgroup管理器等信息。在這個函數中Container建立完畢。其中可能出現路徑不存在、容器已經中止、系統故障等錯誤。
2. Load()函數:傳入參數爲一個已經被成功Create過的容器的容器ID,返回該容器的信息。若是容器已經Create過說明存在id目錄,則會從id目錄下直接讀取state.json來載入容器信息。其中可能出現的錯誤有管道鏈接錯誤和系統故障。
3. StartInitialization()函數:是容器初始化函數,是Libcontainer在容器從新執行期間會調用的內部API。
4. Type()函數:返回容器管理的類型,好比lxc或libcontainer等。
至此,Factory對象完成了容器的建立和初始化。接下來就瞭解一下包含包含容器配置信息的邏輯容器Container。
Container對象至關因而邏輯容器主要包含了容器配置、控制、狀態顯示等功能。其中ID表示容器的ID。Status表示容器內進程的狀態,容器的狀態包括:Running表示容器存在而且正在運行;Pausing表示容器存在而且進程正在被中止;Paused表示容器存在可是全部的進程都被中止了;Checkpointed表示容器存在而且容器狀態都已保存至磁盤;Destoryed表示容器不存在。
Container對象中具備一系列容器相關的函數操做,其中包括:
ID():返回容器的ID,表明容器的惟一標識
Status():返回容器內進程的狀態,可能爲運行狀態也多是中止狀態。可能拋出的錯誤爲ContainerDestroyed表示容器不存在已經被銷燬;Systemerror表示系統錯誤。
State():返回容器的狀態信息,包括容器ID、配置信息、初始進程ID、進程啓動時間、cgroup文件路徑、namespace路徑等。可能出現的錯誤爲Systemerror即系統錯誤。
Config():返回容器的配置信息
Processes():返回容器的PID,這個PID即爲用來調用進程的namespace。有些PID可能不在與容器中的進程相關,除非容器的狀態是PAUSED,這樣才能保證每個PID都是有效的。
Stats():返回容器統計信息,包括cgroup中的統計以及網卡設備的統計信息。
Set():設置容器的資源配置,例如cgroup各個子系統的文件路徑等。
Start():在容器內啓動一個進程,若是進程啓動失敗就返回一個錯誤。能夠根據以往的Process結構追蹤進程的生命週期。其中主要工做有兩個:建立ParentProcess實例,執行ParentProcess.start()來啓動物理容器。ParentProcess是一個接口其具體實現爲initProcess對象,initProcess用於建立容器所需的ParentProcess,爲建立物理容器作準備。用邏輯容器Container執行initProcess.start(),真正的物理容器即Docker容器就生成了。
Destory():在結束全部的正在運行的進程之後銷燬容器。
Process分爲兩類,一類是Process另一類是ParentProcess。Process用於容器內進程的配置和IO的管理,其參數包括:Args表示將要運行的一系列指令;Env指定該進程對象的環境變量;Cwd將進程的工做目錄改至容器的rootfs中;User將爲容器中的正在運行的進程設置UID和GID;Stdin io.Reader表示標準輸入;Stdout io.Writer表示標準輸出;Stderr io.Writer表示標準錯誤;consolePath表示到容器的控制檯的路徑;Capabilities表示容器中進程運行所需的權限;ops表示ParentProcess對象。ParentProcess負責處理容器啓動工做,包含一系列的函數動做:
pid():返回一個正在運行的進程的pid,能夠經過管道從已啓動的容器進程中得到。
start():開始容器中的執行進程。
terminate():發送SIGKILL信號結束進程。
StartTime():獲取進程啓動時間。
signal():發送信號給進程。
wait():等待程序執行結束,返回結束的程序狀態。
本文主要是對libcontainer的原理進行分析和探究。首先在第一小節介紹了linux container所用到的一些技術,而這些技術中Docker實現了其中的前五種即:Namespace用來作資源隔離以實現輕量級虛擬化; Cgroups實現資源限制、優先級分配、資源統計、任務控制;Chroot更改root目錄,用於在container裏查看到的文件系統;Veth實現容器和宿主機之間的通訊;Union FS實現實現疊加的文件系統。根據docker所實現的這五種linux container的技術介紹了libcontainer的本質做用, libcontainer實際上是Docker中用於容器管理的包,對以上這五種技術作了一層封裝,以此實現對容器的控制管理。
在第二章節中對execdriver作了分析介紹,其中包括配置信息的介紹和execdriver工做流程的介紹。配置信息主要介紹了command結構體、namespace相關字段、resource結構體和ProcessConfig結構體。Execdriver的工做流程主要包括:execdriver獲得Docker daemoe提供的command信息、根據command信息獲得容器配置container、調用libcontainer加載容器配置container建立真正的docker容器。後面的章節主要詳細介紹了execdriver調用libcontainer的詳細步驟,主要爲:使用Factory建立邏輯容器Container、啓動邏輯容器Container、用邏輯容器建立物理容器。而後還詳細分析了Factory、Container及Process對象,分析了這些對象的主要參數及主要方法函數等。
源碼分析參考了浙江大學SEL實驗室的《Docker容器與容器雲》這本書,代碼來自github.com/docker/docker/以及github.com/opencontainers/runc/libcontainer/。