Kubernetes CRI - 容器運(yùn)行時(shí)接口解析
本文轉(zhuǎn)載自微信公眾號(hào)「運(yùn)維開發(fā)故事」,作者沒有文案的夏老師。轉(zhuǎn)載本文請(qǐng)聯(lián)系運(yùn)維開發(fā)故事公眾號(hào)。
kubelet 的組件
kubelet 本身,也是按照“控制器”模式來工作的。它實(shí)際的工作原理,可以用如下所示的一幅示意圖來表示清楚。
- Kubelet Server 對(duì)外提供 API,供 kube-apiserver、metrics-server 等服務(wù)調(diào)用。比如 kubectl exec 時(shí)需要通過 Kubelet API /exec/{token} 與容器進(jìn)行交互;
 - Container Manager 管理容器的各種資源,比如 CGroups、QoS、cpuset、device 等;
 - Volume Manager 管理容器的存儲(chǔ)卷,比如格式化磁盤、掛載到 Node 本地、最后再將掛載路徑傳給容器;
 - Eviction 負(fù)責(zé)容器的驅(qū)逐,比如在資源不足時(shí)驅(qū)逐優(yōu)先級(jí)低的容器,保證高優(yōu)先級(jí)容器的運(yùn)行;
 - cAdvisor 負(fù)責(zé)為容器提供 Metrics;
 - Metrics 和 stats 提供容器和節(jié)點(diǎn)的度量數(shù)據(jù),比如 metrics-server 通過 /stats/summary 提取的度量數(shù)據(jù)是 HPA 自動(dòng)擴(kuò)展的依據(jù);
 - Generic Runtime Manager 是容器運(yùn)行時(shí)的管理者,負(fù)責(zé)與CRI 交互,完成容器和鏡像的管理;
 
