轉載自:K8sMeetup社區node
ID:Kuberneteschina2docker
做者:吳葉磊(PingCAP)api
編輯:小君君(才雲)安全
近年來,Runtime(容器運行時)發展迅速,種類也日漸豐富:Docker、rkt、containerd、cri-o、Kata、gVisor……面對這麼多的選擇,若是你正打算部署一個容器系統或 Kubernetes 集羣,你會如何選擇呢?在這篇文章中,來自 PingCAP 的工程師吳葉磊將從典型的 Runtime 架構、OCI、CRI 與被濫用的名詞「Runtime」等方向,生動闡述什麼是 Runtime 以及它們的關係和特色。+186 142 996 20 得到更多技術知識。網絡
在剛開始接觸 Kubernetes 的時候,相信不少人都常常搞不懂 CRI 與 OCI 的聯繫和區別,也不知道爲何要墊那麼多的「shim」(尤爲是 containerd-shim 和 dockershim 這兩個徹底沒什麼關聯的東西都被稱爲 shim)。這篇文章就和你們一塊兒聊聊 Kubernetes Runtime,把下面這張 Landscape 裏的核心項目陳述清楚:架構
經過本文你將瞭解到:app
典型的 Runtime 架構;less
大話容器歷史;ide
OCI、CRI 與被濫用的名詞「Runtime」;函數
containerd 和 CRI-O;
強隔離容器:Kata、gVisor、firecracker;
安全容器與 Serverless。
首先,本文從最多見的 Runtime 方案 Docker 提及:
當 Kubelet 想要建立一個容器時,它須要如下幾個步驟:
Kubelet 經過 CRI 接口(gRPC)調用 dockershim,請求建立一個容器,CRI(容器運行時接口,Container Runtime Interface)。在這一步中 , Kubelet 能夠視做一個簡單的 CRI Client,而 dockershim 就是接收請求的 Server。目前 dockershim 的代碼實際上是內嵌在 Kubelet 中的,因此接收調用的就是 Kubelet 進程;
dockershim 收到請求後,它會轉化成 Docker Daemon 能聽懂的請求,發到 Docker Daemon 上,並請求建立一個容器;
Docker Daemon 早在 1.12 版本中就已經將針對容器的操做移到另外一個守護進程 containerd 中了。所以 Docker Daemon 仍然不能幫人們建立容器,而是須要請求 containerd 建立一個容器;
containerd 收到請求後,並不會本身直接去操做容器,而是建立一個叫作 containerd-shim 的進程,讓 containerd-shim 去操做容器。這是由於容器進程須要一個父進程來作諸如收集狀態、維持 stdin 等 fd 打開工做。假如這個父進程就是 containerd,那每次 containerd 掛掉或升級後,整個宿主機上全部的容器都須要退出,可是引入了 containerd-shim 就規避了這個問題(containerd 和 shim 並非父子進程關係);
建立容器是須要作一些設置 namespace 和 Cgroups、掛載 root filesystem 的操做。這些事已經有了公開的規範 OCI(Open Container Initiative,開放容器標準)。它的一個參考實現叫作 runc。containerd-shim 在這一步須要調用 runc 這個命令行工具,來啓動容器;
runc 啓動完容器後,它會直接退出,containerd-shim 則會成爲容器進程的父進程,負責收集容器進程的狀態,上報給 containerd。並在容器中 pid 爲 1 的進程退出後接管容器中的子進程,而後進行清理,確保不會出現殭屍進程。
Docker Daemon 和 dockershim 看上去就像是兩個不幹活的組件,Kubelet 爲啥不直接調用 containerd 呢?
固然是能夠的!可是,在瞭解這個以前,你們不妨先看看爲何如今的架構如此繁冗。
其實 Kubernetes 最開始的 Runtime 架構遠沒這麼複雜:Kubelet 想要建立容器能夠直接通知 Docker Daemon,那時也不存在 containerd。Docker Daemon 自行調節libcontainer
庫就能夠把容器跑起來。
而熟悉容器和容器編排歷史的讀者應該知道,在這以後就是容器圈的一系列政治鬥爭。先是大佬們認爲 Runtime 標準不能被 Docker 一家公司控制,因而推出了開放容器標準 OCI。Docker 則把libcontainer
封裝起來 , 變成 runc 捐獻出來,做爲 OCI 的參考實現。
此時 rkt 也想從 Docker 那邊分一杯羹,但願 Kubernetes 原生支持 rkt 做爲 Runtime,而且 PR 也成功的合進去了。接觸過一塊業務同時接兩個需求方的讀者應該都知道相似這樣的事情處理起來很麻煩,Kubernetes 中負責維護 Kubelet 的小組 sig-node 也被這件事狠狠的坑了一把。
後來,你們認爲這樣作是不行的。今天能有 rkt,明天就能有其餘的什麼出來,久而久之,sig-node 小組的工做便沒法進行下去(天天都須要處理兼容性的 bug)。因而,Kubernetes v1.5 推出了 CRI 機制(即容器運行時接口,Container Runtime Interface)。Kubernetes 藉此告知你們 , 只要能實現這個接口,誰均可以作 Runtime。
不過 CRI 自己只是 Kubernetes 的一個標準。當時的 Kubernetes 還沒有達到現在這般舉足輕重的地位,容器運行時也不會與 Kubernetes 綁死,只提供 CRI 接口。因而就有了 shim(墊片),一個 shim 的職責就是做爲 Adapter 將各類容器運行時自己的接口適配到 Kubernetes 的 CRI 接口上。
接下來,Docker 的 Swarm 爲進軍 PaaS 市場,作了個架構切分,將容器操做都移動到一個單獨的 Daemon 進程 containerd 中,讓 Docker Daemon 專門負責上層的封裝編排。惋惜 Swarm 並無 Kubernetes 那般功能強大。失敗以後,Docker 公司就把 containerd 項目捐給 CNCF,專心作 Docker 企業版。
通過這些事情以後,就是讀者們在上一張圖中看到的那些東西了。儘管如今已經有 CRI-O、containerd-plugin 這樣更精簡輕量的 Runtime 架構,可是 dockershim 這一套做爲經受了最多生產環境考驗的方案,迄今爲止還是 Kubernetes 默認的 Runtime 實現。
瞭解這些具體的架構,有時能幫助人們在排除故障時少走彎路,但更重要的是它們能做爲一個例子 , 幫助人們更好地理解整個 Kubernetes Runtime 背後的設計邏輯 。
OCI,也就是前文提到的「開放容器標準」,在官方文檔中主要規定了兩點:
容器鏡像應該是什麼樣的,即 ImageSpec。它大體規定的是,你的容器鏡像須要是一個壓縮了的文件夾,文件夾裏以 xxx 結構放入 xxx 文件中;
容器要須要能接收哪些指令,這些指令的行爲是什麼,即 RuntimeSpec。簡單來講,它規定的就是「容器」要可以執行「create」「start」「stop」「delete」這些命令,而且行爲要規範。
runc 爲何叫參考實現?由於它能按照標準將符合標準的容器鏡像運行起來。
標準的好處就是方便搞創新,只要研發的東西符合標準,在生態圈裏就能與其它工具一塊兒愉快地工做。那研發人員自行研發的鏡像就能夠用任意的工具去構建,「容器」也不必定非要用 namespace 和 Cgroups 來作隔離。這就讓各類虛擬化容器能夠更好地參與到遊戲當中。
而 CRI 更簡單,單純是一組 gRPC 接口,看一眼 kubelet/apis/cri/services.go 就能概括出幾套核心接口:
一套針對容器操做的接口,包括建立、啓停容器等;
一套針對鏡像操做的接口,包括拉取鏡像、刪除鏡像等;
還有一套針對 PodSandbox(容器沙箱環境)的操做接口。
如今咱們能夠找到不少符合 OCI 標準或兼容了 CRI 接口的項目,這些項目大致構成了整個 Kuberentes 的 Runtime 生態:
OCI Compatible:runc、Kata(以及它的前身 runV 和 Clear Containers)、gVisor。其它比較偏門的還有 Rust 寫的 railcar;
CRI Compatible:Docker(藉助 dockershim)、containerd(藉助 CRI-containerd)、CRI-O、frakti 等。
不少讀者可能在最開始學習 Kubernetes 的時候,弄不清 OCI 和 CRI 的區別與聯繫。其中一大緣由就是社區裏糟糕的命名:這上面的項目通通能夠稱爲容器運行時(Container Runtime),彼此之間區分的辦法就是給「容器運行時」這個詞加上各類定語和從句來進行修飾。Go 語言的開源貢獻者和項目成員 Dave Cheney 曾說過:
Good naming is like a good joke. If you have to explain it, it’s not funny.
顯然 Container Runtime 在這裏就不是一個好名字了,更準確的說法是:cri-runtime 和 oci-runtime。經過這個粗略的分類,就能夠總結出整個 Runtime 架構萬變不離其宗的三層抽象:
1 Orchestration API -> Container API -> Kernel API
這其中 Kubernetes 已是 Orchestration API 的事實標準。而在 Kubernetes 中,Container API 的接口標準就是 CRI,由 cri-runtime 實現。Kernel API 的規範是 OCI,由 oci-runtime 實現。
根據這個思路 , 咱們就很容易理解下面這兩種東西:
各類更爲精簡的 cri-runtime;
各類「強隔離」容器方案。
讀者們在第一節就看到如今的 Runtime 實在是有點複雜,後來人們就有了直接拿 containerd 作 oci-runtime 的方案。固然,除了 Kubernetes 以外,containerd 還要接諸如 Swarm 等調度系統,所以它不會去直接實現 CRI。這個適配工做就要交給一個 shim 了。
在 containerd v1.0 中,對 CRI 的適配經過一個單獨的進程CRI-containerd
來完成:
containerd v1.1 中作的又更漂亮一點,砍掉 CRI-containerd 進程,直接把適配邏輯做爲插件放進 containerd 主進程中:
但在 containerd 作這些事情以前,社區就已經有了一個更爲專一的 cri-runtime: CRI-O。它很是純粹,能夠兼容 CRI 和 OCI,作一個 Kubernetes 專用的運行時:
其中conmon
就對應 containerd-shim,大致意圖是同樣的。
CRI-O 和 containerd(直接調用)的方案比起默認的 dockershim 簡潔不少,但沒什麼生產環境的驗證案例。本人所知道的僅僅是 containerd 在 GKE 上是 beta 狀態。所以假如你對 Docker 沒有特殊的政治恨意,大可沒必要把 dockershim 這套換掉。
一直以來 Kubernetes 都有一個被詬病的點:難以實現真正的多租戶。
爲何這麼說呢?讀者們先考慮一下什麼樣是理想的多租戶狀態:
理想來講,平臺的各個租戶(tenant)之間應該沒法感覺到彼此的存在,表現得就像每一個租戶獨佔整個平臺同樣。具體來講就是,我不能看到其它租戶的資源,個人資源跑滿了,也不能影響其它租戶的資源使用。我沒法從網絡或內核上***其它租戶。
Kubernetes 固然作不到,其中最大的兩個緣由是:
kube-apiserver 是整個集羣中的單例,而且沒有多租戶概念;
默認的 oci-runtime 是 runc,而 runc 啓動的容器是共享內核的。
一個典型的解決方案就是提供一個新的 OCI 實現,用 VM 來跑容器,實現內核上的硬隔離。runV 和 Clear Containers 都是這個思路。由於這兩個項目作得事情是很相似,後來就合併成了一個項目 Kata Container。Kata 的一張圖很好地解釋了基於虛擬機的容器與基於 namespaces 和 Cgroups 的容器間的區別:
固然,沒有系統是徹底安全的。假如 hypervisor 存在漏洞,那麼用戶仍有可能攻破隔離。但全部的事情都要對比而言,在共享內核的狀況下,暴露的***面是很是大的,作安全隔離的難度就像在美利堅和墨西哥之間修 The Great Wall。而當內核隔離以後,只要守住 hypervisor 這道關子就後顧無虞了。
一個 VM 中跑一個容器,聽上去隔離性很不錯,但不是說虛擬機又笨重又很差管理才切換到容器的嗎,怎麼又要走回去了?
Kata 告訴你,虛擬機沒那麼邪惡,只是之前沒玩好:
很差管理是由於沒有遵循「不可變基礎設施」,之前你們都在虛擬機上瘋狂的試探。這臺裝 Java 8,那臺裝 Java 6,Admin 是要 angry 的。如今,Kata 則支持 OCI 鏡像,徹底能夠用上 Dockerfile + 鏡像,讓很差管理成爲了過去時;
笨重是由於以前要虛擬化整個系統。如今咱們只着眼於虛擬化應用,那就能夠裁剪掉不少功能,把 VM 作得很輕量。所以即使用虛擬機來作容器,Kata 仍是能夠將容器啓動時間壓縮得很是短,啓動後在內存上和 IO 上的 overhead 也儘量去優化。
不過話說回來,Kubernetes 上的調度單位是 Pod,是容器組,Kata 虛擬機裏的一個容器。那同一個 Pod 間的容器應該如何作 namespace 的共享?
這就要說回前文講到的 CRI 中,針對 PodSandbox(容器沙箱環境)的操做接口了。本文第一節刻意簡化了場景,只考慮建立一個容器,而沒有討論建立一個Pod。你們都知道,真正啓動 Pod 裏定義的容器以前,Kubelet 會先啓動一個 infra 容器,並執行 /pause 讓 infra 容器的主進程永遠掛起。
這個容器存在的目的就是維持住整個 Pod 的各類 namespace。真正的業務容器只要加入 infra 容器的 network 等 namespace 就能實現對應 namespace 的共享。而 infra 容器創造的這個共享環境則被抽象爲 PodSandbox。每次 Kubelet 在建立 Pod 時,就會先調用 CRI 的RunPodSandbox
接口啓動一個沙箱環境,再調用CreateContainer
在沙箱中建立的容器。
這裏就已經說出答案了,對於 Kata Container 而言,只要在RunPodSandbox
調用中建立一個 VM,以後再往 VM 中添加容器就能夠了。最後運行 Pod 的樣子就是這樣的:
說完了 Kata,其實 gVisor 和 firecracker 都不言自明瞭,大致上都是相似的,只是:
gVisor 並不會去建立一個完整的 VM,而是實現了一個叫「Sentry」的用戶態進程來處理容器的 syscall,而攔截 syscall 並重定向到 Sentry 的過程則由 KVM 或 ptrace 實現;
firecracker 稱本身爲 microVM,即輕量級虛擬機,它自己仍是基於 KVM 的。不過 KVM 一般使用 QEMU 來虛擬化除 CPU 和內存外的資源,好比 IO 設備、網絡設備。firecracker 則使用 rust 實現了最精簡的設備虛擬化,爲的就是壓榨虛擬化的開銷,越輕量越好。
你可能以爲安全容器對本身而言沒什麼用:大不了在每一個產品線上都部署 Kubernetes,機器池都隔離掉,從基礎設施的層面就隔離掉。
這麼作固然能夠,但同時也要知道,這種作法最終實際上是以 IaaS 的方式在賣資源,是作不了真正的 PaaS 乃至 Serverless 的。
Serverless 要作到全部的用戶容器或函數按需使用計算資源,那必須知足兩點:
多租戶強隔離:用戶的容器或函數都是按需啓動按秒計費,咱們可不能給每一個用戶預先分配一坨隔離的資源。所以咱們要保證整個 Platform 是多租戶強隔離的;
極度輕量:Serverless 的第一個特色是運行時沙箱會更頻繁地建立和銷燬;第二個特色是切分的粒度會很是很是細,細中細就是 FaaS,一個函數就要一個沙箱。所以就要求兩點:
沙箱啓動刪除必須飛快;
沙箱佔用的資源越少越好。
這兩點在 long-running,粒度不大的容器運行環境下可能不明顯,但在 Serverless 環境下就會急劇被放大。這時候去作 MicroVM 的 ROI 就比之前要高不少。想一想 , 用傳統的 KVM 去跑 FaaS,那還不得虧到姥姥家了?
整篇文章的內容很是多,但 rkt、lxc、lxd 都還沒涉及。爲控制篇幅,這裏只提供類比,你們能夠自行拓展閱讀:rkt 跟 Docker 同樣是一個容器引擎,特色是無 daemon,目前這個項目基本不活躍;lxc 是 Docker 最先使用的容器工具集,位置能夠類比 runc,提供跟 kernel 打交道的庫 & 命令行工具;lxd 則是基於 lxc 的一個容器引擎,只不過大多數容器引擎的目標是容器化應用,lxd 的目標則是容器化操做系統。
最後 , 這篇文章涉及內容較多,若有紕漏,敬請指正!