一文搞懂 Docker、Containerd、RunC 間的聯(lián)系和區(qū)別
什么是RunC
Docker、Google、CoreOS 和其他供應(yīng)商創(chuàng)建了開放容器計劃 (OCI),目前主要有兩個標準文檔:容器運行時標準 (runtime spec)和 容器鏡像標準(image spec)。
OCI 對容器 runtime 的標準主要是指定容器的運行狀態(tài),和 runtime 需要提供的命令。下圖可以是容器狀態(tài)轉(zhuǎn)換圖:
- init 狀態(tài):這個是我自己添加的狀態(tài),并不在標準中,表示沒有容器存在的初始狀態(tài)
- creating:使用 create 命令創(chuàng)建容器,這個過程稱為創(chuàng)建中
- created:容器創(chuàng)建出來,但是還沒有運行,表示鏡像和配置沒有錯誤,容器能夠運行在當前平臺
- running:容器的運行狀態(tài),里面的進程處于 up 狀態(tài),正在執(zhí)行用戶設(shè)定的任務(wù)
- stopped:容器運行完成,或者運行出錯,或者 stop 命令之后,容器處于暫停狀態(tài)。這個狀態(tài),容器還有很多信息保存在平臺中,并沒有完全被刪除
Runc的來歷
RunC 是從 Docker 的 libcontainer 中遷移而來的,實現(xiàn)了容器啟停、資源隔離等功能。Docker將RunC捐贈給 OCI 作為OCI 容器運行時標準的參考實現(xiàn)。Docker 默認提供了 docker-runc 實現(xiàn)。事實上,通過 containerd 的封裝,可以在 Docker Daemon 啟動的時候指定 RunC的實現(xiàn)。最初,人們對 Docker 對 OCI 的貢獻感到困惑。他們貢獻的是一種“運行”容器的標準方式,僅此而已。它們不包括鏡像格式或注冊表推/拉格式。當你運行一個 Docker 容器時,這些是 Docker 實際經(jīng)歷的步驟:
- 下載鏡像
- 將鏡像文件解開為bundle文件,將一個文件系統(tǒng)拆分成多層
- 從bundle文件運行容器
Docker標準化的僅僅是第三步。在此之前,每個人都認為容器運行時支持Docker支持的所有功能。最終,Docker方面澄清:原始OCI規(guī)范指出,只有“運行容器”的部分組成了runtime。這種“概念失聯(lián)”一直持續(xù)到今天,并使“容器運行時”成為一個令人困惑的話題。希望我能證明雙方都不是完全錯誤的,并且在本文中將廣泛使用該術(shù)語。RunC 就可以按照這個 OCI 文檔來創(chuàng)建一個符合規(guī)范的容器,既然是標準肯定就有其他 OCI 實現(xiàn),比如 Kata、gVisor 這些容器運行時都是符合 OCI 標準的。
怎么使用 runc
- create the bundle
- $ mkdir -p /mycontainer/rootfs
- # [ab]use Docker to copy a root fs into the bundle
- $ docker export $(docker create busybox) | tar -C /mycontainer/rootfs -xvf -
- # create the specification, by default sh will be the entrypoint of the container
- $ cd /mycontainer
- $ runc spec
- # launch the container
- $ sudo -i
- $ cd /mycontainer
- $ runc run mycontainerid
- # list containers
- $ runc list
- # stop the container
- $ runc kill mycontainerid
- # cleanup
- $ runc delete mycontainerid
在命令行中使用 runc,我們可以根據(jù)需要啟動任意數(shù)量的容器。但是,如果我們想自動化這個過程,我們需要一個容器管理器。為什么這樣?想象一下,我們需要啟動數(shù)十個容器來跟蹤它們的狀態(tài)。其中一些需要在失敗時重新啟動,需要在終止時釋放資源,必須從注冊表中提取圖像,需要配置容器間網(wǎng)絡(luò)等等。就需要有Low-Level和High-Level容器運行時,runc就是Low-Level實現(xiàn)的實現(xiàn)。
Low-Level和High-Level容器運行時
當人們想到容器運行時,可能會想到一系列示例;runc、lxc、lmctfy、Docker(容器)、rkt、cri-o。這些中的每一個都是為不同的情況而構(gòu)建的,并實現(xiàn)了不同的功能。有些,如 containerd 和 cri-o,實際上使用 runc 來運行容器,在High-Level實現(xiàn)鏡像管理和 API。與 runc 的Low-Level實現(xiàn)相比,可以將這些功能(包括鏡像傳輸、鏡像管理、鏡像解包和 API)視為High-Level功能。考慮到這一點,您可以看到容器運行時空間相當復雜。每個運行時都涵蓋了這個Low-Level到High-Level頻譜的不同部分。這是一個非常主觀的圖表:
因此,從實際出發(fā),通常只專注于正在運行的容器的runtime通常稱為“Low-Level容器運行時”,支持更多高級功能(如鏡像管理和gRPC / Web API)的運行時通常稱為“High-Level容器運行時”,“High-Level容器運行時”或通常僅稱為“容器運行時”,我將它們稱為“High-Level容器運行時”。值得注意的是,Low-Level容器運行時和High-Level容器運行時是解決不同問題的、從根本上不同的事物。
- Low-Level容器運行時:容器是通過Linux nanespace和Cgroups實現(xiàn)的,Namespace能讓你為每個容器提供虛擬化系統(tǒng)資源,像是文件系統(tǒng)和網(wǎng)絡(luò),Cgroups提供了限制每個容器所能使用的資源的如內(nèi)存和CPU使用量的方法。在最低級別的運行時中,容器運行時負責為容器建立namespaces和cgroups,然后在其中運行命令,Low-Level容器運行時支持在容器中使用這些操作系統(tǒng)特性。目前來看低級容器運行時有:runc :我們最熟悉也是被廣泛使用的容器運行時,代表實現(xiàn)Docker。runv:runV 是一個基于虛擬機管理程序(OCI)的運行時。它通過虛擬化 guest kernel,將容器和主機隔離開來,使得其邊界更加清晰,這種方式很容易就能幫助加強主機和容器的安全性。代表實現(xiàn)是kata和Firecracker。runsc:runsc = runc + safety ,典型實現(xiàn)就是谷歌的gvisor,通過攔截應(yīng)用程序的所有系統(tǒng)調(diào)用,提供安全隔離的輕量級容器運行時沙箱。截止目前,貌似并不沒有生產(chǎn)環(huán)境使用案例。wasm : Wasm的沙箱機制帶來的隔離性和安全性,都比Docker做的更好。但是wasm 容器處于草案階段,距離生產(chǎn)環(huán)境尚有很長的一段路。
- High-Level容器運行時:通常情況下,開發(fā)人員想要運行一個容器不僅僅需要Low-Level容器運行時提供的這些特性,同時也需要與鏡像格式、鏡像管理和共享鏡像相關(guān)的API接口和特性,而這些特性一般由High-Level容器運行時提供。就日常使用來說,Low-Level容器運行時提供的這些特性可能滿足不了日常所需,因為這個緣故,唯一會使用Low-Level容器運行時的人是那些實現(xiàn)High-Level容器運行時以及容器工具的開發(fā)人員。那些實現(xiàn)Low-Level容器運行時的開發(fā)者會說High-Level容器運行時比如containerd和cri-o不像真正的容器運行時,因為從他們的角度來看,他們將容器運行的實現(xiàn)外包給了runc。但是從用戶的角度來看,它們只是提供容器功能的單個組件,可以被另一個的實現(xiàn)替換,因此從這個角度將其稱為runtime仍然是有意義的。即使containerd和cri-o都使用runc,但是它們是截然不同的項目,支持的特性也是非常不同的。dockershim, containerd 和cri-o都是遵循CRI的容器運行時,我們稱他們?yōu)楦邔蛹夁\行時(High-level Runtime)。
Kubernetes 只需支持 containerd 等high-level container runtime即可。由containerd 按照OCI 規(guī)范去對接不同的low-level container runtime,比如通用的runc,安全增強的gvisor,隔離性更好的runv。
containerd
與RunC_一樣_,我們又可以在這里看到一個docker公司的開源產(chǎn)品containerd曾經(jīng)是開源docker項目的一部分。盡管_containerd_是另一個自給自足的軟件。
- 一方面,它稱自己為容器運行時,但是與運行時__RunC_不同_。不僅_containerd_和_runc_的職責不同,組織形式也不同。顯然_runc_是只是一個命令行工具,_containerd_是一個長期居住守護進程。_runc_的實例不能超過底層容器進程。通常它在create調(diào)用時開始它的生命,然后只是在容器的 rootfs 中的指定文件去運行。
- 另一方面,_containerd _可以管理超過數(shù)千個_runc_容器。它更像是一個服務(wù)器,它偵聽傳入請求以啟動、停止或報告容器的狀態(tài)。在引擎蓋下_containerd_使用RunC。然而,_containerd_不僅僅是一個容器生命周期管理器。它還負責鏡像管理(從注冊中心拉取和推送鏡像,在本地存儲鏡像等)、跨容器網(wǎng)絡(luò)管理和其他一些功能。
image.png
containerd 是一個工業(yè)級標準的容器運行時,它強調(diào)簡單性、健壯性和可移植性,containerd 可以負責干下面這些事情:
- 管理容器的生命周期(從創(chuàng)建容器到銷毀容器)
- 拉取/推送容器鏡像
- 存儲管理(管理鏡像及容器數(shù)據(jù)的存儲)
- 調(diào)用 runc 運行容器(與 runc 等容器運行時交互)
- 管理容器網(wǎng)絡(luò)接口及網(wǎng)絡(luò)
上圖是 Containerd 整體的架構(gòu)。由下往上,Containerd支持的操作系統(tǒng)和架構(gòu)有 Linux、Windows 以及像 ARM 的一些平臺。在這些底層的操作系統(tǒng)之上運行的就是底層容器運行時,其中有上文提到的runc、gVisor 等。在底層容器運行時之上的是Containerd 相關(guān)的組件,比如 Containerd 的 runtime、core、API、backend、store 還有metadata 等等。構(gòu)筑在 Containerd 組件之上以及跟這些組件做交互的都是 Containerd 的 client,Kubernetes 跟 Containerd 通過 CRI 做交互時,本身也作為 Containerd 的一個 client。Containerd 本身有提供了一個 CRI,叫 ctr,不過這個命令行工具并不是很好用。
在這些組件之上就是真正的平臺,Google Cloud、Docker、IBM、阿里云、微軟云還有RANCHER等等都是,這些平臺目前都已經(jīng)支持 containerd, 并且有些已經(jīng)作為自己的默認容器運行時了。
從 k8s 的角度看,選擇 containerd作為運行時的組件,它調(diào)用鏈更短,組件更少,更穩(wěn)定,占用節(jié)點資源更少。
Docker
Docker 于 2013 年發(fā)布,解決了開發(fā)人員在端到端運行容器時遇到的許多問題。這里是他包含的所有東西:
- 容器鏡像格式
- 一種構(gòu)建容器鏡像的方法(Dockerfile/docker build);
- 一種管理容器鏡像(docker image、docker rm等);
- 一種管理容器實例的方法(docker ps, docker rm 等);
- 一種共享容器鏡像的方法(docker push/pull);
- 一種運行容器的方式(docker run);
當時,Docker 是一個單體系統(tǒng)。但是,這些功能中沒有一個是真正相互依賴的。這些中的每一個都可以在可以一起使用的更小、更集中的工具中實現(xiàn)。每個工具都可以通過使用一種通用格式、一種容器標準來協(xié)同工作。從 Docker 1.11 之后,Docker Daemon 被分成了多個模塊以適應(yīng) OCI 標準。拆分之后,結(jié)構(gòu)分成了以下幾個部分。圖片
其中,containerd 獨立負責容器運行時和生命周期(如創(chuàng)建、啟動、停止、中止、信號處理、刪除等),其他一些如鏡像構(gòu)建、卷管理、日志等由 Docker Daemon 的其他模塊處理。
Docker 的模塊塊擁抱了開放標準,希望通過 OCI 的標準化,容器技術(shù)能夠有很快的發(fā)展。
現(xiàn)在創(chuàng)建一個docker容器的時候,Docker Daemon 并不能直接幫我們創(chuàng)建了,而是請求 containerd 來創(chuàng)建一個容器。當containerd 收到請求后,也不會直接去操作容器,而是創(chuàng)建一個叫做 containerd-shim 的進程。讓這個進程去操作容器,我們指定容器進程是需要一個父進程來做狀態(tài)收集、維持 stdin 等 fd 打開等工作的,假如這個父進程就是 containerd,那如果 containerd 掛掉的話,整個宿主機上所有的容器都得退出了,而引入 containerd-shim 這個墊片就可以來規(guī)避這個問題了,就是提供的live-restore的功能。這里需要注意systemd的 MountFlags=slave。
然后創(chuàng)建容器需要做一些 namespaces 和 cgroups 的配置,以及掛載 root 文件系統(tǒng)等操作。runc 就可以按照這個 OCI 文檔來創(chuàng)建一個符合規(guī)范的容器。
真正啟動容器是通過 containerd-shim 去調(diào)用 runc 來啟動容器的,runc 啟動完容器后本身會直接退出,containerd-shim 則會成為容器進程的父進程, 負責收集容器進程的狀態(tài), 上報給 containerd, 并在容器中 pid 為 1 的進程退出后接管容器中的子進程進行清理, 確保不會出現(xiàn)僵尸進程。containerd,containerd-shim和容器進程(即容器主進程)三個進程,是有依賴關(guān)系的??梢詤⒖肌禼ontainerd,containerd-shim和runc的依存關(guān)系》[1],查看怎么保證live-restore的功能的。