CRI shim:kubelet怎么與runtime交互(一)
CRI shim是什么?
實(shí)現(xiàn)了 CRI 接口的容器運(yùn)行時(shí)通常稱為 CRI shim, 這是一個(gè) gRPC Server,監(jiān)聽在本地的 unix socket 上;而 kubelet 作為 gRPC 的客戶端來調(diào)用 CRI 接口,來進(jìn)行 Pod 和容器、鏡像的生命周期管理。另外,容器運(yùn)行時(shí)需要自己負(fù)責(zé)管理容器的網(wǎng)絡(luò),推薦使用 CNI。
kubelet 調(diào)用下層容器運(yùn)行時(shí)的執(zhí)行過程,并不會(huì)直接調(diào)用Docker 的 API,而是通過一組叫作 CRI(Container Runtime Interface,容器運(yùn)行時(shí)接口)的 gRPC 接口來間接執(zhí)行的,意味著需要使用新的連接方式與 docker 通信,為了兼容以前的版本,k8s 提供了針對(duì) docker 的 CRI 實(shí)現(xiàn),也就是kubelet包下的dockershim包,dockershim是一個(gè) grpc 服務(wù),監(jiān)聽一個(gè)端口供 kubelet 連接,dockershim收到 kubelet 的請(qǐng)求后,將其轉(zhuǎn)化為 REST API 請(qǐng)求,再發(fā)送給docker daemon。Kubernetes 項(xiàng)目之所以要在 kubelet 中引入這樣一層單獨(dú)的抽象,當(dāng)然是為了對(duì) Kubernetes 屏蔽下層容器運(yùn)行時(shí)的差異。
解決思路再次體現(xiàn)了《代碼大全2》里提到的那句經(jīng)典名言:any problem in computer science can be sloved by another layer of indirecition。計(jì)算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過增加一個(gè)中間層來解決,我們的 CRI shim就是加了這樣一層。
CRI shim server 接口圖示
**CRI 接口包括 RuntimeService 和 ImageService 兩個(gè)服務(wù),這兩個(gè)服務(wù)可以在一個(gè) gRPC server 中實(shí)現(xiàn),也可以分開成兩個(gè)獨(dú)立服務(wù)。**目前社區(qū)的很多運(yùn)行時(shí)都是將其在一個(gè) gRPC server 里面實(shí)現(xiàn)。
ImageServiceServer 提供了 5 個(gè)接口,用于管理容器鏡像。管理鏡像的 ImageService 提供了 5 個(gè)接口:
- 查詢鏡像列表;
- 拉取鏡像到本地;
- 查詢鏡像狀態(tài);
- 刪除本地鏡像;
- 查詢鏡像占用空間等。
關(guān)于容器鏡像的操作比較簡(jiǎn)單,所以我們就暫且略過。接下來,我主要為你講解一下 RuntimeService 部分。RuntimeService 則提供了更多的接口,按照功能可以劃分為四組:
- PodSandbox 的管理接口:CRI 設(shè)計(jì)的一個(gè)重要原則,就是確保這個(gè)接口本身,只關(guān)注容器,不關(guān)注 Pod。
- PodSandbox 是對(duì) Kubernete Pod 的抽象,用來給容器提供一個(gè)隔離的環(huán)境(比如掛載到相同的 CGroup 下面),并提供網(wǎng)絡(luò)等共享的命名空間。PodSandbox 通常對(duì)應(yīng)到一個(gè) Pause 容器或者一臺(tái)虛擬機(jī);
- Container 的管理接口:在指定的 PodSandbox 中創(chuàng)建、啟動(dòng)、停止和刪除容器;
- Streaming API 接口:包括 Exec、Attach 和 PortForward 等三個(gè)和容器進(jìn)行數(shù)據(jù)交互的接口,這三個(gè)接口返回的是運(yùn)行時(shí) Streaming Server 的 URL,而不是直接跟容器交互。kubelet 需要跟容器項(xiàng)目維護(hù)一個(gè)長(zhǎng)連接來傳輸數(shù)據(jù)。這種 API,我們就稱之為 Streaming API。
- 狀態(tài)接口:包括查詢 API 版本和查詢運(yùn)行時(shí)狀態(tài)。
我們通過 kubectl 命令來運(yùn)行一個(gè) Pod,那么 Kubelet 就會(huì)通過 CRI 執(zhí)行以下操作:
- 首先調(diào)用 RunPodSandbox 接口來創(chuàng)建一個(gè) Pod 容器,Pod 容器是用來持有容器的相關(guān)資源的,比如說網(wǎng)絡(luò)空間、PID空間、進(jìn)程空間等資源;
- 然后調(diào)用 CreatContainer 接口在 Pod 容器的空間創(chuàng)建業(yè)務(wù)容器;
- 再調(diào)用 StartContainer 接口啟動(dòng)運(yùn)行容器
- 最后調(diào)用停止,銷毀容器的接口為 StopContainer 與 RemoveContainer。
就完成了整個(gè)Container的生命周期。
Streaming API
CRI shim 對(duì) Streaming API 的實(shí)現(xiàn),依賴于一套獨(dú)立的 Streaming Server 機(jī)制。Streaming API 用于客戶端與容器進(jìn)行交互,包括 Exec、PortForward 和 Attach 等三個(gè)接口。kubelet 內(nèi)置的 Docker 通過 nsenter、socat 等方法來支持這些特性,但它們不一定適用于其他的運(yùn)行時(shí),也不支持 Linux 之外的其他平臺(tái)。因而,CRI 也顯式定義了這些 API,并且要求容器運(yùn)行時(shí)返回一個(gè) Streaming Server 的 URL 以便 kubelet 重定向 API Server 發(fā)送過來的流式請(qǐng)求。
因?yàn)樗腥萜鞯牧魇秸?qǐng)求都會(huì)經(jīng)過 kubelet,這可能會(huì)給節(jié)點(diǎn)的網(wǎng)絡(luò)流量帶來瓶頸,因而 CRI 要求容器運(yùn)行時(shí)啟動(dòng)一個(gè)對(duì)應(yīng)請(qǐng)求的單獨(dú)的流服務(wù)器,將地址返回給 kubelet。kubelet 將這個(gè)信息再返回給 Kubernetes API Server,會(huì)直接打開與運(yùn)行時(shí)提供的服務(wù)器相連的流連接,并通過它與客戶端連通。
這樣一個(gè)完整的 Exec 流程就如上圖所示,分為多個(gè)階段:
- 客戶端 kubectl exec -i -t ...;
- kube-apiserver 向 kubelet 發(fā)送流式請(qǐng)求 /exec/;
- kubelet 通過 CRI 接口向 CRI Shim 請(qǐng)求 Exec 的 URL;
- CRI Shim 向 kubelet 返回 Exec URL;
- kubelet 向 kube-apiserver 返回重定向的響應(yīng);
- kube-apiserver 重定向流式請(qǐng)求到 Exec URL,然后將 CRI Shim 內(nèi)部的 Streaming Server 跟 kube-apiserver 進(jìn)行數(shù)據(jù)交互,完成 Exec 的請(qǐng)求和響應(yīng)。
也就是說 apiserver 其實(shí)實(shí)際上是跟 streaming server 交互來獲取我們的流式數(shù)據(jù)的。這樣一來讓我們的整個(gè) CRI Server 接口更輕量、更可靠。
注意:當(dāng)然,這個(gè) Streaming Server 本身,是需要通過使用 SIG-Node 為你維護(hù)的 Streaming API 庫來實(shí)現(xiàn)的。并且,Streaming Server 會(huì)在 CRI shim 啟動(dòng)時(shí)就一起啟動(dòng)。此外,Stream Server 這一部分具體怎么實(shí)現(xiàn),完全可以由 CRI shim 的維護(hù)者自行決定。比如,對(duì)于 Docker 項(xiàng)目來說,dockershim 就是直接調(diào)用 Docker 的 Exec API 來作為實(shí)現(xiàn)的。
CRI-containerd架構(gòu)解析與主要接口解析
整個(gè)架構(gòu)看起來非常直觀。這里的 Meta services、Runtime service 與 Storage service 都是 containerd 提供的接口。它們是通用的容器相關(guān)的接口,包括鏡像管理、容器運(yùn)行時(shí)管理等。CRI 在這之上包裝了一個(gè) gRPC 的服務(wù)。右側(cè)就是具體的容器的實(shí)現(xiàn)。比如說,創(chuàng)建容器時(shí)就要?jiǎng)?chuàng)建具體的 runtime 和它的containerd-shim。Container 和 Pod Sandbox組成了一個(gè)Pod。
CRI-containerd 的一個(gè)好處是,containerd 還額外實(shí)現(xiàn)了更豐富的容器接口,所以它可以用 containerd 提供的 ctr 工具來調(diào)用這些豐富的容器運(yùn)行時(shí)接口,而不只是 CRI 接口
CRI實(shí)現(xiàn)了兩個(gè)GRPC協(xié)議的API,提供兩種服務(wù)ImageService和RuntimeService。
- // grpcServices are all the grpc services provided by cri containerd.
- type grpcServices interface {
- runtime.RuntimeServiceServer
- runtime.ImageServiceServer
- }
- // CRIService is the interface implement CRI remote service server.
- type CRIService interface {
- Run() error
- // io.Closer is used by containerd to gracefully stop cri service.
- io.Closer
- plugin.Service
- grpcServices
- }
CRI的實(shí)現(xiàn)CRIService中包含了很多重要的組件:其中最重要的是cni.CNI,用于配置容器網(wǎng)絡(luò)。還有containerd.Client,用于連接containerd來創(chuàng)建容器。
- // criService implements CRIService.
- type criService struct {
- // config contains all configurations.
- config criconfig.Config
- // imageFSPath is the path to image filesystem.
- imageFSPath string
- // os is an interface for all required os operations.
- os osinterface.OS
- // sandboxStore stores all resources associated with sandboxes.
- sandboxStore *sandboxstore.Store
- // sandboxNameIndex stores all sandbox names and make sure each name
- // is unique.
- sandboxNameIndex *registrar.Registrar
- // containerStore stores all resources associated with containers.
- containerStore *containerstore.Store
- // containerNameIndex stores all container names and make sure each
- // name is unique.
- containerNameIndex *registrar.Registrar
- // imageStore stores all resources associated with images.
- imageStore *imagestore.Store
- // snapshotStore stores information of all snapshots.
- snapshotStore *snapshotstore.Store
- // netPlugin is used to setup and teardown network when run/stop pod sandbox.
- netPlugin cni.CNI
- // client is an instance of the containerd client
- client *containerd.Client
- // streamServer is the streaming server serves container streaming request.
- streamServer streaming.Server
- // eventMonitor is the monitor monitors containerd events.
- eventMonitor *eventMonitor
- // initialized indicates whether the server is initialized. All GRPC services
- // should return error before the server is initialized.
- initialized atomic.Bool
- // cniNetConfMonitor is used to reload cni network conf if there is
- // any valid fs change events from cni network conf dir.
- cniNetConfMonitor *cniNetConfSyncer
- // baseOCISpecs contains cached OCI specs loaded via `Runtime.BaseRuntimeSpec`
- baseOCISpecs map[string]*oci.Spec
- }
我們知道 Kubernetes 的一個(gè)運(yùn)作的機(jī)制是面向終態(tài)的,在每一次調(diào)協(xié)的循環(huán)中,Kubelet 會(huì)向 apiserver 獲取調(diào)度到本 Node 的 Pod 的數(shù)據(jù),再做一個(gè)面向終態(tài)的處理,以達(dá)到我們預(yù)期的狀態(tài)。
循環(huán)的第一步,首先通過 List 接口拿到容器的狀態(tài)。確保有鏡像,如果沒有鏡像則 pull 鏡像再通過 Sandbox 和 Container 接口來創(chuàng)建容器。需要注意的是,我們的 CNI(容器網(wǎng)絡(luò)接口)也是在 CRI 進(jìn)行操作的,因?yàn)槲覀冊(cè)趧?chuàng)建 Pod 的時(shí)候需要同時(shí)創(chuàng)建網(wǎng)絡(luò)資源然后注入到 Pod 中(PS:CNI包含在創(chuàng)建Pod 這個(gè)動(dòng)作里)。接下來就是我們的容器和鏡像。我們通過具體的容器創(chuàng)建引擎來創(chuàng)建一個(gè)具體的容器。
執(zhí)行流程為:
- Kubelet 通過 CRI runtime service API 調(diào)用 CRI plugin 創(chuàng)建 pod
- CRI 通過 CNI 創(chuàng)建 pod 的網(wǎng)絡(luò)配置和 namespace
- CRI使用 containerd 創(chuàng)建并啟動(dòng) pause container (sandbox container) 并且把這個(gè) container 置于 pod 的 cgroups/namespace
- Kubelet 接著通過 CRI image service API 調(diào)用 CRI plugin, 獲取容器鏡像
- CRI 通過 containerd 獲取容器鏡像
- Kubelet 通過 CRI runtime service API 調(diào)用 CRI, 在 pod 的空間使用拉取的鏡像啟動(dòng)容器
- CRI 通過 containerd 創(chuàng)建/啟動(dòng) 應(yīng)用容器, 并且把 container 置于 pod 的 cgroups/namespace. Pod 完成啟動(dòng)。
總結(jié)
發(fā)現(xiàn) CRI 只是服務(wù)于 Kubernetes 的,而且它呈現(xiàn)向上匯報(bào)的狀態(tài)。它是幫助 Kubernetes 的,它不幫助OCI的。所以說當(dāng)你去做這個(gè)集成時(shí)候,你會(huì)發(fā)現(xiàn)尤其對(duì)于 VM gVisor\KataContainer 來說,它與 CRI 的很多假設(shè)或者是 API 的寫法上是不對(duì)應(yīng)的。所以你的集成工作會(huì)比較費(fèi)勁,這是一個(gè)不 match 的狀態(tài)。
最后一個(gè)就是我們維護(hù)起來非常困難,因?yàn)橛捎谟辛? CRI 之后,比如 RedHat 擁有自己的 CRI 實(shí)現(xiàn)叫 cri-o,他們和 containerd 在本質(zhì)上沒有任何區(qū)別,跑到最后都是靠 runC 起容器,為什么還需要cri-o這種東西?
我們不知道,如果我想使用Kata container與containerd多運(yùn)行時(shí)的話,我需要給他們兩個(gè)分別寫兩部分的一體化把 Kata 集成進(jìn)去。這就很麻煩,就意味著我有 100 種這樣的 CRI ,我就要寫 100 個(gè)shim去集成,而且他們的功能全部都是重復(fù)的。
所以這就產(chǎn)生了Containerd ShimV2的這樣的shim來解決這個(gè)問題。我們下回分解。
reference
https://time.geekbang.org/column/article/71499?utm_campaign=guanwang&utm_source=baidu-ad&utm_medium=ppzq-pc&utm_content=title&utm_term=baidu-ad-ppzq-title
https://blog.frognew.com/2021/04/relearning-container-02.html
https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md
https://developer.aliyun.com/article/679993
本文轉(zhuǎn)載自微信公眾號(hào)「運(yùn)維開發(fā)故事」