CRI Shimv2:一種 Kubernetes 集成容器運行時的新思路

Kubernetes 項目目前的重點發展方向,是爲開發者和使用者暴露更多的接口和可擴展機制,將更多的用戶需求下放到社區來完成。其中,發展最爲成熟也最爲重要的一個接口就是 CRI。在過去的一年裏,已經有大量的容器項目比如 Docker,containerd,runC 等通過 CRI 成爲了 Kubernetes 的標準容器運行時。

而在 2018 年,由 containerd 社區主導的 shimv2 API 的出現,在 CRI 的基礎上,爲用戶集成自己的容器運行時帶來了更加成熟和方便的實踐方法。本文整理自張磊在 KubeCon + CloudNativeCon 2018 現場的演講,將爲你分享關於 Kubernetes 接口化設計、CRI、容器運行時、shimv2、RuntimeClass 等關鍵技術特性的設計與實現,並以 KataContainers 爲例,爲你演示上述技術特性的使用方法。

Kubernetes 的工作原理

其實大家都知道 Kubernetes 最上面是一層 Control Panel ,它也被很多人稱之爲 Master 節點。當把 workload, 也就是應用提交給 Kubernetes 之後,首先爲你做事情的是 API server,它會把你的 應用存到 etcd 裏,以 API 對象的方式存到 etcd 中去。

而 Kubernetes 中負責編排的是 Controller manager,一堆 controller 通過控制循環在運行。通過這個控制循環來做編排工作,幫你去創建出這些應用所需要的 Pod,注意不是容器,是 Pod。

而一旦一個 Pod 出現之後,Scheduler 會 watch 新 Pod 的變化。如果他發現有一個新的 Pod 出現,Schduler 會幫你去把所有調度算法都 run 一遍,把 run 到的結果:就是一個 Node 的名字,寫在Pod 對象 NodeName 字段上面,就是一個所謂的 bind 操作,然後把 bind 的結果寫回到 etcd 裏去,這就是所謂的 Schduler 工作過程。所以 Control Panel 忙活這麼一圈下來,最後得到的結果是什麼呢?你的一個 Pod 跟一個 Node 綁定(bind)在了一起,就是所謂 Schdule 了。

而 Kubelet 呢?它是運行在所有節點上。Kubelet 會 watch 所有 Pod 對象的變化,當它發現一個 Pod 與一個 Node 綁定在一起的時,並且它又發現這個被綁定的 Node 是它自己,那麼 Kubelet 就會幫你去接管接下來的所有事情。

如果你看一下 Kubelet ,看看它在做什麼呢?很簡單,其實當 Kubelet 拿到這個信息之後,他是去call你運行在每個機器上的 Containerd 進程,去 run 這個 Pod 裏的每一個容器。

這時候,Containerd 幫你去 call runC 所以最後其實是 runC 幫你去創建起來這些 namespace、Cgroup,是它去幫你chroot ,“搭”出來所謂的應用和需要的容器。這就是整個 Kubernetes 工作的簡單原理。

Linux Container

所以這個時候你可能會提出一個問題:什麼是容器?其實容器非常簡單,我們平常所說這個容器就是 Linux 容器,你可以把 Linux 容器分爲兩部分:第一個是 容器Runtime,第二個是容器鏡像。

所謂的 Runtime 部分就是你所運行進程的動態視圖和資源邊界,所以它是由 Namespace 和 Cgroup 爲你構建出來的。而對於 Image(鏡像),你可以把它理解爲是你想要運行的程序的靜態視圖,所以它其實是你的程序+數據+所有的依賴+所有的目錄文件組成一個壓縮包而已。

而這些壓縮包被以 union mount 的方式 mount 在一起的時候,我們稱之爲 rootfs 。rootfs 就是整個 process 的靜態視圖,他們看到這個世界就這樣子,所以這是 Linux Container。

KataContainer

可今天我們還要聊另外一種容器,它與前面 Linux Container 截然不同。他的 容器Runtime 是用 hypervisor 實現的,是用 hardware virtualization 實現的,像個虛擬機一樣。所以每一個像這樣的 KataContainer 的 Pod,都是一個輕量級虛擬機,它是有完整的 Linux 內核。

