1. 程式人生 > >Docker run執行流詳解(以volume,network和libcontainer為線索)

Docker run執行流詳解(以volume,network和libcontainer為線索)

通常我們都習慣了使用Docker run來執行一個Docker容器,那麼在我們執行Docker run之後,Docker到底都做了什麼工作呢?本文通過追蹤Docker run(Docker 1.9版本)的執行流程,藉由對volume,network和libcontainer的使用和配置的介紹,對Docker run的原理進行了詳細解讀。

首先,使用者通過Docker client輸入docker run來建立被執行一個容器。Docker client主要的工作是通過解析使用者所提供的一系列引數後,分別傳送了這樣兩條請求:

"POST", "/containers/create?"+containerValues
"POST"
, "/containers/"+createResponse.ID

這樣client的工作也就完成了,很顯然client做的事情很少,主要是負責給Docker daemon傳送請求。不過,通過client所傳送的兩條請求,我們可以很自然的把docker run的整個執行過程分成create與start兩個階段。下圖對docker run中各個關鍵事件的發生時間點分別進行了標註。下面我將分別從這個兩個階段並結合下圖,對docker run的執行流程進行介紹。

圖 1 各事件發生的時間點

1 Create階段

這階段Docker daemon的主要工作是對client提交的POST表單進行分析整理,獲得具有可移植性的配置引數結構體config和不可移植的配置結構體hostconfig。然後daemon會呼叫daemon.newContainer函式來建立一個基本的container物件,並將config和hostconfig中儲存的資訊填寫到container物件中。當然此時的container物件並不是一個具體的物理容器,它其中儲存著所有使用者指定的引數和Docker生成的一些預設的配置資訊。最後,Docker會將container物件進行JSON編碼,然後儲存到其對應的狀態檔案中。

上述過程完成後,一個容器的基本配置資訊就已經完全具備,使用者可以使用docker inspect來檢視這個容器所對應的各種配置資訊。

2 start階段

完成了create階段後,client緊接著會發送start請求來啟動一個真正的物理容器。當Docker daemon接收到這個start請求後,會使用在create階段配置好的container物件中的各種配置引數來完成volume掛點的註冊,容器網路的建立和建立並啟動物理容器等工作。下面先對容器的網路環境建立過程做一個介紹。

2.1 volume掛載點的註冊

Docker daemon在將hostconfig配置到容器配置資訊的過程中,會呼叫daemon.registerMountPoints函式對client提供的POST表單中的volume相關資訊進行註冊,並以mountpoint的形式儲存在容器的配置資訊中,在真正啟動物理容器的時候才會進行掛載。

Volume的掛載點可以分為兩類,一類為使用其他容器中的掛載點,另一類為使用者指定的繫結掛載。下面我們來看一下兩種volume掛載點的註冊流程。

  • 1.使用其他容器中的掛載點:在對這類掛載點進行註冊時,首先會使用容器的id在Docker daemon中查詢對應的結構體。然後遍歷其中的所有掛載點,並且將其中的掛載點資訊全部都註冊到當前的容器結構體之中。

  • 2.使用者指定的繫結掛載:使用者指定的繫結掛載可以有Source Path:Destination Path的格式,也可以是Name:Destination Path的格式。如果使用者輸入的引數是Source Path:Destination Path的格式那麼,daemon會解析其中的Source Path和Destination Path,並使用它們註冊對應的掛載點。如果使用者輸入的引數是Name:Destination Path的格式,那麼daemon會查詢使用者提供的Name是否已經對應了一個使用docker volume已經建立好了的掛載點資訊,如果是的話,則會使用這個掛載點的資訊和使用者提供的Destination Path進行本容器的掛載點註冊。如果這個Name在daemon中沒有對應的掛載點的話,daemon則會在其預設資料夾下建立一個目錄,作為掛載點中的Source Path,然後使用使用者提供的Destination Path和自行建立的Source Path進行本容器的掛載點註冊。

在完成了掛載點的註冊之後,daemon會將所有的掛載點資訊更新到容器的配置資訊中,以備後續使用。

2.2 網路的建立

Docker daemon使用client提供的POST表單中網路相關的引數,通過呼叫daemon.initializeNetworking函式來完成容器網路棧的建立和配置。daemon.initializeNetworking函式則通過對Docker的網路依賴庫(即libnetwork)的一系列呼叫,來完成容器的網路棧建立和配置等工作。

要理解Docker容器的網路部分的執行流程,那麼首先要清楚libnetwork中的三個核心概念。

  • 沙盒(Sandbox):一個沙盒包含了一個容器網路棧的資訊。沙盒可以對容器的介面,路由和DNS設定等進行管理。沙盒的實現可以是Linux Network Namespace, FreeBSD Jail或者類似的機制。一個沙盒可以有多個端點(Endpoint)和多個網路(Network)。

  • 端點(Endpoint):一個端點可以加入一個沙盒和一個網路。端點的實現可以是veth pair, Open vSwitch內部埠或者相似的裝置。一個端點只可以屬於一個網路並且只屬於一個沙盒。

  • 網路(Network):一個網路是一組可以直接互相聯通的端點。網路的實現可以是Linux bridge,VLAN等等。一個網路可以包含多個端點。

