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系統調用來執行用戶所指定的在容器中運行的程序。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章