所以我們經常說 KataContainer 與 VM 一樣能提供強隔離性,但由於它的優化和性能設計,它擁有與容器項媲美的敏捷性。這個一點稍後會強調,而對於鏡像部分, KataContainer 與 Docker 這些項目沒有任何不同,它使用的是標準 Linux Continer 容器,支持標準的 OCR Image 所以這一部分是完全一樣的。

容器安全

可是你可能會問,爲什麼我們會有 KataContainer 這種項目? 其實很簡單,因爲我們關心安全這個事,比如很多金融的場景、加密的場景,甚至現在區塊鏈很多場景下,都需要一個安全的容器Runtime,所以這是我們強調 KataContainer 的一個原因。

如果你現在正在使用 Docker, 我問一個問題:你怎樣才能安全地使用 Docker?你可能會有很多套路去做。比如說你會 drop 一些 Linux capibility,你可以去指定 Runtime 可以做什麼,不能做什麼。第二個你可以去 read-only mount points 。第三,你可以使用 SELinux 或者 AppArmor 這些工具把容器給保護起來。還有一種方式是可以直接拒絕一些 syscalls,可以用到 SECCOMP。

但是我需要強調的是所有這些操作都會在你的 容器和 Host 之間引入新的層,因爲它要去做過濾,攔截 syscalls,所以這個部分搭的層越多,容器性能越差,它一定是有額外的負面性能損耗的。

更重要的是,做這些事情之前你要想清楚到底應該幹什麼,到底應該 drop 哪些 syscalls,這是需要具體問題具體分析的,那麼這時候我應該怎麼去跟我的用戶講如何做這件事情?

所以,這些事情說起來很簡單,但實際執行起來很少有人知道到底該怎麼去做。所以在 99.99% 的情況下,大多數人都是把容器運行到虛擬機裏去的,尤其在公有云場景下。

而對於 KataContainer 這種項目來說,它由於使用了與虛擬機一樣的 hardware virualization,是有獨立內核的,所以這個時候它提供的 isolation 是完全可信任的,就與你信任 VM 是一樣的。

更重要的是,由於現在每一個 Pod 裏是有一個 Independent Kernel,跟個小虛擬機一樣,所以這時候就允許容器運行的 Kernel 版本跟 Host machine 適應是完全不一樣。這是完全 OK 的,就與你在在虛擬機中做這件事一樣,所以這就是爲什麼我會強調 KataContainers 的一個原因,因爲它提供了安全和多租戶的能力。

Kubernetes+KataContainers

所以也就很自然會有一個需求:我們怎麼把 KataContainer 運行在 Kubernetes 裏? 這個時候我們還是先來看 Kubelet 在做什麼事情。Kubelet 要想辦法像 call Containerd 一樣去 call KataContainer,然後由 KataContainer 負責幫忙把 hypervisor 這些東西搭建起來,把小VM 運行起來。這個時候就要需要想怎麼讓 Kubernetes 能合理的操作 KataContainers。

Container Runtime Interface

對於這個訴求,就關係到了我們之前一直在社區推進的 Container Runtime Interface ,我們叫它 CRI。CRI 的作用其實只有一個:描述對於 Kubernetes 來說,一個 Container 應該有哪些操作,每個操作有哪些參數。這就是 CRI 的一個設計原理。但需要注意的是,CRI 是一個以容器爲核心的 API,它裏面沒有 Pod 的這個概念。

我們爲什麼要這麼設計呢?很簡單,我們不希望像 Docker 這樣的項目,必須得懂什麼是 Pod,暴露出 Pod 的 API,這是不合理的訴求。Pod 永遠都是一個 Kubernetes 的編排概念,這跟容器沒有關係,所以這就是爲什麼我們要把這個 API 做成 Containerd -centric。

另外一個原因出於 maintain 的考慮,因爲如果現在, CRI 裏有 Pod 這個概念,那麼接下來任何一個 Pod feature 的變更都有可能會引起 CRI 的變動,對於一個接口來說,這樣的維護代價是比較大的。所以如果你細看一下 CRI,你會發現它其實定了一些非常普遍的操作容器接口。