CRI中定義了容器和鏡像的服務(wù)的接口,因?yàn)槿萜鬟\(yùn)行時(shí)與鏡像的生命周期是彼此隔離的,因此需要定義兩個(gè)服務(wù)。該接口使用Protocol Buffer,基于gRPC,在Kubernetes v1.10+版本中是在pkg/kubelet/apis/cri/runtime/v1alpha2的api.proto中定義的。
CRI架構(gòu)
Kubernetes 中的容器運(yùn)行時(shí)組成
按照不同的功能可以分為四個(gè)部分:
(1)kubelet 中容器運(yùn)行時(shí)的管理,kubeGenericRuntimeManager,它管理與 CRI shim 通信的客戶端,完成容器和鏡像的管理(代碼位置:pkg/kubelet/kuberuntime/kuberuntime_manager.go);
(2)容器運(yùn)行時(shí)接口 CRI,包括了容器運(yùn)行時(shí)客戶端接口與容器運(yùn)行時(shí)服務(wù)端接口;
(3)CRI shim 客戶端,kubelet 持有,用于與 CRI shim 服務(wù)端進(jìn)行通信;
(4)CRI shim 服務(wù)端,即具體的容器運(yùn)行時(shí)實(shí)現(xiàn),包括 kubelet 內(nèi)置的 dockershim (代碼位置:pkg/kubelet/dockershim)以及外部的容器運(yùn)行時(shí)remote。如 cri-containerd(用于支持容器引擎containerd)、rktlet(用于支持容器引擎rkt)等。
更普遍的場景,就是你需要在每臺(tái)宿主機(jī)上單獨(dú)安裝一個(gè)負(fù)責(zé)響應(yīng) CRI 的組件。這個(gè)組件,一般被稱作 CRI shim。顧名思義,CRI shim 的工作,就是扮演 kubelet 與容器項(xiàng)目之間的“墊片”(shim)。所以它的作用非常單一,那就是實(shí)現(xiàn) CRI 規(guī)定的每個(gè)接口,然后把具體的 CRI 請(qǐng)求“翻譯”成對(duì)后端容器項(xiàng)目的請(qǐng)求或者操作。如下所示
CRI gRPC Server的具體實(shí)現(xiàn)
Container Runtime實(shí)現(xiàn)了CRI gRPC Server,包括RuntimeService和ImageService。該gRPC Server需要監(jiān)聽本地的Unix socket,而kubelet則作為gRPC Client運(yùn)行。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)。這其中包含了兩個(gè)gRPC服務(wù):
看一下源碼,Kubernetes 1.20中的CRI接口在api.proto中的定義如下:
- // Runtime service defines the public APIs for remote container runtimes
 - service RuntimeService {
 - // Version returns the runtime name, runtime version, and runtime API version.
 - rpc Version(VersionRequest) returns (VersionResponse) {}
 - // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
 - // the sandbox is in the ready state on success.
 - rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
 - // StopPodSandbox stops any running process that is part of the sandbox and
 - // reclaims network resources (e.g., IP addresses) allocated to the sandbox.
 - // If there are any running containers in the sandbox, they must be forcibly
 - // terminated.
 - // This call is idempotent, and must not return an error if all relevant
 - // resources have already been reclaimed. kubelet will call StopPodSandbox
 - // at least once before calling RemovePodSandbox. It will also attempt to
 - // reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
 - // multiple StopPodSandbox calls are expected.
 - rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
 - // RemovePodSandbox removes the sandbox. If there are any running containers
 - // in the sandbox, they must be forcibly terminated and removed.
 - // This call is idempotent, and must not return an error if the sandbox has
 - // already been removed.
 - rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
 - // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
 - // present, returns an error.
 - rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
 - // ListPodSandbox returns a list of PodSandboxes.
 - rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}
 - // CreateContainer creates a new container in specified PodSandbox
 - rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
 - // StartContainer starts the container.
 - rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
 - // StopContainer stops a running container with a grace period (i.e., timeout).
 - // This call is idempotent, and must not return an error if the container has
 - // already been stopped.
 - // The runtime must forcibly kill the container after the grace period is
 - // reached.
 - rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
 - // RemoveContainer removes the container. If the container is running, the
 - // container must be forcibly removed.
 - // This call is idempotent, and must not return an error if the container has
 - // already been removed.
 - rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
 - // ListContainers lists all containers by filters.
 - rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
 - // ContainerStatus returns status of the container. If the container is not
 - // present, returns an error.
 - rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
 - // UpdateContainerResources updates ContainerConfig of the container.
 - rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
 - // ReopenContainerLog asks runtime to reopen the stdout/stderr log file
 - // for the container. This is often called after the log file has been
 - // rotated. If the container is not running, container runtime can choose
 - // to either create a new log file and return nil, or return an error.
 - // Once it returns error, new container log file MUST NOT be created.
 - rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}
 - // ExecSync runs a command in a container synchronously.
 - rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
 - // Exec prepares a streaming endpoint to execute a command in the container.
 - rpc Exec(ExecRequest) returns (ExecResponse) {}
 - // Attach prepares a streaming endpoint to attach to a running container.
 - rpc Attach(AttachRequest) returns (AttachResponse) {}
 - // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
 - rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}
 - // ContainerStats returns stats of the container. If the container does not
 - // exist, the call returns an error.
 - rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {}
 - // ListContainerStats returns stats of all running containers.
 - rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {}
 - // PodSandboxStats returns stats of the pod. If the pod sandbox does not
 - // exist, the call returns an error.
 - rpc PodSandboxStats(PodSandboxStatsRequest) returns (PodSandboxStatsResponse) {}
 - // ListPodSandboxStats returns stats of the pods matching a filter.
 - rpc ListPodSandboxStats(ListPodSandboxStatsRequest) returns (ListPodSandboxStatsResponse) {}
 - // UpdateRuntimeConfig updates the runtime configuration based on the given request.
 - rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {}
 - // Status returns the status of the runtime.
 - rpc Status(StatusRequest) returns (StatusResponse) {}
 - }
 - // ImageService defines the public APIs for managing images.
 - service ImageService {
 - // ListImages lists existing images.
 - rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
 - // ImageStatus returns the status of the image. If the image is not
 - // present, returns a response with ImageStatusResponse.Image set to
 - // nil.
 - rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {}
 - // PullImage pulls an image with authentication config.
 - rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
 - // RemoveImage removes the image.
 - // This call is idempotent, and must not return an error if the image has
 - // already been removed.
 - rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
 - // ImageFSInfo returns information of the filesystem that is used to store images.
 - rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {}
 - }
 
