Kubernetes 項目目前的重點發展方向,是爲開發者和使用者暴露更多的接口和可擴展機制,將更多的用戶需求下放到社區來完成。其中,發展最爲成熟也最爲重要的一個接口就是 CRI。2018 年,由 containerd 社區主導的 shimv2 API 的出現,在 CRI 的基礎上,爲用戶集成本身的容器運行時帶來了更加成熟和方便的實踐方法。算法
本次演講分享了關於 Kubernetes 接口化設計、CRI、容器運行時、shimv二、RuntimeClass 等關鍵技術特性的設計與實現,並以 KataContainers 爲例,爲聽衆演示上述技術特性的使用方法。本文整理自張磊在 KubeCon + CloudNativeCon 2018 現場的演講速記。api
今天,我給你們帶來的分享是關於 Kubernetes CRI 和 containerd shimv2 的設計,這也是目前社區裏比較重要的一個大方向。你們好,我是張磊,如今在阿里巴巴集團工做。既然今天我們會聊 Kubernetes 這個項目,那麼首先咱們來簡單看一下 Kubernetes 這個項目的工做原理。安全
Kubernetes 的工做原理工具
其實你們都知道 Kubernetes 這個項目它最上面是一層 Control Panel ,它也被不少人稱之爲 Master 節點。當你把 workload 就是你的應用提交給 Kubernetes 以後,首先爲你作事情的是 API server,它會把你的 Application 存到 etcd 裏,以 API 對象的方式存到 etcd 中去。性能
而 Kubernetes 中負責編排的是 Controller manager,一堆 controller 經過控制循環在 run。經過這個控制循環來作編排工做,幫你去建立出這些應用所須要的 Pod,注意不是容器,是 Pod。區塊鏈
而一旦一個 Pod 出現以後,Scheduler 會 watch 新 Pod 的變化。若是他發現有一個新的 Pod 出現,Scheduler 會幫你去把全部調度算法都 run 一遍,把 run 到的結果:就是一個 Node 的名字,寫在我這個 Pod 對象 NodeName 字段上面,就是一個所謂的 bind 的操做。而後把 bind 的結果寫回到 etcd 裏去,這就是所謂的 Scheduler 工做過程。因此 Control Panel 它忙活這麼一圈下來,最後獲得的結果是什麼呢?你的一個 Pod 跟一個 Node 綁定(bind)在了一塊兒,就是所謂 Schedule 了。優化
而 Kubelet 呢?它是運行在全部節點上。Kubelet 會 watch 全部 Pod 對象的變化,當它發現一個 Pod 與一個 Node 綁定在一塊兒的時,而且它又發現這個被綁定的 Node 是它本身,那麼 Kubelet 就會幫你去接管接下來的全部事情。加密
若是你看一下 Kubelet ,看看它在作什麼呢?很簡單,其實當 Kubelet 拿到這個信息以後,他是去 call 你運行在每一個機器上的 Containerd 進程,去 run 這個 Pod 裏的每個容器。spa
這時候,Containerd 幫你去 call runC 因此最後實際上是 runC 幫你去 set up 起來這些 namespace、Cgroup 這些東西,是它去幫你 chroot ,「搭」出來所謂的一個應用和須要的容器。這就是整個 Kubernetes 工做的一個簡單原理。插件
Linux Container
因此這個時候你可能會提出一個問題就是什麼是容器?其實容器很是簡單,咱們日常所說這個容器就是 Linux 容器,你能夠把 Linux 容器分爲兩部分:第一個是 Container Runtime,第二個是 Container Image。
所謂的 Runtime 部分就是你所運行進程的動態視圖和資源邊界,因此它是由 Namespace 和 Cgroup 爲你構建出來的。而對於 Image(鏡像),你能夠把它理解爲是你想要運行的程序的靜態視圖,因此它實際上是你的程序+數據+全部的依賴+全部的目錄文件組成一個壓縮包而已。
而這些壓縮包被以 union mount 的方式 mount 在一塊兒的時候,咱們稱之爲 rootfs 。rootfs 就是你的整個 process 的靜態視圖,他們看到這個世界就這樣子,因此這是 Linux Container。
KataContainer
可今天咱們還要聊另一種 Container,它與前面 Linux Container 大相徑庭。他的 Container Runtime 是用 hypervisor 實現的,是用 hardware virtualization 實現的,像個虛擬機同樣。因此每個像這樣的 KataContainer 的 Pod,都是一個輕量級虛擬機,它是有完整的 Linux 內核。因此咱們常常說 KataContainer 與 VM 同樣能提供強隔離性,但因爲它的優化和性能設計,它擁有與容器項媲美的敏捷性。這個一點稍後會強調,而對於鏡像部分, KataContainer 與 Docker 這些項目沒有任何不一樣,它使用的是標準 Linux Continer 容器,支持標準的 OCR Image 因此這一部分是徹底同樣的。
容器安全
但是你可能會問爲何咱們會有 KataContainer 這種項目? 其實很簡單,由於咱們關心安全這個事,好比不少金融的場景、加密的場景,甚至如今區塊鏈不少場景下,都須要一個安全的 Container Runtime,因此這是咱們強調 KataContainer 的一個緣由。
若是你如今正在使用 Docker, 我問一個問題就是你怎樣才能安全地使用 Docker?你可能會有不少套路去作。好比說你會 drop 掉一些 Linux capibility,你能夠去指定 Runtime 能夠作什麼,不能作什麼。第二個你能夠去 read-only mount points 。第三,你可使用 SELinux 或者 AppArmor 這些工具把容器給保護起來。還有一種方式是能夠直接拒絕一些 syscalls,能夠用到 SECCOMP。
可是我須要強調的是全部這些操做都會在你的 Container 和 Host 之間引入新的 layer,由於它要去作過濾,它要去攔截你的 syscalls,因此這個部分你搭的層越多,你容器性能越差,它必定是有額外的負面性能損耗的。
更重要的是,作這些事情以前你要想清楚到底應該幹什麼,到底應該 drop 掉哪些 syscalls,這個是須要具體問題具體分析的,那麼這時候我應該怎麼去跟個人用戶去講如何作這件事情?
因此,這些事情提及來很簡單,但實際執行起來不多有人知道到底該怎麼去作。因此在 99.99% 的狀況下,大多數人都是把容器 run 到虛擬機裏去的,尤爲在公有云場景下。
而對於 KataContainer 這種項目來講,它因爲使用了與虛擬機同樣的 hardware virualization,它是有獨立內核的,因此這個時候它提供的 isolation 是徹底可信任的,就與你信任 VM 是同樣的。
更重要的是,因爲如今每個 Pod 裏是有一個 Independent Kernel,跟個小虛擬機同樣,因此這時候就容許你容器運行的 Kernel 版本跟 Host machine 適應是徹底不同。這是徹底 OK 的,就與你在在虛擬機中作這件事同樣,因此這就是爲何我會強調 KataContainers 的一個緣由,由於它提供了安全和多租戶的能力。
Kubernetes + 安全容器
因此也就很天然會與有一個需求,就是咱們怎麼去把 KataContainer run 在 Kubernetes 裏?
那麼這個時候咱們仍是先來看 Kubelet 在作什麼事情,因此 Kubelet 要想辦法像 call Containerd 同樣去 call KataContainer,而後由 KataContainer 負責幫忙把 hypervisor 這些東西 set up 起來,幫我把這個小VM 運行起來。因此這個時候就要須要想怎麼讓 Kubernetes 能合理的操做 KataContainers。
Container Runtime Interface(CRI)
對於這個訴求,就關係到了咱們以前一直在社區推動的 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 大體它分爲 Container 和 Sandbox。Sandbox 用來描述的是我經過什麼樣的機制來去實現 Pod ,因此它其實就是 Pod這個概念真正跟容器項目相關的字段。對於 Docker 或 Linux 容器來講,它其實 match 到最後 run 起來的是一個叫 infra container 的容器,就是一個極小的容器,這個容器用來 hold 整個 Pod 的 Node 和 Namespace。
不過, Kubernetes 若是用 Linux Container Runtim, 好比 Docker 的話,它不會給你提供 Pod level 的 isolation,除了一層 Pod level cgroups 。這是一個不一樣點。由於,若是你用 KataContainers 的話,KataContaniners 會在這一步爲你建立一個輕量級的虛擬機。
接下來到下一階段,到 Containers 這個 API 的時候,對於 Docker 來講它就給你起在宿主機上啓動用戶容器,但對 Kata 來講不是這樣的,它會在前面的 Pod 對應的輕量級虛擬機裏面,也就在前面建立的 Sandbox 裏面 set up 這些用戶容器所須要 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,這是不同的。
因此接下來你 creat start container A 和 B 的時候,在 Docker 裏面是起兩個容器,但在 Kata 裏面是在我這個小虛擬機裏面,在這 Sandbox 裏面起兩個小 NameSpace,這是不同的。因此你把這一切東西總結一下,你會發現 OK,我如今要把 Kata run 在 Kubernetes 裏頭,因此我要作工做,在這一步要須要去作這個 CRI shim,我就想辦法給 Kata 做一個 CRI shim。
而咱們可以想到一個方式,我能不能重用如今的這些 CRI shim。重用如今哪些?好比說 CRI containerd 這個項目它就是一個 containerd 的 CRI shim,它能夠去響應 CRI 的請求過來,因此接下來我能不能把這些狀況翻譯成對 Kata 這些操做,因此這個是能夠的,這也是咱們將用一種方式,就是把 KataContainers 接到個人 Containerd 後面。這時候它的工做原理大概這樣這個樣子,Containerd 它有一個獨特設計,就是他會爲每個 Contaner 起個叫作 Contained shim。你 run 一下以後你會看他那個宿主機裏面,會 run 一片這個 Containerd shim 一個一個對上去。
而這時候因爲 Kata 是一個有 Sandbox 概念的這樣一個 container runtime,因此 Kata 須要去 match 這些 Shim 與 Kata 之間的關係,因此 Kata 作一個 Katashim。把這些東西對起來,就把你的 Contained 的處理的方式翻譯成對 kata 的 request,這是咱們以前的一個方式。
可是你能看到這其實有些問題的,最明顯的一個問題在於 對 Kata 或 gVisor 來講,他們都是有實體的 Sandbox 概念的,而有了 Sandbox 概念後,它就不該該去再去給他的每個 Container 啓動有一個 shim match 起來,由於這給咱們帶來很大的額外性能損耗。咱們不但願每個容器都去 match 一個 shim,咱們但願一個 Sandbox match 一個 shim。
另外,就是你會發現 CRI 是服務於 Kubernetes 的,並且它呈現向上彙報的狀態,它是幫助 Kubernetes 的,可是它不幫助 Container runtime。因此說當你去作這個集成時候,你會發現尤爲對於 VM gVisor\KataContainer 來講,它與 CRI 的不少假設或者是 API 的寫法上是不對應的。因此你的集成工做會比較費勁,這是一個不 match 的狀態。
最後一個就是咱們維護起來很是困難,由於因爲有了 CRI 以後,好比 RedHat 擁有本身的 CRI 實現叫 cri-o,他們和 containerd 在本質上沒有任何區別,跑到最後都是靠 runC 起容器,爲何要這種東西?
咱們不知道,可是我做爲 Kata maintainer,我須要給他們兩個分別寫兩部分的 integration 把 Kata 集成進去。這就很麻煩,者就意味着我有 100 種這種 CRI 我就要寫 100 個集成,並且他們的功能所有都是重複的。
Containerd ShimV2
因此在今天我給你們 propose 的這個東西叫作 Containerd ShimV2。前面咱們說過 CRI,CRI 決定的是 Runtime 和 Kubernetes 之間的關係,那麼咱們如今能不能再有一層更細緻的 API 來決定個人 CRI Shim 跟下面的 Runtime 之間真正的接口是什麼樣的?
這就是 ShimV2 出現的緣由,它是一層 CRI shim 到 Containerd runtime 之間的標準接口,因此前面我直接從 CRI 到 Containerd 到 runC,如今不是。咱們是從 CRI 到 Containerd 到 ShimV2,而後 ShimV2 再到 RunC 再到 KataContainer。這麼作有什麼好處?
咱們來看一下,最大的區別在於:在這種方式下,你能夠爲每個 Pod 指定一個 Shim。由於在最開始的時候,Containerd 是直接啓動了一個 Containerd Shim 來去作響應,但咱們新的 API 是這樣寫的,是 Containerd Shim start 或者 stop。因此這個 start 和 stop 操做怎麼去實現是你要作的事情。
而如今,我做爲一位 KataContainers項目的 maintainer 我就能夠這麼實現。我在 created Sandbox 的時候 call 這個 start 的時候,我啓動一個 Containerd Shim。可是當我下一步是 call API 的時候,就前面那個 CRI 裏面, Container API 時候,我就再也不起了,我是 reuse,我重用爲你建立好的這個 Sandbox,這就位你的實現提供了很大的自由度。
因此這時候你會發現整個實現的方式變了,這時候 Containerd 用過來以後,它再也不去 care 每一個容器起 Containerd Shim,而是由你本身去實現。個人實現方式是我只在 Sandbox 時候,去建立 containerd-shim-v2,而接下來整個後面的 container level 操做,我會所有走到這個 containerd-shim-v2 裏面,我去重用這個 Sandbox,因此這個跟前面的時間就出現很大的不一樣。
因此你如今去總結一下這個圖的話,你發現咱們實現方式是變成這個樣子:
首先,你仍是用原來的 CRI Containerd,只不過如今裝的是 runC,你如今再裝一個 katacontainer 放在那機器上面。接下來咱們 Kata 那邊會給你寫一個實現叫 kata-Containerd-Shimv2。因此前面要寫一大坨 CRI 的東西,如今不用了。如今,咱們只 focus 在怎麼去把 Containerd 對接在 kata container 上面,就是所謂的實現 Shimv2 API,這是咱們要作的工做。而具體到咱們這要作的事情上,其實它就是這樣一系列與 run 一個容器相關的 API。
好比說我能夠去 create、start,這些操做所有映射在我 Shimv2 上面去實現,而不是說我如今考慮怎麼去映射,去實現 CRI,這個自由度因爲以前太大,形成了咱們如今的一個局面,就有一堆 CRI Shim 能夠用。這實際上是一個很差的事情。有不少政治緣由,有不少非技術緣由,這都不是咱們做爲技術人員應該關心的事情,你如今只須要想我怎麼去跟 Shimv2 對接就行了。
接下來,我爲你演示一下經過 CRI + containerd shimv2調用 KataContainers 的一個 Demo(具體內容略)
總結
Kubernetes 如今的核心設計思想,就是經過接口化和插件化,將本來複雜的、對主幹代碼有侵入性的特性,逐一從核心庫中剝離和解耦。而在這個過程當中,CRI 就是 Kubernetes 項目中最先完成了插件化的一個調用接口。而此次分享,主要爲你介紹了在CRI基礎上的另外一種集成容器運行時的思路,即:CRI + containerd shimv2 的方式。經過這種方式,你就不須要再爲本身的容器運行時專門編寫一個 CRI 實現(CRI shim),而是能夠直接重用 containerd對 CRI 的支持能力,而後經過 containerd shimv2的方式來對接具體的容器運行時(好比 runc)。目前,這種集成方式已經成爲了社區對接下層容器運行時的主流思路,像不少相似於 KataContainers,gVisor,Firecracker 等基於獨立內核或者虛擬化的容器項目,也都開始經過 shimv2 ,進而藉助 containerd項目無縫接入到 Kubernetes 當中。
而衆所周知,在阿里內部,Sigma/Kubernetes 系統使用的容器運行時主要是 PouchContainer。事實上,PouchContainer 自己選擇使用 containerd 做爲其主要的容器運行時管理引擎,並自我實現了加強版的 CRI 接口,使其知足阿里巴巴強隔離、生產級別的容器需求。因此在 shimv2 API 在 containerd 社區發佈以後,PouchContainer 項目就已經率先開始探索和嘗試經過 containerd shimv2 來對接下層的容器運行時,進而更高效的完成對其餘種類的容器運行時尤爲是虛擬化容器的集成工做。咱們知道,自從開源以來,PouchContainer 團隊一直都在積極地推進 containerd 上游社區的發展和演進工做,而在此次 CRI + containerd shimv2 的變革裏, PouchContainer 再一次走到了各個容器項目的最前面。