在這裏,我可以把 CRI 大致它分爲容器和 Sandbox。Sandbox 用來描述通過什麼樣的機制實現 Pod ,所以它是 Pod這個概念真正跟容器項目相關的字段。對於 Docker 或 Linux 容器來說,它其實匹配到最後運行起來的是一個叫 infra container 的容器,就是一個極小的容器,這個容器用來 hold 整個 Pod 的 Node 和 Namespace。

不過, Kubernetes 如果用 Linux Container Runtim, 比如 Docker 的話,不會提供 Pod級別的 isolation,除了一層 Pod level cgroups 。這是一個不同點。因爲,如果你用 KataContainers 的話,KataContaniners 會在這一步創建一個輕量級的虛擬機。

接下來到容器的 API,對於 Docker 來說它能在宿主機上啓動用戶容器,但對 Kata 來說不是這樣的,它會在前面的 Pod 對應的輕量級虛擬機裏面,也就在前面創建的 Sandbox 裏面創建這些用戶容器所需要 Namespace ,而不會再起新的容器。

所以有了這樣一個機制,當上面 Contol Panel 完成它的工作之後,它說我把 Pod 調度好了,這時候 Kubelet 這邊啓動或創建這個 Pod 的時候一路走下去,最後一步纔會去 call 我們這個所謂 CRI。在此之前,在 Kubelet 或者 Kubernetes 這是沒有所謂 Containers runtime 這個概念的。

所以走到這一步之後,如果你用 Docker 的話,那麼 Kubernetes 裏負責響應這個 CRI 請求的是 Dockershim。但如果你用的不是 Docker 的話一律都要去走一個叫 remote 的模式,就是你需要寫一個 CRI Shim,去 serve 這個 CRI 請求,這就是我們今天所討論下一個主題。

CRI Shim 如何工作?

CRI Shim 可以做什麼?它可以把 CRI 請求翻譯成 Runtime API。我舉個例子,比如說現在有個 Pod 裏有一個 A 容器和有個 B 容器,這時候我們把這件事提交給 Kubernetes 之後,在 Kubelet 那一端發起的 CRI code 大概是這樣的序列:首先它會 run Sandbox foo,如果是 Docker 它會起一個 infra 容器,就是一個很小的容器叫 foo,如果是 Kata 它會給你起一個虛擬機叫 foo,這是不一樣的。

所以接下來你啓動容器 A 和 B 的時候,在 Docker 裏面是起兩個容器,但在 Kata 裏面是在小虛擬機裏面,在這個Sandbox 裏面起兩個小 NameSpace,這是不一樣的。所以你把這一切東西總結一下,你會發現我現在要把 Kata 運行在 Kubernetes 裏頭,所以我要做工作,在這一步要需要去做這個 CRI shim,我就想辦法給 Kata 作一個 CRI shim。

而我們能夠想到一個方式,我能不能重用現在的這些 CRI shim。重用在哪些?比如說 CRI containerd 這個項目它就是一個 containerd 的 CRI shim,它可以去響應 CRI 的請求過來,所以接下來我能不能把這些情況翻譯成對 Kata 這些操作,所以這個是可以的,這也是我們將用一種方式,就是把 KataContainers 接到我的 Containerd 後面。

這時候它的工作原理大概這樣這個樣子,Containerd 它有一個獨特設計,會爲每一個容器起個 Contained shim。運行一下之後會看宿主機裏面運行了一片Containerd shim,一個一個對上去。

而這時候由於 Kata 是一個有 Sandbox 概念的容器 runtime,所以 Kata 需要去匹配Shim 與 Kata 之間的關係,所以 Kata 做一個 Katashim。把這些東西對起來,就把你的 Contained 的處理的方式翻譯成對 kata 的 request,這是我們之前的一個方式。

但是你能看到這裏其實有些問題的,最明顯的一個問題在於 對 Kata 或 gVisor 來說,他們都是有實體的 Sandbox 概念的,而有了 Sandbox 概念後,它就不應該去再去給他的每一個容器啓動有一個 shim,因爲這給我們帶來很大的額外性能損耗。我們不希望每一個容器都去匹配一個 shim,而是一個 Sandbox 匹配一個 shim。

