這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
【編者的話】本次分享首先對OCI標準進行解讀。然後從源碼層面對runC容器運行原理進行解讀。講解runC是如何用給定的設定檔run出一個容器來。最後對熱遷移的配置和原理進行簡介。
在過去兩年中隨著互連網和容器技術的發展,幾乎主要的所有的IT供應商和雲端服務供應商都開始採用以容器技術為基礎的解決方案,與容器相關的組織也如雨後春筍般增長。於是為了確保容器的可遷移性,容器格式和運行時標準的建立就顯得尤為重要。
所以,Linux基金會於2015年6月成立OCI(Open Container Initiative)組織,旨在圍繞容器格式和運行時制定一個開放的工業化標準。該組織一成立便得到了包括Google、微軟、亞馬遜、華為等一系列雲端運算廠商的支援。
1. 容器格式標準是什嗎?
制定容器語式正確宗旨概括來說就是不受上層結構的綁定,如特定的用戶端、編排棧等,同時也不受特定的供應商或項目的綁定,即不限於某種特定作業系統、硬體、CPU架構、公用雲端等。
該標準目前由libcontainer和appc的項目負責人(maintainer)進行維護和制定,其規範文檔就作為一個項目在GitHub上維護。
1.1 容器標準化宗旨
標準化容器的宗旨具體分為如下五條。
- 操作標準化:容器的標準化操作包括使用標準容器建立、啟動、停止容器,使用標準檔案系統工具複製和建立容器快照,使用標準化網路工具進行下載和上傳。
- 內容無關:內容無關指不管針對的具體容器內容是什麼,容器標準操作執行後都能產生同樣的效果。如容器可以用同樣的方式上傳、啟動,不管是PHP應用還是MySQL資料庫服務。
- 基礎設施無關:無論是個人的膝上型電腦還是AWS S3,亦或是OpenStack,或者其它基礎設施,都應該對支援容器的各項操作。
- 為自動化量身定製:制定容器統一標準,是的操作內容無關化、平台無關化的根本目的之一,就是為了可以使容器操作全平台自動化。
- 工業級交付:制定容器標準一大目標,就是使軟體分發可以達到工業級交付成為現實。
1.2 容器標準包(bundle)和配置
一個標準的容器包具體應該至少包含三塊部分:
config.json: 基本設定檔,包括與宿主機獨立的和應用相關的特定資訊,如安全許可權、環境變數和參數等。具體如下:
- 容器格式版本
- rootfs路徑及是否唯讀
- 各類檔案掛載點及相應容器內掛載目錄(此配置資訊必須與runtime.json 配置中保持一致)
- 初始進程配置資訊,包括是否綁定終端、運行可執行檔的工作目錄、環境變數配置、可執行檔及執行參數、uid、gid以及額外需要加入的gid、hostname、低層作業系統及CPU架構資訊。
runtime.json:運行時設定檔,包含運行時與主機相關的資訊,如記憶體限制、本地裝置存取權限、掛載點等。除了上述配置資訊以外,運行時設定檔還提供了“鉤子(hooks)”的特性,這樣可以在容器運行前和停止後各執行一些自訂指令碼。hooks的配置包含執行指令碼路徑、參數、環境變數等。
rootfs/:根檔案系統目錄,包含了容器執行所需的必要環境依賴,如/bin、/var、/lib、/dev、/usr等目錄及相應檔案。rootfs目錄必須與包含配置資訊的config.json檔案同時存在容器目錄最頂層。
1.3 容器運行時和生命週期
容器標準格式也要求容器把自身運行時的狀態持久化到磁碟中,這樣便於外部的其它工具對此資訊使用和演繹。該運行時狀態以JSON格式編碼儲存。推薦把運行時狀態的JSON檔案儲存體在臨時檔案系統中以便系統重啟後會自動移除。
基於Linux核心的作業系統,該資訊應該統一地儲存在/run/opencontainer/containers目錄,該目錄結構下以容器ID命名的檔案夾(/run/opencontainer/containers/<containerID>/state.json)中存放容器的狀態資訊並即時更新。有了這樣預設的容器狀態資訊儲存位置以後,外部的應用程式就可以在系統上簡便地找到所有運行著的容器了。
state.json檔案中包含的具體資訊需要有:
- 版本資訊:存放OCI標準的具體版本號碼。
- 容器ID:通常是一個雜湊值,也可以是一個易讀的字串。在state.json檔案中加入容器ID是為了便於之前提到的運行時hooks只需載入state.json就可以定位到容器,然後檢測state.json,發現檔案不見了就認為容器關停,再執行相應預定義的指令碼操作。
- PID:容器中啟動並執行首個進程在宿主機上的進程號。
- 容器檔案目錄:存放容器rootfs及相應配置的目錄。外部程式只需讀取state.json就可以定位到宿主機上的容器檔案目錄。
標準的容器生命週期應該包含三個基本過程。
- 容器建立:建立包括檔案系統、namespaces、cgroups、使用者權限在內的各項內容。
- 容器進程的啟動:運行容器進程,進程的可執行檔定義在的config.json中,args項。
- 容器暫停:容器實際上作為進程可以被外部程式關停(kill),然後容器標準規範應該包含對容器暫停訊號的捕獲,並做相應資源回收的處理,避免孤兒進程的出現。
1.4 基於開放容器格式(OCF)標準的具體實現
從上述幾點中總結來看,開放容器規範的格式要求非常寬鬆,它並不限定具體的實現技術也不限定相應架構,目前已經有基於OCF的具體實現,相信不久後會有越來越多的項目出現。
容器運行時opencontainers/runc,即本文所講的RunC項目,是後來者的參照標準。
虛擬機器運行時hyperhq/runv,基於Hypervisor技術的開放容器規範實現。
測試huawei-openlab/oct基於開放容器規範的測試架構。
2. runC工作原理與實現方式
2.1 runC從libcontainer的變遷
runC的前身實際上是Docker的libcontainer項目演化而來。runC實際上就是libcontainer配上了一個輕型的用戶端。
從本質上來說,容器是提供一個與宿主機系統共用核心但與系統中的其它進程資源相隔離的執行環境。Docker通過調用libcontainer包對namespaces、cgroups、capabilities以及檔案系統的管理和分配來“隔離”出一個上述執行環境。同樣的,runC也是對libcontainer包進行調用,去除了Docker包含的諸如鏡像、Volume等進階特性,以最樸素簡潔的方式達到符合OCF標準的容器管理實現。
總體而言,從libcontainer項目轉變為runC項目至今,其功能和特性並沒有太多變化,具體有如下幾點。
- 把原先的nsinit移除,放到外面,命令名稱改為runC,同樣使用cli.go實現,一目瞭然。
- 按照開放容器標準把原先所有資訊混在一起的一個設定檔拆分成config.json和runtime.json兩個。
- 增加了按照開放容器標準設定的容器運行前和停止後執行的hook指令碼功能。
- 相比原先的nsinit時期的指令,增加了runc kill命令,用於發送一個SIG_KILL訊號給指定容器ID的init進程。
總體而言,runC希望包含的特徵有:
- 支援所有的Linux namespaces,包括user namespaces。目前user namespaces尚未包含。
- 支援Linux系統上原有的所有安全相關的功能,包括Selinux、 Apparmor、seccomp、cgroups、capability drop、pivot_root、 uid/gid dropping等等。目前已完成上述功能的支援。
- 支援容器熱遷移,通過CRIU技術實現。目前功能已經實現,但是使用起來還會產生問題。
- 支援Windows 10 平台上的容器運行,由微軟的工程師開發中。目前只支援Linux平台。
- 支援Arm、Power、Sparc硬體架構,將由Arm、Intel、Qualcomm、IBM及整個硬體製造商生態圈提供支援。
- 計劃支援尖端的硬體功能,如DPDK、sr-iov、tpm、secure enclave等等。
- 生產環境下的高效能適配最佳化,由Google工程師基於他們在生產環境下的容器部署經驗而貢獻。
- 作為一個正式真實而全面具體的標準存在!
2.2 runC是如何啟動容器的?
從開放容器標準中我們已經定義了關於容器的兩份設定檔和一個依賴包,runC就是通過這些來啟動一個容器的。首先我們按照官方的步驟來操作一下。
runC運行時需要有rootfs,最簡單的就是你本地已經安裝好了Docker,通過
docker pull busybox
下載一個基本的鏡像,然後通過
docker export $(docker create busybox) > busybox.tar
匯出容器鏡像的rootfs檔案壓縮包,命名為busybox.tar。然後解壓縮為rootfs目錄,
mkdir rootfstar -C rootfs -xf busybox.tar
,
這時我們就有了OCF標準的rootfs目錄,需要說明的是,我們使用Docker只是為了擷取rootfs目錄的方便,runc的運行本身不依賴Docker。
接下來你還需要
config.json
和
runtime.json
,使用
runc spec
可以產生一份標準的
config.json
和
runtime.json
設定檔,當然你也可以按照格式自己編寫。
如果你還沒有安裝runC,那就需要按照如下步驟安裝一下,目前runC暫時只支援Linux平台。
# create a 'github.com/opencontainers' in your GOPATH/srccd github.com/opencontainersgit clone https://github.com/opencontainers/runccd runcmakesudo make install
最後執行
runc start
你就啟動了一個容器了。
2.3 runC start運行原理
上面說到過runC就是libcontainer外面裹上了一層很薄的Cli。其中的Cli是為了快速開發Go語言的命令列應用而實現的開發包,它可以為你處理諸如子命令定義,標誌位定義和設定協助資訊等等。並且Cli也是託管在Git上面的一個開源項目,地址為:github.com/codegangsta/cli。
從源碼角度,分析runC start的執行流程,整個分析過程如:
2.3.1.一切從main()函數開始
整個程式首先執行main.go中的main()函數,在這個函數中,程式通過cli包對runC的各個子命令、參數、版本號碼以及協助資訊進行規定。然後程式會通過使用者輸入的子命令來調用對應的處理函數,這裡則調用start.go中的startContainer()函數。
2.3.2.建立邏輯容器Container與邏輯進程process
所謂的邏輯容器container和邏輯進程process並非時真正運行著的容器和進程,而是libcontainer中所定義的結構體。邏輯容器container中包含了namespace、cgroups、device和mountpoint等各種配置資訊。邏輯進程process中則包含了容器中所要啟動並執行指令以其參數和環境變數等。
對於runC來說,容器的定義只需要一種就夠了,不同的容器只是執行個體的內容(屬性和參數)不一樣而已。對於libcontainer來說,由於它需要與底層打交道,不同的平台上就需要建立出完全異構的“邏輯容器物件”(比如Linux容器和Windows容器),這也就解釋了為什麼這裡會使用“原廠模式”:今後libcontainer可以支援更多平台上各種類型容器的實現,而不必改變調用介面。
下面解釋一下邏輯容器Container與邏輯進程process的建立過程。
在startContainer()函數中,程式首先將*.json裝入可以被libcontainer使用的結構體config中。然後使用config作為參數來調用。libcontainer.New()產生用來產生container的工廠factory。再調用factory.Create(config),就會產生一個將config包含其中的邏輯容器container。接下來調用newProcess(config)來將config中關於容器內所要運行命令的相關資訊填充到process結構體中,這個結構體即為邏輯進程process。使用container.Start(process)來啟動邏輯容器。
2.3.3.啟動邏輯容器container
runC會調用Start(),Start()函數位於libcontainer/container_linux.go中,主要工作就是調用newParentProcess()來產生parentprocess執行個體(結構體)和用於runC與容器內init進程相互連信的管道。
在parentprocess執行個體中,除了有記錄了將來與容器內進程進行通訊的管道與各種基本配置等,還有一個極為重要的欄位就是其中的cmd。
cmd欄位是定義在os/exec包中的一個結構體。os/exec包主要用於建立一個新的進程,並在這個進程中執行指定的命令。開發人員可以在工程中匯入os/exec包,然後將cmd結構體進行填充,即將所需運行程式的路徑和程式名,程式所需參數,環境變數,各種作業系統特有的屬性和拓展的檔案描述符等。
在runC中程式將cmd的應用路徑欄位Path填充為/proc/self/exe(即為應用程式本身,runC)。參數欄位Args填充為init,表示對容器進行初始化。SysProcAttr欄位中則填充了各種runC所需啟用的namespace等屬性。
然後調用parentprocess.cmd.Start()啟動物理容器中的init進程。接下來將物理容器中init進程的進程號加入到Cgroup控制組中,對容器內的進程實施資源控制。再把配置參數通過管道傳送給init進程。最後通過管道等待init進程根據上述配置完成所有的初始化工作,或者出錯退出。
2.3.4.物理容器的配置和建立
容器中的init進程首先會調用StartInitialization()函數,通過管道從父進程接收各種配置參數。然後對容器進行如下配置:
- 如果使用者指定,則將init進程加入其指定的namespace。
- 設定進程的會話ID。
- 初始化網路裝置。
- 對指定目錄下的檔案系統進行掛載,並切換根目錄到新掛載的檔案系統下。設定hostname,載入profile資訊。
- 最後使用exec系統調用來執行使用者所指定的在容器中啟動並執行程式。
3.熱遷移的配置與原理簡介
3.1 熱遷移簡介
所謂熱遷移就是將一個容器進行Checkpoint操作,並獲得一系列檔案,使用這一系列檔案可以在本機或者其他主機上進行容器的Restore工作。目前,在runC中使用了CRIU作為熱遷移的工具,並實現了對容器的Checkpoint和Restore功能。簡要的過程如所示。
3.2 runC熱遷移原理簡介
在runC中熱遷移的工作主要是調用CRIU(Checkpoint and Restore in Userspace)來完成。CIRU負責凍結進程,並將作為一系列檔案儲存體在硬碟上。並負責使用這些檔案還原這個被凍結的進程。
runC使用SWRK模式來調用criu。這種模式是criu另外兩種模式CLI和RPC的結合體,允許使用者需要的時候像使用命令列工具一樣運行criu,並接受使用者遠程調用的請求。
runC主要通過如下兩個步驟完成熱遷移工作。
產生container,通過state.json或者設定檔*.json來產生container結構體。
使用SWRK模式調用CRIU,runC首先收集並整理要進行Checkpoint或者Restore操作的容器的相關資訊,並填入要發給SWRK模式下的CRIU的結構體中。結構體主要內容如下:
req := &criurpc.CriuReq{
Type: &t, //C or R
Opts: &rpcOpts, //criu相關參數
}
其中的欄位t指定了這個請求是進行Checkpoint操作還是Restore操作,欄位rpcOpts中則各種使用者指定的選項和CRIU運行所需的參數。
隨後通過syscall.Socketpair()建立runC(criuClient)與CIRU(criuServer)之間的通訊管道。然後使用go語言中的os/exec包,以SWRK方式啟動criu。再通過criuClient向criuServer發送request。最後通過criuClient接收執行結果即可。
3.3 目前的版本下runC熱遷移的配置與使用
由於目前的版本的CRIU並非十分完善,還不能完全支援runC中的一少部分特性,所以在進行熱遷移工作的時候需要對設定檔進行一些修改。具體修改的內容和原因如下:
- 因為CRIU不支援seccomp,所以需要將config.json檔案中關於seccomp的相關內容置空。
- 因為CRIU不支外部終端,所以需要將config.json檔案中terminal的值置為false。
- 因為CRIU的需求runC所掛載的檔案系統時可讀的,所以將config.json檔案中檔案系統的可讀寫性設定為可讀。
部分配置如所示。
正確安裝CRIU及其相關依賴並且對config.json做出以上的修改後就可以使用runC內建的命令對容器進行熱遷移了。
Q&A
Q:對容器熱遷移聽得比較少,也比較好奇,想問問熱遷移的時候容器依賴的檔案系統怎麼解決,是直接copy到目標機嗎,可否使用公用網路檔案系統解決呢,還有對目標機有什麼特殊要求嗎?
A:依賴檔案系統要保持一致。最好事先在目標機預置檔案系統。有些容器涉及到不能遷移的裝置之類的,那麼這個容器則不能被遷移。目標機的OS位元要一樣。
Q:解runC,問下和Docker 對比,有那些不同,又有什麼優勢?
A:runC的優勢體現在輕量級和標準化,這是Docker沒有的。而Docker那一套龐大的體系,也是runC沒有的。
Q:dumpfiles是可以持久化的嗎,還是僅僅用於一次性的熱遷移交換?
A:儲存在硬碟,可以進行多次使用。
Q:CRIU 熱遷移網路設定資訊會儲存麼,還有就是大概熱遷移延時是多少?
A:只要設定了參數就會儲存,熱遷移的延時這個要看你配套設施和配套軟體。
Q:我對這個熱遷移沒瞭解過。想問下,這個熱遷移主要是遷移配置rootfs麼,能遷移當前的容器狀態麼,比如進行中的計算?
A:是的,這個要在對應的機器上有對應的rootfs才能遷移。狀態什麼的都可以遷移。
Q:那是不是意味著我可以利用CRIU工具把容器固化下來,類似於虛擬機器的快照,以便在未來的某個時刻進行恢複或者遷移?
A:是的,就是這樣。
===========================
以上內容根據2015年10月27日晚群分享內容整理。分享
高相林,浙江大學SEL實驗室碩士研究生,目前在雲平台團隊從事科研和開發工作。浙大團隊對PaaS、Docker、大資料和主流開源雲端運算技術有深入的研究和二次開發經驗,團隊現聯合社區將部分技術文章貢獻出來,希望能對讀者有所協助。 DockOne每周都會組織定向的技術分享,歡迎感興趣的同學加:liyingjiesx,進群參與,您有想聽的話題可以給我們留言。