轉載:Docker源碼分析(一):Docker架構
原文地址: http://www.infoq.com/cn/articles/docker-source-code-analysis-part1 作者:孫宏亮
1 背景
1.1 Docker簡介
Docker是Docker公司開源的一個基於輕量級虛擬化技術的容器引擎項目,整個項目基於Go語言開發,並遵從Apache 2.0協議。目前,Docker可以在容器內部快速自動化部署應用,並可以通過內核虛擬化技術(namespaces及cgroups等)來提供容器的資源隔離與安全保障等。由於Docker通過操作系統層的虛擬化實現隔離,所以Docker容器在運行時,不需要類似虛擬機(VM)額外的操作系統開銷,提高資源利用率,並且提升諸如IO等方面的性能。
由於眾多新穎的特性以及項目本身的開放性,Docker在不到兩年的時間裏迅速獲得諸多廠商的青睞,其中更是包括Google、Microsoft、VMware等業界行業領導者。Google在今年六月份推出了Kubernetes,提供Docker容器的調度服務,而今年8月Microsoft宣布Azure上支持Kubernetes,隨後傳統虛擬化巨頭VMware宣布與Docker強強合作。今年9月中旬,Docker更是獲得4000萬美元的C輪融資,以推動分布式應用方面的發展。
從目前的形勢來看,Docker的前景一片大好。本系列文章從源碼的角度出發,詳細介紹Docker的架構、Docker的運行以及Docker的卓越特性。本文是Docker源碼分析系列的第一篇---——Docker架構篇。
1.2 Docker版本信息
本文關於Docker架構的分析都是基於Docker的源碼與Docker相應版本的運行結果,其中Docker為最新的1.2版本。
2 Docker架構分析內容安排
本文的目的是:在理解Docker源代碼的基礎上,分析Docker架構。分析過程中主要按照以下三個步驟進行:
- Docker的總架構圖展示
- Docker架構圖內部各模塊功能與實現分析
- 以Docker命令的執行為例,進行Docker運行流程闡述
3 Docker總架構圖
學習Docker的源碼並不是一個枯燥的過程,反而可以從中理解Docker架構的設計原理。Docker對使用者來講是一個C/S模式的架構,而Docker的後端是一個非常松耦合的架構,模塊各司其職,並有機組合,支撐Docker的運行。
在此,先附上Docker總架構,如圖3.1。
圖3.1 Docker總架構圖
如圖3.1,不難看出,用戶是使用Docker Client與Docker Daemon建立通信,並發送請求給後者。
而Docker Daemon作為Docker架構中的主體部分,首先提供Server的功能使其可以接受Docker Client的請求;而後Engine執行Docker內部的一系列工作,每一項工作都是以一個Job的形式的存在。
Job的運行過程中,當需要容器鏡像時,則從Docker Registry中下載鏡像,並通過鏡像管理驅動graphdriver將下載鏡像以Graph的形式存儲;當需要為Docker創建網絡環境時,通過網絡管理驅動networkdriver創建並配置Docker容器網絡環境;當需要限制Docker容器運行資源或執行用戶指令等操作時,則通過execdriver來完成。
而libcontainer是一項獨立的容器管理包,networkdriver以及execdriver都是通過libcontainer來實現具體對容器進行的操作。
當執行完運行容器的命令後,一個實際的Docker容器就處於運行狀態,該容器擁有獨立的文件系統,獨立並且安全的運行環境等。
4 Docker架構內各模塊的功能與實現分析
接下來,我們將從Docker總架構圖入手,抽離出架構內各個模塊,並對各個模塊進行更為細化的架構分析與功能闡述。主要的模塊有:Docker Client、Docker Daemon、Docker Registry、Graph、Driver、libcontainer以及Docker container。
4.1 Docker Client
Docker Client是Docker架構中用戶用來和Docker Daemon建立通信的客戶端。用戶使用的可執行文件為docker,通過docker命令行工具可以發起眾多管理container的請求。
Docker Client可以通過以下三種方式和Docker Daemon建立通信:tcp://host:port,unix://path_to_socket和fd://socketfd。為了簡單起見,本文一律使用第一種方式作為講述兩者通信的原型。與此同時,與Docker Daemon建立連接並傳輸請求的時候,Docker Client可以通過設置命令行flag參數的形式設置安全傳輸層協議(TLS)的有關參數,保證傳輸的安全性。
Docker Client發送容器管理請求後,由Docker Daemon接受並處理請求,當Docker Client接收到返回的請求相應並簡單處理後,Docker Client一次完整的生命周期就結束了。當需要繼續發送容器管理請求時,用戶必須再次通過docker可執行文件創建Docker Client。
4.2 Docker Daemon
Docker Daemon是Docker架構中一個常駐在後臺的系統進程,功能是:接受並處理Docker Client發送的請求。該守護進程在後臺啟動了一個Server,Server負責接受Docker Client發送的請求;接受請求後,Server通過路由與分發調度,找到相應的Handler來執行請求。
Docker Daemon啟動所使用的可執行文件也為docker,與Docker Client啟動所使用的可執行文件docker相同。在docker命令執行時,通過傳入的參數來判別Docker Daemon與Docker Client。
Docker Daemon的架構,大致可以分為以下三部分:Docker Server、Engine和Job。Daemon架構如圖4.1。
圖4.1 Docker Daemon架構示意圖
4.2.1 Docker Server
Docker Server在Docker架構中是專門服務於Docker Client的server。該server的功能是:接受並調度分發Docker Client發送的請求。Docker Server的架構如圖4.2。
圖4.2 Docker Server架構示意圖
在Docker的啟動過程中,通過包gorilla/mux,創建了一個mux.Router,提供請求的路由功能。在Golang中,gorilla/mux是一個強大的URL路由器以及調度分發器。該mux.Router中添加了眾多的路由項,每一個路由項由HTTP請求方法(PUT、POST、GET或DELETE)、URL、Handler三部分組成。
若Docker Client通過HTTP的形式訪問Docker Daemon,創建完mux.Router之後,Docker將Server的監聽地址以及mux.Router作為參數,創建一個httpSrv=http.Server{},最終執行httpSrv.Serve()為請求服務。
在Server的服務過程中,Server在listener上接受Docker Client的訪問請求,並創建一個全新的goroutine來服務該請求。在goroutine中,首先讀取請求內容,然後做解析工作,接著找到相應的路由項,隨後調用相應的Handler來處理該請求,最後Handler處理完請求之後回復該請求。
需要註意的是:Docker Server的運行在Docker的啟動過程中,是靠一個名為"serveapi"的job的運行來完成的。原則上,Docker Server的運行是眾多job中的一個,但是為了強調Docker Server的重要性以及為後續job服務的重要特性,將該"serveapi"的job單獨抽離出來分析,理解為Docker Server。
4.2.2 Engine
Engine是Docker架構中的運行引擎,同時也Docker運行的核心模塊。它扮演Docker container存儲倉庫的角色,並且通過執行job的方式來操縱管理這些容器。
在Engine數據結構的設計與實現過程中,有一個handler對象。該handler對象存儲的都是關於眾多特定job的handler處理訪問。舉例說明,Engine的handler對象中有一項為:{"create": daemon.ContainerCreate,},則說明當名為"create"的job在運行時,執行的是daemon.ContainerCreate的handler。
4.2.3 Job
一個Job可以認為是Docker架構中Engine內部最基本的工作執行單元。Docker可以做的每一項工作,都可以抽象為一個job。例如:在容器內部運行一個進程,這是一個job;創建一個新的容器,這是一個job,從Internet上下載一個文檔,這是一個job;包括之前在Docker Server部分說過的,創建Server服務於HTTP的API,這也是一個job,等等。
Job的設計者,把Job設計得與Unix進程相仿。比如說:Job有一個名稱,有參數,有環境變量,有標準的輸入輸出,有錯誤處理,有返回狀態等。
4.3 Docker Registry
Docker Registry是一個存儲容器鏡像的倉庫。而容器鏡像是在容器被創建時,被加載用來初始化容器的文件架構與目錄。
在Docker的運行過程中,Docker Daemon會與Docker Registry通信,並實現搜索鏡像、下載鏡像、上傳鏡像三個功能,這三個功能對應的job名稱分別為"search","pull" 與 "push"。
其中,在Docker架構中,Docker可以使用公有的Docker Registry,即大家熟知的Docker Hub,如此一來,Docker獲取容器鏡像文件時,必須通過互聯網訪問Docker Hub;同時Docker也允許用戶構建本地私有的Docker Registry,這樣可以保證容器鏡像的獲取在內網完成。
4.4 Graph
Graph在Docker架構中扮演已下載容器鏡像的保管者,以及已下載容器鏡像之間關系的記錄者。一方面,Graph存儲著本地具有版本信息的文件系統鏡像,另一方面也通過GraphDB記錄著所有文件系統鏡像彼此之間的關系。Graph的架構如圖4.3。
圖4.3 Graph架構示意圖
其中,GraphDB是一個構建在SQLite之上的小型圖數據庫,實現了節點的命名以及節點之間關聯關系的記錄。它僅僅實現了大多數圖數據庫所擁有的一個小的子集,但是提供了簡單的接口表示節點之間的關系。
同時在Graph的本地目錄中,關於每一個的容器鏡像,具體存儲的信息有:該容器鏡像的元數據,容器鏡像的大小信息,以及該容器鏡像所代表的具體rootfs。
4.5 Driver
Driver是Docker架構中的驅動模塊。通過Driver驅動,Docker可以實現對Docker容器執行環境的定制。由於Docker運行的生命周期中,並非用戶所有的操作都是針對Docker容器的管理,另外還有關於Docker運行信息的獲取,Graph的存儲與記錄等。因此,為了將Docker容器的管理從Docker Daemon內部業務邏輯中區分開來,設計了Driver層驅動來接管所有這部分請求。
在Docker Driver的實現中,可以分為以下三類驅動:graphdriver、networkdriver和execdriver。
graphdriver主要用於完成容器鏡像的管理,包括存儲與獲取。即當用戶需要下載指定的容器鏡像時,graphdriver將容器鏡像存儲在本地的指定目錄;同時當用戶需要使用指定的容器鏡像來創建容器的rootfs時,graphdriver從本地鏡像存儲目錄中獲取指定的容器鏡像。
在graphdriver的初始化過程之前,有4種文件系統或類文件系統在其內部註冊,它們分別是aufs、btrfs、vfs和devmapper。而Docker在初始化之時,通過獲取系統環境變量”DOCKER_DRIVER”來提取所使用driver的指定類型。而之後所有的graph操作,都使用該driver來執行。
graphdriver的架構如圖4.4:
圖4.4 graphdriver架構示意圖
networkdriver的用途是完成Docker容器網絡環境的配置,其中包括Docker啟動時為Docker環境創建網橋;Docker容器創建時為其創建專屬虛擬網卡設備;以及為Docker容器分配IP、端口並與宿主機做端口映射,設置容器防火墻策略等。networkdriver的架構如圖4.5:
圖4. 5 networkdriver架構示意圖
execdriver作為Docker容器的執行驅動,負責創建容器運行命名空間,負責容器資源使用的統計與限制,負責容器內部進程的真正運行等。在execdriver的實現過程中,原先可以使用LXC驅動調用LXC的接口,來操縱容器的配置以及生命周期,而現在execdriver默認使用native驅動,不依賴於LXC。具體體現在Daemon啟動過程中加載的ExecDriverflag參數,該參數在配置文件已經被設為"native"。這可以認為是Docker在1.2版本上一個很大的改變,或者說Docker實現跨平臺的一個先兆。execdriver架構如圖4.6:
圖4.6 execdriver架構示意圖
4.6 libcontainer
libcontainer是Docker架構中一個使用Go語言設計實現的庫,設計初衷是希望該庫可以不依靠任何依賴,直接訪問內核中與容器相關的API。
正是由於libcontainer的存在,Docker可以直接調用libcontainer,而最終操縱容器的namespace、cgroups、apparmor、網絡設備以及防火墻規則等。這一系列操作的完成都不需要依賴LXC或者其他包。libcontainer架構如圖4.7:
圖4.7 libcontainer示意圖
另外,libcontainer提供了一整套標準的接口來滿足上層對容器管理的需求。或者說,libcontainer屏蔽了Docker上層對容器的直接管理。又由於libcontainer使用Go這種跨平臺的語言開發實現,且本身又可以被上層多種不同的編程語言訪問,因此很難說,未來的Docker就一定會緊緊地和Linux捆綁在一起。而於此同時,Microsoft在其著名雲計算平臺Azure中,也添加了對Docker的支持,可見Docker的開放程度與業界的火熱度。
暫不談Docker,由於libcontainer的功能以及其本身與系統的松耦合特性,很有可能會在其他以容器為原型的平臺出現,同時也很有可能催生出雲計算領域全新的項目。
4.7 Docker container
Docker container(Docker容器)是Docker架構中服務交付的最終體現形式。
Docker按照用戶的需求與指令,訂制相應的Docker容器:
- 用戶通過指定容器鏡像,使得Docker容器可以自定義rootfs等文件系統;
- 用戶通過指定計算資源的配額,使得Docker容器使用指定的計算資源;
- 用戶通過配置網絡及其安全策略,使得Docker容器擁有獨立且安全的網絡環境;
- 用戶通過指定運行的命令,使得Docker容器執行指定的工作。
Docker容器示意圖如圖4.8:
圖4.8 Docker容器示意圖
5 Docker運行案例分析
上一章節著重於Docker架構中各個部分的介紹。本章的內容,將以串聯Docker各模塊來簡要分析,分析原型為Docker中的docker pull與docker run兩個命令。
5.1 docker pull
docker pull命令的作用為:從Docker Registry中下載指定的容器鏡像,並存儲在本地的Graph中,以備後續創建Docker容器時的使用。docker pull命令執行流程如圖5.1。
圖5.1 docker pull命令執行流程示意圖
如圖,圖中標記的紅色箭頭表示docker pull命令在發起後,Docker所做的一系列運行。以下逐一分析這些步驟。
(1) Docker Client接受docker pull命令,解析完請求以及收集完請求參數之後,發送一個HTTP請求給Docker Server,HTTP請求方法為POST,請求URL為"/images/create? "+"xxx";
(2) Docker Server接受以上HTTP請求,並交給mux.Router,mux.Router通過URL以及請求方法來確定執行該請求的具體handler;
(3) mux.Router將請求路由分發至相應的handler,具體為PostImagesCreate;
(4) 在PostImageCreate這個handler之中,一個名為"pull"的job被創建,並開始執行;
(5) 名為"pull"的job在執行過程中,執行pullRepository操作,即從Docker Registry中下載相應的一個或者多個image;
(6) 名為"pull"的job將下載的image交給graphdriver;
(7) graphdriver負責將image進行存儲,一方創建graph對象,另一方面在GraphDB中記錄image之間的關系。
5.2 docker run
docker run命令的作用是在一個全新的Docker容器內部運行一條指令。Docker在執行這條命令的時候,所做工作可以分為兩部分:第一,創建Docker容器所需的rootfs;第二,創建容器的網絡等運行環境,並真正運行用戶指令。因此,在整個執行流程中,Docker Client給Docker Server發送了兩次HTTP請求,第二次請求的發起取決於第一次請求的返回狀態。Docker run命令執行流程如圖5.2。
圖5.2 docker run命令執行流程示意圖
如圖,圖中標記的紅色箭頭表示docker run命令在發起後,Docker所做的一系列運行。以下逐一分析這些步驟。
(1) Docker Client接受docker run命令,解析完請求以及收集完請求參數之後,發送一個HTTP請求給Docker Server,HTTP請求方法為POST,請求URL為"/containers/create? "+"xxx";
(2) Docker Server接受以上HTTP請求,並交給mux.Router,mux.Router通過URL以及請求方法來確定執行該請求的具體handler;
(3) mux.Router將請求路由分發至相應的handler,具體為PostContainersCreate;
(4) 在PostImageCreate這個handler之中,一個名為"create"的job被創建,並開始讓該job運行;
(5) 名為"create"的job在運行過程中,執行Container.Create操作,該操作需要獲取容器鏡像來為Docker容器創建rootfs,即調用graphdriver;
(6) graphdriver從Graph中獲取創建Docker容器rootfs所需要的所有的鏡像;
(7) graphdriver將rootfs所有鏡像,加載安裝至Docker容器指定的文件目錄下;
(8) 若以上操作全部正常執行,沒有返回錯誤或異常,則Docker Client收到Docker Server返回狀態之後,發起第二次HTTP請求。請求方法為"POST",請求URL為"/containers/"+container_ID+"/start";
(9) Docker Server接受以上HTTP請求,並交給mux.Router,mux.Router通過URL以及請求方法來確定執行該請求的具體handler;
(10)mux.Router將請求路由分發至相應的handler,具體為PostContainersStart;
(11)在PostContainersStart這個handler之中,名為"start"的job被創建,並開始執行;
(12)名為"start"的job執行完初步的配置工作後,開始配置與創建網絡環境,調用networkdriver;
(13)networkdriver需要為指定的Docker容器創建網絡接口設備,並為其分配IP,port,以及設置防火墻規則,相應的操作轉交至libcontainer中的netlink包來完成;
(14)netlink完成Docker容器的網絡環境配置與創建;
(15)返回至名為"start"的job,執行完一些輔助性操作後,job開始執行用戶指令,調用execdriver;
(16)execdriver被調用,初始化Docker容器內部的運行環境,如命名空間,資源控制與隔離,以及用戶命令的執行,相應的操作轉交至libcontainer來完成;
(17)libcontainer被調用,完成Docker容器內部的運行環境初始化,並最終執行用戶要求啟動的命令。
6 總結
本文從Docker 1.2的源碼入手,分析抽象出Docker的架構圖,並對該架構圖中的各個模塊進行功能與實現的分析,最後通過兩個docker命令展示了Docker內部的運行。
通過對Docker架構的學習,可以全面深化對Docker設計、功能與價值的理解。同時在借助Docker實現用戶定制的分布式系統時,也能更好地找到已有平臺與Docker較為理想的契合點。另外,熟悉Docker現有架構以及設計思想,也能對雲計算PaaS領域帶來更多的啟發,催生出更多實踐與創新。
轉載:Docker源碼分析(一):Docker架構