另外,你會發現 CRI 是服務於 Kubernetes 的,而且它呈現向上彙報的狀態,它是幫助 Kubernetes 的,但是它不幫助 Container runtime。所以說當做集成時候,會發現尤其對於 VM gVisor\KataContainer 來說,它與 CRI 的很多假設或者是 API 的寫法上是不對應的。所以你的集成工作會比較費勁,這是一個不 match 的狀態。

最後,我們維護起來非常困難。因爲由於有了 CRI 之後,比如 RedHat 擁有自己的 CRI 實現叫 cri-o,他們和 containerd 在本質上沒有任何區別,跑到最後都是靠 runC 起容器,爲什麼要這種東西?

我們不知道,但是我作爲 Kata維護者,我需要給他們兩個分別寫兩部分的 integration 把 Kata 集成進去。這就很麻煩,意味着有 100 種CRI 我就要寫 100 個集成,而且他們的功能全部都是重複的。

Containerd ShimV2

所以我給大家提出的這個東西叫做 Containerd ShimV2。前面我們說過 CRI決定的是 Runtime 和 Kubernetes 之間的關係,那麼我們現在能不能再有一層更細緻的 API 來決定CRI Shim 跟下面的 Runtime 之間真正的接口是什麼樣的?

這就是 ShimV2 出現的原因,它是一層 CRI shim 到 Containerd run time 之間的標準接口,所以前面我直接從 CRI 到 Containerd 到 runC,現在不是。我們是從 CRI 到 Containerd 到 ShimV2,然後 ShimV2 再到 RunC 再到 KataContainer。這麼做有什麼好處?

最大的區別在於:在這種方式下,你可以爲每一個 Pod 指定一個 Shim。因爲在最開始的時候,Containerd 直接啓動了一個 Containerd Shim 做響應,但我們新的 API 是這樣寫的,是 Containerd Shim start 或者 stop。所以這個 start 和 stop 操作怎麼去實現是你要做的事情。

而現在,我作爲一個 Kata維護者我就可以這麼實現。在創建Sandbox 的時候,我啓動一個 Containerd Shim。但是當我下一步是 call API 的時候,我就不再起了,而是 reuse,我重用爲你創建好的這個 Sandbox,這就爲你的實現提供了很大的自由度。

所以這時候你會發現整個實現的方式變了,這時候 Containerd 用過來之後,它不再去關心每個容器起Containerd ShimV,而是由你自己去實現。我的實現方式是我只在 Sandbox 時候,去創建 Containerd Shimv,而接下來整個後面的 容器級別的操作,全部走到containerd shimV2 裏,重用Sandbox,所以這個跟前面的實踐就出現很大的不同。

所以現在去總結一下這個圖的話,你發現我們實驗方式是這樣的:

首先,你還是用原來的 CRI Containerd,只不過現在裝的是 runC,你現在再裝一個 katacontainer 放在機器上面。接下來Kata 會寫一個實現叫 kata-Containerd-Shimv2。所以前面要寫一大坨 CRI 的東西,現在不用了。

現在,我們只關注怎麼把 Containerd 對接在 kata container 上面,就是所謂的實現 Shimv2 API。而具體到我們這要做的事情上,其實就是這樣一系列運行一個容器相關的 API。

比如說我可以去 creat start 一類,這些操作全部映射在我 Shimv2 上面去實現,而不是說現在考慮怎麼映射、實現 CRI,這個自由度由於之前太大,造成了我們現在的一個局面,就有一堆 CRI Shim 可以用。這其實是一個不好的事情。有很多政治原因,有很多非技術原因,這都不是我們應該作爲技術人員應該關心的事情,你現在只需要想我怎麼去跟 Shimv2 對接就好了。


作者簡介

張磊,Kubernetes 社區資深成員與項目維護者。他是 Kubernetes 項目的成員和聯合維護者,主要聚焦容器運行時接口(CRI)、調度、資源管理和基於管理程序的容器運行時間。他曾是 KataContainers/Hyper 團隊成員,微軟研究院(MSR)訪問學者。現在阿里巴巴集團擔任高級技術專家,共同負責 Kubernetes 上游和阿里巴巴大型集羣管理系統的工程工作。張磊是 KubeCon 大會備受歡迎的演講者,也曾在 LinuxCon 和 OpenStack Summit 發表過多次技術演講。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章