清楚了以上三個核心概念之後,我們從Docker原始碼的角度並通過Docker中預設的網路模式(bridge模式)來看一下容器網路棧的建立過程。

  • 在Docker daemon啟動之後,會建立一個預設的network,其本質工作就是建立了一個名為docker0的預設網橋。

  • 確定預設網橋之後,daemon會呼叫container.BuildCreateEndpointOptions來建立此容器中endpoint的配置資訊。然後再呼叫Network.CreateEndpoint使用上面配置好的資訊建立對應的endpoint。在bridge模式中,libnetwork建立的裝置是veth pair。Libnetwork中呼叫netlink.LinkAdd(veth)進行了veth pair的建立,把其中的的一個veth裝置是加入到docker0網橋中,另一個則是為了sandbox所準備的。

  • 接下來daemon會呼叫daemon.buildSandboxOptions來建立此容器的sandbox,然後呼叫Network.NewSandbox來建立屬於此容器的新的sandbox。libnetwork在接收到建立sandbox的請求後,會使用系統呼叫為容器建立一個新的netns,並將這個netns的路徑返寫入到對應容器的配置資訊中,以便後續的使用。

  • 最後,daemon會呼叫ep.Join(sb)將endpoint加入到容器對應的sandbox中。先將endpoint加入到容器對應的sandbox中,然後對endpoint的ip資訊和gateway等資訊進行配置,並將所有的資訊更新到對應容器的配置資訊中。

2.3 容器的建立和啟動

在完成建立容器的各種準備工作之後,Docker daemon會通過對libcontainer的一系列呼叫來完成容器的建立和啟動工作。Libcontainer是Docker的執行時庫,它可以通過呼叫者提供的配置引數來建立並執行一個容器出來,下面我們來看一Docker是如何使用之前配置的結構體中的各項引數,通過libcontainer建立並執行一個容器的。

2.3.1 建立邏輯容器Container與邏輯程序process

所謂的邏輯容器container和邏輯程序process並非時真正執行著的容器和程序,而是libcontainer中所定義的結構體。邏輯容器container中包含了namespace,cgroups,device和mountpoint等各種配置資訊。邏輯程序process中則包含了容器中所要執行的指令以其引數和環境變數等。

Docker daemon會呼叫execdriver.Run來完成和libcontainer的一系列互動工作。首先將會將所有和新建容器相關的引數裝入可以被libcontainer使用的結構體config中。然後使用config作為引數來呼叫libcontainer.New()生成用來產生container的工廠factory。再呼叫factory.Create(config),就會生成一個將config包含其中的邏輯容器container。接下來呼叫newProcess(config)來將config中關於容器內所要執行命令的相關資訊填充到process結構體中,這個結構體即為邏輯程序process。使用container.Start(process)來啟動邏輯容器。

2.3.2 啟動邏輯容器container

Docker daemon會呼叫linuxContainer.Start來啟動邏輯容器。這個函式的主要工作就是呼叫newParentProcess()來生成parentprocess例項(結構體)和用於runC與容器內init程序相互通訊的管道。

在parentprocess例項中,除了有記錄了將來與容器內程序進行通訊的管道與各種基本配置等,還有一個極為重要的欄位就是其中的cmd。

cmd欄位是定義在os/exec包中的一個結構體。os/exec包主要用於建立一個新的程序,並在這個程序中執行指定的命令。開發者可以在工程中匯入os/exec包,然後將cmd結構體進行填充,即將所需執行程式的路徑和程式名,程式所需引數,環境變數,各種作業系統特有的屬性和拓展的檔案描述符等。

在Docker中程式將cmd的應用路徑欄位Path填充為/proc/self/exe(即為應用程式本身,Docker)。引數欄位Args填充為init,表示對容器進行初始化。SysProcAttr欄位中則填充了各種Docker所需啟用的namespace(其中包括前面所講到的netns路徑)等屬性。

然後呼叫parentprocess.cmd.Start()啟動物理容器中的init程序。接下來將物理容器中init程序的程序號加入到Cgroup控制組中,對容器內的程序實施資源控制。再把配置引數通過管道傳送給init程序。最後通過管道等待init程序根據上述配置完成所有的初始化工作,或者出錯退出。

2.3.3 物理容器的配置和建立

容器中的init程序首先會呼叫StartInitialization()函式,通過管道從父程序接收各種配置引數。然後對容器進行如下配置:

  • 1.將init程序加入其指定的namespace中,這裡會將init程序加入到前面已經建立好的netns中,這樣init程序就擁有了自己獨立的網路棧,完成了網路建立和配置的最後一步。

  • 2.設定程序的會話ID。

  • 3.使用系統呼叫,將前面註冊好的掛載點全部掛載到物理主機上,這樣就完成了volume的建立。

  • 4.對指定目錄下的檔案系統進行掛載,並切換根目錄到新掛載的檔案系統下。設定hostname,載入profile資訊。

  • 5.最後使用exec系統呼叫來執行使用者所指定的在容器中執行的程式。

這樣就完成了一個容器的建立和啟動過程。