天天看點

《容器技術系列》一2.2 建立Docker Client

本節書摘來華章計算機《容器技術系列》一書中的第2章 ,第2.2節,孫宏亮 著, 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

對于docker這樣一個client/server的架構,用戶端的存在意味着docker相應任務的發起。使用者首先需要建立一個dockerclient,随後将特定的請求類型與參數傳遞至docker client,最終由docker client轉義成docker server能識别的形式,并發送至docker server。

docker client的建立實質上是docker使用者通過二進制可執行檔案docker,建立與docker server建立聯系的用戶端。以下分3個小節分别闡述docker client的建立流程。

docker client完整的運作流程如圖2-1所示。

《容器技術系列》一2.2 建立Docker Client

通過學習圖2-1,我們可以更為清晰地了解docker client建立及執行請求的過程。其中涉及諸多docker源碼層次中的專有名詞,本章後續會一一解釋與分析。

衆所周知,在docker的具體實作中,docker server與docker client均由可執行檔案docker來完成建立并啟動。那麼,了解docker可執行檔案通過何種方式來區分到底是docker server還是docker client,就顯得尤為重要。

首先通過docker指令舉例說明其中的差別。docker server的啟動,指令為docker -d或docker --daemon=true;而docker client的啟動則展現為docker --daemon=false ps、docker pull name等。

其實,對于docker請求中的參數,我們可以将其分為兩類:第一類為指令行參數,即docker程式運作時所需提供的參數,如: -d、--daemon=true、--daemon=false等;第二類為docker發送給docker server的實際請求參數,如:ps、pull name等。

對于第一類,我們習慣将其稱為flag參數,在go語言的标準庫中,專門為該類參數提供了一個flag包,友善進行指令行參數的解析。

清楚docker二進制檔案的使用以及基本的指令行flag參數之後,我們可以進入實作docker client建立的源碼中,位于./docker/docker/docker.go。這個go檔案包含了整個docker的main函數,也就是整個docker(不論docker daemon還是docker client)的運作入口。部分main函數代碼如下:

以上源碼實作中,首先判斷reexec.init()方法的傳回值,若為真,則直接退出運作,否則将繼續執行。reexec.init()函數的定義位于./docker/reexec/reexec.go,可以發現由于在docker運作之前沒有任何initializer注冊,故該代碼段執行的傳回值為假。reexec存在的作用是:協調execdriver與容器建立時dockerinit這兩者的關系。第13章在分析dockerinit的啟動時,将詳細講解reexec的作用。

判斷reexec.init()之後,docker的main函數通過調用flag.parse()函數,解析指令行中的flag參數。如果熟悉go語言中的flag參數,一定知道解析flag參數的值之前,程式必須先定義相應的flag參數。進一步檢視docker的源碼,我們可以發現docker在./docker/docker/flag.go中定義了多個flag參數,并通過init函數進行部分flag參數的初始化。代碼如下:

以上源碼展示了docker如何定義flag參數,以及在init函數中實作部分flag參數的初始化。docker的main函數執行前,這些變量建立以及初始化工作已經全部完成。這裡涉及了go語言的一個特性,即init函數的執行。go語言中引入其他包(import package)、變量的定義、init函數以及main函數這四者的執行順序如圖2-2所示。

關于golang中的init函數,深入分析可以得出以下特性:

init函數用于程式執行前包的初始化工作,比如初始化變量等;

每個包可以有多個init函數;

包的每一個源檔案也可以有多個init函數;

同一個包内的init函數的執行順序沒有明确的定義;

不同包的init函數按照包導入的依賴關系決定初始化的順序;

init函數不能被調用,而是在main函數調用前自動被調用。

《容器技術系列》一2.2 建立Docker Client

清楚go語言一些基本的特性之後,回到docker中來。docker的main函數執行之前,docker已經定義了諸多flag參數,并對很多flag參數進行初始化。定義并初始化的指令行flag參數有:flversion、fldaemon、fldebug、flsocketgroup、flenablecors、fltls、fltlsverify、flca、flcert、flkey、flhosts等。

以下具體分析fldaemon:

定義:fldaemon = flag.bool([]string{"d", "-daemon"}, false, "enable daemon mode");

fldaemon的類型為bool類型;

fldaemon名稱為"d"或者"-daemon",該名稱會出現在docker指令中,如docker –d;

fldaemon的預設值為false;

fldaemon的用途資訊為"enable daemon mode";

通路fldaemon的值時,使用指針*fldaemon解引用通路。

在解析指令行flag參數時,以下語句為合法的(以fldaemon為例):

-d, --daemon

-d=true, --daemon=true

-d="true", --daemon="true"

-d='true', --daemon='true'

當解析到第一個非定義的flag參數時,指令行flag參數解析工作結束。舉例說明,當執行docker指令docker --daemon=false --version=false ps時,flag參數解析主要完成兩個工作:

完成指令行flag參數的解析,根據flag的名稱-daemon和-version,得知具體的flag參數為fldaemon和flversion,并獲得相應的值,均為false。

遇到第一個非定義的flag參數ps時,flag包會将ps及其之後所有的參數存入flag.args(),以便之後執行docker client具體的請求時使用。