RuntimeService
RuntimeService 則提供了更多的接口,按照功能可以劃分為四組:
- PodSandbox 的管理接口: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,而不是直接跟容器交互;
 
狀態(tài)接口:包括查詢 API 版本和查詢運(yùn)行時(shí)狀態(tài)。
ImageService
管理鏡像的 ImageService 提供了 5 個(gè)接口:
- 查詢鏡像列表;
 - 拉取鏡像到本地;
 - 查詢鏡像狀態(tài);
 - 刪除本地鏡像;
 - 查詢鏡像占用空間等。
 
這些都很容易映射到 Docker API 或者CRI上面。
CRI相關(guān)初始化
跟容器最相關(guān)的一個(gè) Manager 是 Generic Runtime Manager,就是一個(gè)通用的運(yùn)行時(shí)管理器。我們可以看到目前 dockershim 還是存在于 Kubelet 的代碼中的,它是當(dāng)前性能最穩(wěn)定的一個(gè)容器運(yùn)行時(shí)的實(shí)現(xiàn)。remote 指的就是 CRI 接口。CRI 接口主要包含兩個(gè)部分:
- 一個(gè)是 CRI Server,即通用的比如說創(chuàng)建、刪除容器這樣的接口;
 - 另外一個(gè)是流式數(shù)據(jù)的接口 Streaming Server,比如 exec、port-forward 這些流式數(shù)據(jù)的接口。
 
CNI(容器網(wǎng)絡(luò)接口)也是在 CRI 進(jìn)行操作的,因?yàn)槲覀冊(cè)趧?chuàng)建 Pod 的時(shí)候需要同時(shí)創(chuàng)建網(wǎng)絡(luò)資源然后注入到 Pod 中。接下來就是我們的容器和鏡像。我們通過具體的容器創(chuàng)建引擎來創(chuàng)建一個(gè)具體的容器。kubelet中CRI相關(guān)初始化邏輯如下:
(1)當(dāng)kubelet選用dockershim作為容器運(yùn)行時(shí),則初始化并啟動(dòng)容器運(yùn)行時(shí)服務(wù)端dockershim(初始化dockershim過程中也會(huì)初始化網(wǎng)絡(luò)插件CNI)。
- 如果是外部外部容器運(yùn)行時(shí)的時(shí)候,需要在每臺(tái)宿主機(jī)上單獨(dú)安裝一個(gè)負(fù)責(zé)響應(yīng) CRI 的組件。這個(gè)組件就是CRI shim,需要包含網(wǎng)絡(luò)插件CNI。比如支持containerd的CRI-Containerd的shim。到了 containerd 1.1 版本后就去掉了 CRI-Containerd 這個(gè) shim,直接把適配邏輯作為插件的方式集成到了 containerd 主進(jìn)程中,所以我們現(xiàn)在可以直接使用--container-runtime-endpoint=unix:///run/containerd/containerd.sock這個(gè)套接字,就可以無縫切換的containerd。
 
