OCI標準和runC原理解讀

jopen 9年前發布 | 17K 次閱讀 OCI


在過去兩年中隨著互聯網和容器技術的發展,幾乎主要的所有的IT供應商和云服務提供商都開始采用以容器技術為基礎的解決方案,與容器相關的組織也如雨后春筍搬增長。于是為了確保容器的可遷移性,容器格式和運行時的標準的建立就顯得尤為重要。

所以,Linux基金會于2015年6月成立OCI(Open Container Initiative)組織,旨在圍繞容器格式和運行時制定一個開放的工業化標準。該組織一成立便得到了包括谷歌、微軟、亞馬遜、華為等一系列云計算廠商的支持。

1. 容器格式標準是什么?

制定容器格式標準的宗旨概括來說就是不受上層結構的綁定,如特定的客戶端、編排棧等,同時也不受特定的供應商或項目的綁定,即不限于某種特定操作系統、硬件、CPU架構、公有云等。

該標準目前由libcontainer和appc的項目負責人(maintainer)進行維護和制定,其規范文檔就作為一個項目在GitHub上維護,地址為 https://github.com/opencontainers/specs

1.1 容器標準化宗旨

標準化容器的宗旨具體分為如下五條。

操作標準化:容器的標準化操作包括使用標準容器感覺創建、啟動、停止容器,使用標準文件系統工具復制和創建容器快照,使用標準化網絡工具進行下載和上傳。

內容無關:內容無關指不管針對的具體容器內容是什么,容器標準操作執行后都能產生同樣的效果。如容器可以用同樣的方式上傳、啟動,不管是php應用還是mysql數據庫服務。

基礎設施無關:無論是個人的筆記本電腦還是AWS S3,亦或是Openstack,或者其他基礎設施,都應該對支持容器的各項操作。

為自動化量身定制:制定容器統一標準,是的操作內容無關化、平臺無關化的根本目的之一,就是為了可以使容器操作全平臺自動化。

工業級交付:制定容器標準一大目標,就是使軟件分發可以達到工業級交付成為現實。

1.2 容器標準包(bundle)和配置

一個標準的容器包具體應該至少包含三塊部分:

config.json: 基本配置文件,包括與宿主機獨立的和應用相關的特定信息,如安全權限、環境變量和參數等。具體如下:

1,容器格式版本

2,rootfs路徑及是否只讀

3,各類文件掛載點及相應容器內掛載目錄(此配置信息必須與runtime.json 配置中保持一致)

4,初始進程配置信息,包括是否綁定終端、運行可執行文件的工作目錄、環境變量配置、可執行文件及執行參數、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的執行流程,整個分析過程如下圖:

OCI標準和runC原理解讀

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()函數,通過管道從父進程接收各種配置參數。然后對容器進行如下配置:

1.如果用戶指定,則將init進程加入其指定的namespace。

2.設置進程的會話ID。

3.初始化網絡設備。

4.對指定目錄下的文件系統進行掛載,并切換根目錄到新掛載的文件系統下。設置hostname,加載profile信息。

最后使用exec系統調用來執行用戶所指定的在容器中運行的程序。

3. 熱遷移的配置與原理簡介

3.1 熱遷移簡介

所謂熱遷移就是將一個容器進行Checkpoint操作,并獲得一系列文件,使用這一系列文件可以在本機或者其他主機上進行容器的Restore 工作。目前,在runC中使用了CRIU作為熱遷移的工具,并實現了對容器的Checkpoint和Restore功能。簡要的過程如下圖所示。

OCI標準和runC原理解讀

3.2 runC熱遷移原理簡介

在runC中熱遷移的工作主要是調用CRIU (Checkpoint and Restore in Userspace)來完成。CIRU負責凍結進程,并將作為一系列文件存儲在硬盤上。并負責使用這些文件還原這個被凍結的進程。

runC使用SWRK模式來調用criu。這種模式是criu另外兩種模式CLI和RPC的結合體,允許用戶需要的時候像使用命令行工具一樣運行criu,并接受用戶遠程調用的請求。

runC主要通過如下兩個步驟完成熱遷移工作。

1. 生成container

通過state.json或者配置文件*.json來生成container結構體。

2.使用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文件中文件系統的可讀寫性設置為可讀。

部分配置如下圖所示。

OCI標準和runC原理解讀

正確安裝CRIU及其相關依賴并且對config.json做出以上的修改后就可以使用runC內置的命令對容器進行熱遷移了。

高相林, 浙江大學SEL實驗室 碩士研究生,目前在云平臺團隊從事科研和開發工作。浙大團隊對PaaS,Docker,大數據和主流開源云計算技術有深入的研究和二次開發經驗,團隊現聯合社區將部分技術文章貢獻出來,希望能對讀者有所幫助。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!