如需深入學習flag的實作,可以參見docker源碼./docker/pkg/mflag/flag.go。

了解go語言解析flag參數的相關知識,可以很大程度上幫助了解docker的main函數的執行流程。通過總結,首先列出源碼中處理的flag資訊以及收集docker client的配置資訊,然後再一一進行分析:

處理的flag參數有:flversion、fldebug、fldaemon、fltlsverify以及fltls。

為docker client收集的配置資訊有:protoaddrparts(通過flhosts參數獲得,作用是提供docker client與docker server的通信協定以及通信位址)、tlsconfig(通過一系列flag參數獲得,如fltls、fltlsverify,作用是提供安全傳輸層協定的保障)。

清楚flag參數以及docker client的配置資訊之後,我們進入main函數的源碼,具體分析如下。

在flag.parse()之後的源碼如下:

以上代碼很好了解,解析flag參數後,若docker發現flag參數flversion為真,則說明docker使用者希望檢視docker的版本資訊。此時,docker調用showversion()顯示版本資訊,并從main函數退出;否則的話,繼續往下執行。

若fldebug參數為真的話,docker通過os包中的setenv函數建立一個名為debug的環境變量,并将其值設為"1";繼續往下執行。

以上的源碼主要分析内部變量flhosts。flhosts的作用是為docker client提供所要連接配接的host對象,也就是為docker server提供所要監聽的對象。

在分析過程中,首先判斷flhosts變量是否長度為0。若是的話,則說明使用者并沒有顯性傳入位址,此時docker的政策為選用預設值。docker通過os包擷取名為docker_host環境變量的值,将其指派于defaulthost。若defaulthost為空或者fldaemon為真,說明目前還沒有一個定義的host對象,則将其預設設定為unix socket,值為api.defaultunixsocket,該常量位于./docker/api/common.go,值為"/var/run/docker.sock",故defaulthost為"unix:///var/run/docker.sock"。驗證該defaulthost的合法性之後,将defaulthost的值追加至flhost的末尾,繼續往下執行。當然若flhost的長度不為0,則說明使用者已經指定位址,同樣繼續往下執行。

若fldaemon參數為真,則說明使用者的需求是啟動docker daemon。docker随即執行maindaemon函數,實作docker daemon的啟動;若maindaemon函數執行完畢,則退出main函數。一般maindaemon函數不會主動終結,docker daemon将作為一個常駐程序運作在主控端上。本章着重介紹docker client的啟動,故假設fldaemon參數為假,不執行以上代碼塊。繼續往下執行。

由于不執行docker daemon的啟動流程,故屬于docker client的執行邏輯。首先,判斷flhosts的長度是否大于1。若flhosts的長度大于1,則說明需要新建立的docker client通路不止1個docker daemon位址,顯然邏輯上行不通,故抛出錯誤日志,提醒使用者隻能指定一個docker daemon位址。接着,docker将flhosts這個string數組中的第一個元素進行分割,通過"://"來分割,分割出的兩個部分放入變量protoaddrparts數組中。protoaddrparts的作用是:解析出docker client與docker server建立通信的協定與位址,為docker client建立過程中不可或缺的配置資訊之一。一般情況下,flhosts[0]的值可以是tcp://0.0.0.0:2375或者unix:///var/run/docker.sock等。

由于之前已經假設過fldaemon為假,可以認定main函數的運作是為了docker client的建立與執行。docker在這裡建立了兩個變量:一個為類型是*client.dockercli的對象cli,另一個為類型是tls.config的對象tlsconfig。定義完變量之後,docker将tlsconfig的insecureskipverify屬性置為真。tlsconfig對象的建立是為了保障cli在傳輸資料的時候遵循安全傳輸層協定(tls)。安全傳輸層協定(tls)用于確定兩個通信應用程式之間的保密性與資料完整性。tlsconfig是docker client建立過程中可選的配置資訊。

若fltlsverify這個flag參數為真,則說明docker client需docker server一起驗證連接配接的安全性。此時,tlsconfig對象需要加載一個受信的ca檔案。該ca檔案的路徑為*flca參數的值,最終完成tlsconfig對象中rootcas屬性的指派,并将insecureskipverify屬性置為假。

如果fltls和fltlsverify兩個flag參數中有一個為真,則說明需要加載并發送用戶端的證書。最終将證書内容交給tlsconfig的certificates屬性。

至此,flag參數已經全部處理完畢,dockerclient也已經收集到所需的配置資訊。下一節将主要分析如何建立docker client。

docker client的建立其實就是在已有配置參數資訊的情況下,通過client包中的newdockercli方法建立一個docker clinet執行個體cli。具體源碼實作如下:

若flag參數fltls為真或者fltlsverify為真,則說明需要使用tls協定來保障傳輸的安全性,故建立docker client的時候,将tlsconfig參數傳入;否則,同樣建立docker client,隻不過tlsconfig為nil。

關于client包中的newdockercli函數的實作,可以具體參見./docker/api/clie<code>`</code>javascript

nt/cli.go。

func newdockercli(in io.readcloser, out, err io.writer, proto, addr string, tlsconfig tls.config) dockercli {

var (

)

if tlsconfig != nil {

}

if in != nil {

if err == nil {

return &amp;dockercli{

繼續閱讀