(2)初始化容器運(yùn)行時(shí)CRI shim客戶端(用于調(diào)用CRI shim服務(wù)端:內(nèi)置的容器運(yùn)行時(shí)dockershim或remote容器運(yùn)行時(shí));
(3)初始化Generic Runtime Manager,用于容器運(yùn)行時(shí)的管理。初始化完成后,后續(xù)kubelet對(duì)容器以及鏡像的相關(guān)操作都會(huì)通過該結(jié)構(gòu)體持有的CRI shim客戶端,與CRI shim服務(wù)端進(jìn)行通信來完成。
下面來簡單分析幾個(gè)比較重要的CRI相關(guān)啟動(dòng)參數(shù):(1)--container-runtime:指定kubelet要使用的容器運(yùn)行時(shí),可選值docker、remote、rkt (deprecated),默認(rèn)值為docker,即使用kubelet內(nèi)置的容器運(yùn)行時(shí)dockershim。當(dāng)需要使用外部容器運(yùn)行時(shí),該參數(shù)配置為remote,并設(shè)置--container-runtime-endpoint參數(shù)值為監(jiān)聽的 unix socket位置。(2)--runtime-cgroups:容器運(yùn)行時(shí)使用的cgroups,可選值。(3)--docker-endpoint:docker暴露服務(wù)的socket地址,默認(rèn)值為unix:///var/run/docker.sock,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時(shí)有效。(4)--pod-infra-container-image:pod sandbox的鏡像地址,默認(rèn)值為k8s.gcr.io/pause:3.5,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時(shí)有效。(5)--image-pull-progress-deadline:容器鏡像拉取超時(shí)時(shí)間,默認(rèn)值為1分鐘,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時(shí)有效。(6)--experimental-dockershim:設(shè)置為true時(shí),啟用dockershim模式,只啟動(dòng)dockershim,默認(rèn)值為false,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時(shí)有效。(7)--experimental-dockershim-root-directory:dockershim根目錄,默認(rèn)值為/var/lib/dockershim,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時(shí)有效。(8)--container-runtime-endpoint:容器運(yùn)行時(shí)的endpoint,linux中默認(rèn)值為unix:///var/run/dockershim.sock,注意與上面的--docker-endpoint區(qū)分開來。
- unix:///var/run/dockershim.sock
 - unix:///run/containerd/containerd.sock,即使用本地的containerd作為容器運(yùn)行時(shí)。
 - 默認(rèn)是unix:///var/run/dockershim.sock,即默認(rèn)使用本地的docker作為容器運(yùn)行時(shí)。
 
