Docker背后的標準化容器執行引擎 - runC
隨著容器技術發展的愈發火熱,Linux基金會于2015年6月成立OCI(Open Container Initiative)組 織,旨在圍繞容器格式和運行時制定一個開放的工業化標準。該組織一成立便得到了包括谷歌、微軟、亞馬遜、華為等一系列云計算廠商的支持。而runC就是 Docker貢獻出來的,按照該開放容器格式標準(OCF, Open Container Format)制定的一種具體實現。本文作者及浙大團隊將在接下來的容器系列文章中,從架構和源碼層面詳細解讀這個開源項目的設計思想和實現原理,敬請關 注。
1. 容器格式標準是什么?
制定容器格式標準的宗旨概括來說就是不受上層結構的綁定,如特定的客戶端、編排棧等,同時也不受特定的供應商或項目的綁定,即不限于某種特定操作系統、硬件、CPU架構、公有云等。
該標準目前由libcontainer和appc的項目負責人(maintainer)進行維護和制定,其規范文檔就作為一個項目在GitHub上維護,地址為https://github.com/opencontainers/specs。
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工作原理與實現方式
runC的前身實際上是Docker的libcontainer項目,筆者曾經寫過一篇文章《Docker背后的容器管理——Libcontainer深度解析》專門對libcontainer進行源碼分析和解讀,感興趣的讀者可以先閱讀一下,目前runC也是對libcontainer包的調用,libcontainer包變化并不大。所以此文將不再花費太多筆墨分析其源碼,我們將著重講解其中的變化。
2.1 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 rootfs tar -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/src cd github.com/opencontainers git clone https://github.com/opencontainers/runc cd runc make sudo make install
最后執行
runc start
你就啟動了一個容器了。
可以看到,我們對容器的所有定義,均包含在兩份配置文件中,一份簡略的config.json配置文件類似如下,已用省略號省去部分信息,完整的可以查看官方github。
{ "version": "0.1.0", "platform": { "os": "linux", "arch": "amd64" }, "process": { "terminal": true, "user": { "uid": 0, "gid": 0, "additionalGids": null }, "args": [ "sh" ], "env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM=xterm" ], "cwd": "" }, "root": { "path": "rootfs", "readonly": true }, "hostname": "shell", "mounts": [ { "name": "proc", "path": "/proc" }, …… { "name": "cgroup", "path": "/sys/fs/cgroup" } ], "linux": { "capabilities": [ "CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE" ] } }
各部分表示的意思在1.2節中已經講解,針對具體的內容我們可以看到,版本是0.10,該配置文件針對的是AMD64架構下的Linux系統,啟動容器后執行的命令就是sh,配置的環境變量有兩個,是PATH和TERM,啟動后user的uid和gid都為0,表示進入后是root用戶。cwd項為空表示工作目錄為當前目錄。capabilities能力方面則使用白名單的形式,從配置上可以看到只允許三個能力,功能分別為允許寫入審計日志、允許發送信號、允許綁定socket到網絡端口。
一份簡略的runtime.json配置則如下,同樣用省略號省去了部分內容:
{ "mounts": { "proc": { "type": "proc", "source": "proc", "options": null }, …… "cgroup": { "type": "cgroup", "source": "cgroup", "options": [ "nosuid", "noexec", "nodev", "relatime", "ro" ] } }, "hooks": { "prestart": null, "poststop": null }, "linux": { "uidMappings": null, "gidMappings": null, "rlimits": [ { "type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024 } ], "sysctl": null, "resources": { "disableOOMKiller": false, "memory": { "limit": 0, "reservation": 0, "swap": 0, "kernel": 0, "swappiness": -1 }, "cpu": { "shares": 0, "quota": 0, "period": 0, "realtimeRuntime": 0, "realtimePeriod": 0, "cpus": "", "mems": "" }, "pids": { "limit": 0 }, "blockIO": { "blkioWeight": 0, "blkioWeightDevice": "", "blkioThrottleReadBpsDevice": "", "blkioThrottleWriteBpsDevice": "", "blkioThrottleReadIopsDevice": "", "blkioThrottleWriteIopsDevice": "" }, "hugepageLimits": null, "network": { "classId": "", "priorities": null } }, "cgroupsPath": "", "namespaces": [ { "type": "pid", "path": "" }, { "type": "network", "path": "" }, { "type": "ipc", "path": "" }, { "type": "uts", "path": "" }, { "type": "mount", "path": "" } ], "devices": [ { "path": "/dev/null", "type": 99, "major": 1, "minor": 3, "permissions": "rwm", "fileMode": 438, "uid": 0, "gid": 0 }, …… { "path": "/dev/urandom", "type": 99, "major": 1, "minor": 9, "permissions": "rwm", "fileMode": 438, "uid": 0, "gid": 0 } ], "apparmorProfile": "", "selinuxProcessLabel": "", "seccomp": { "defaultAction": "SCMP_ACT_ALLOW", "syscalls": [] }, "rootfsPropagation": "" } }
可以看到基本的幾項配置分別為掛載點信息、啟動前與停止后hooks腳本、然后就是針對Linux的特性支持的諸如用戶uid/gid綁 定,rlimit配置、namespace設置、cgroups資源限額、設備權限配置、apparmor配置項目錄、selinux標記以及 seccomp配置。其中,namespaces和cgroups筆者均有寫文章詳細介紹過。
再下面的工作便都由libcontainer完成了,大家可以閱讀這個系列前一篇文章《Docker背后的容器管理——Libcontainer深度解析》或者購買書籍《Docker容器與容器云》,里面均有詳細介紹。
簡單來講,有了配置文件以后,runC就開始借助libcontainer處理以下事情:
- 創建libcontainer構建容器需要使用的進程,稱為Process;
- 設置容器的輸出管道,這里使用的就是daemon提供的pipes;
- 使用名為Factory的工廠類,通過factory.Create(<容器ID>, <填充好的容器模板container>)創建一個邏輯上的容器,稱為Container;
- 執行Container.Start(Process)啟動物理的容器;
- runC等待Process的所有工作都完成。
可以看到,具體的執行者是libcontainer,它是對容器的一層抽象,它定義了Process和Container來對應Linux中“進 程”與“容器”的關系。一旦上述物理的容器創建成功,其他調用者就可以通過ID獲取這個容器,接著使用Container.Stats得到容器的資源使用 信息,或者執行Container.Destory來銷毀這個容器。
綜上,runC實際上是把配置交給libcontainer,然后由libcontainer完成容器的啟動,而libcontainer中最主要 的內容是Process、Container以及Factory這3個邏輯實體的實現原理。runC或者其他調用者只要依次執行“使用Factory創建 邏輯容器Container”、“用Process啟動邏輯容器Container”即可。
3. 總結
本文從OCI組織的成立開始講起,描述了開放容器格式的標準及其宗旨,這其實就是runC的由來。繼而針對具體的runC特性及其啟動進行了詳細介 紹。筆者在后續的文章中還將針對runC中諸如CRIU熱遷移、selinux、apparmor及seccomp配置等特性進行具體的介紹。可以看到 OCI的成立表明了社區及各大廠商對容器技術的肯定以及加快容器技術發展進步的強烈決心,相信在不久的將來,符合OCI標準的開放容器項目會越來越多,容 器技術將更加欣欣向榮地不斷前進。
4. 作者簡介
孫健波,浙江大學SEL實驗室碩士研究生,《Docker容器與容器云》主要作者之一,目前在云平臺團隊從事科研和開發工作。浙大團隊對PaaS、Docker、大數據和主流開源云計算技術有深入的研究和二次開發經驗,團隊現將部分技術文章貢獻出來,希望能對讀者有所幫助。
來自:http://www.infoq.com/cn/articles/docker-standard-container-execution-engine-runc