(簡單介紹一下socket通信之Unix domain socket:Unix domain socket 又叫 IPC(inter-process communication 進(jìn)程間通信。用于實(shí)現(xiàn)同一主機(jī)上的進(jìn)程間通信。socket 原本是為網(wǎng)絡(luò)通訊設(shè)計(jì)的,但后來在 socket 的框架上發(fā)展出一種 IPC 機(jī)制,就是 UNIX domain socket。雖然網(wǎng)絡(luò) socket 也可用于同一臺(tái)主機(jī)的進(jìn)程間通訊(通過 loopback 地址 127.0.0.1),但是 UNIX domain socket 用于 IPC 更有效率:不需要經(jīng)過網(wǎng)絡(luò)協(xié)議棧,不需要打包拆包、計(jì)算校驗(yàn)和、維護(hù)序號(hào)和應(yīng)答等,只是將應(yīng)用層數(shù)據(jù)從一個(gè)進(jìn)程拷貝到另一個(gè)進(jìn)程。這是因?yàn)?,IPC 機(jī)制本質(zhì)上是可靠的通訊,而網(wǎng)絡(luò)協(xié)議是為不可靠的通訊設(shè)計(jì)的。)
(9)--image-service-endpoint:鏡像服務(wù)的endpoint,linux中默認(rèn)值為unix:///var/run/dockershim.sock。
當(dāng)前支持的CRI后端
我們最初在使用Kubernetes時(shí)通常會(huì)默認(rèn)使用Docker作為容器運(yùn)行時(shí),其實(shí)從Kubernetes 1.5開始已經(jīng)開始支持CRI,目前是處于Alpha版本,通過CRI接口可以指定使用其它容器運(yùn)行時(shí)作為Pod的后端,docker、containerd、CRI-O、Frakti、pouch,它們銜接Kubelet與運(yùn)行時(shí)方式對(duì)比如下:
棄用 docker 后到底會(huì)產(chǎn)生什么影響
正常的 K8s 用戶不會(huì)有任何影響
生產(chǎn)環(huán)境中高版本的集群只需要把運(yùn)行時(shí)從 docker 切換到 containerd即可。containerd 是 docker 中的一個(gè)底層組件,主要負(fù)責(zé)維護(hù)容器的生命周期,跟隨 docker 經(jīng)歷了長期考驗(yàn)。同時(shí) 2019年初就從 CNCF 畢業(yè),可以單獨(dú)作為容器運(yùn)行時(shí)用在集群中。到了 containerd 1.1 版本后就去掉了 CRI-Containerd 這個(gè) shim,直接把適配邏輯作為插件的方式集成到了 containerd 主進(jìn)程中,所以我們現(xiàn)在可以直接使用--container-runtime-endpoint=unix:///run/containerd/containerd.sock這個(gè)套接字,就可以無縫切換的containerd。因此把 runtime 從 docker 轉(zhuǎn)換到 containerd 是一個(gè)基本無痛的過程。
- 開發(fā)環(huán)境中通過docker build構(gòu)建出來的鏡像依然可以在集群中使用鏡像一直是容器生態(tài)的一大優(yōu)勢,雖然人們總是把鏡像稱之為“docker鏡像”,但鏡像早就成為了一種規(guī)范了。具體規(guī)范可以參考image-spec。在任何地方只要構(gòu)建出符合 Image Spec 的鏡像,就可以拿到其他符合 Image Spec 的容器運(yùn)行時(shí)上運(yùn)行。如果你是一名開發(fā)/運(yùn)維人員,你依舊可以繼續(xù)使用 Docker 來構(gòu)建鏡像,以相同的方式將鏡像推送到 Registry,并且將這些鏡像部署到你的 Kubernetes 中;如果你是運(yùn)行和操作集群的用戶,你只需要將 Docker 切換成你需要的containerd 容器運(yùn)行時(shí)即可。
 - 在 Pod 中使用 DinD(Docker in Docker)的用戶會(huì)受到影響
 
有些使用者會(huì)把 docker 的 socket (/run/docker.sock)掛載到 Pod 中,并在 Pod 中調(diào)用 docker 的 api 構(gòu)建鏡像或創(chuàng)建編譯容器等,官方在這里的建議是使用 Kaniko、Img 或 Buildah。
2.我們可以通過把 docker daemon 作為 DaemonSet 或者給想要使用 docker 的 Pod 添加一個(gè) docker daemon 的 sidecar 的方式在任意運(yùn)行時(shí)中使用 DinD 的方案。
3.同一集群中docker 節(jié)點(diǎn)與 containerd 節(jié)點(diǎn)共存,通過按節(jié)點(diǎn)標(biāo)簽調(diào)度,保 證這類業(yè)務(wù)調(diào)度到 docker 節(jié)點(diǎn)沒有通過上述方案。
預(yù)告
后期會(huì)圍繞runc,shim等探索容器的底層實(shí)現(xiàn)與管理API的暴露。敬請(qǐng)期待!!!!
reference
https://feisky.xyz/posts/kubernetes-container-runtime/https://jimmysong.io/kubernetes-handbook/concepts/cri.htmlhttps://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2221-remove-dockershimhttps://kubernetes.io/zh/docs/setup/production-environment/container-runtimes/https://www.qikqiak.com/post/containerd-usage/https://kubernetes.io/zh/blog/2020/12/02/dockershim-faq/https://github.com/containerdhttps://www.zhihu.com/question/324124344




















 
 
 







 
 
 
 