Kubernetes CRI - 容器運(yùn)行時接口解析
本文轉(zhuǎn)載自微信公眾號「運(yùn)維開發(fā)故事」,作者沒有文案的夏老師。轉(zhuǎn)載本文請聯(lián)系運(yùn)維開發(fā)故事公眾號。
kubelet 的組件
kubelet 本身,也是按照“控制器”模式來工作的。它實(shí)際的工作原理,可以用如下所示的一幅示意圖來表示清楚。
- Kubelet Server 對外提供 API,供 kube-apiserver、metrics-server 等服務(wù)調(diào)用。比如 kubectl exec 時需要通過 Kubelet API /exec/{token} 與容器進(jìn)行交互;
- Container Manager 管理容器的各種資源,比如 CGroups、QoS、cpuset、device 等;
- Volume Manager 管理容器的存儲卷,比如格式化磁盤、掛載到 Node 本地、最后再將掛載路徑傳給容器;
- Eviction 負(fù)責(zé)容器的驅(qū)逐,比如在資源不足時驅(qū)逐優(yōu)先級低的容器,保證高優(yōu)先級容器的運(yùn)行;
- cAdvisor 負(fù)責(zé)為容器提供 Metrics;
- Metrics 和 stats 提供容器和節(jié)點(diǎn)的度量數(shù)據(jù),比如 metrics-server 通過 /stats/summary 提取的度量數(shù)據(jù)是 HPA 自動擴(kuò)展的依據(jù);
- Generic Runtime Manager 是容器運(yùn)行時的管理者,負(fù)責(zé)與CRI 交互,完成容器和鏡像的管理;
CRI中定義了容器和鏡像的服務(wù)的接口,因?yàn)槿萜鬟\(yùn)行時與鏡像的生命周期是彼此隔離的,因此需要定義兩個服務(wù)。該接口使用Protocol Buffer,基于gRPC,在Kubernetes v1.10+版本中是在pkg/kubelet/apis/cri/runtime/v1alpha2的api.proto中定義的。
CRI架構(gòu)
Kubernetes 中的容器運(yùn)行時組成
按照不同的功能可以分為四個部分:
(1)kubelet 中容器運(yùn)行時的管理,kubeGenericRuntimeManager,它管理與 CRI shim 通信的客戶端,完成容器和鏡像的管理(代碼位置:pkg/kubelet/kuberuntime/kuberuntime_manager.go);
(2)容器運(yùn)行時接口 CRI,包括了容器運(yùn)行時客戶端接口與容器運(yùn)行時服務(wù)端接口;
(3)CRI shim 客戶端,kubelet 持有,用于與 CRI shim 服務(wù)端進(jìn)行通信;
(4)CRI shim 服務(wù)端,即具體的容器運(yùn)行時實(shí)現(xiàn),包括 kubelet 內(nèi)置的 dockershim (代碼位置:pkg/kubelet/dockershim)以及外部的容器運(yùn)行時remote。如 cri-containerd(用于支持容器引擎containerd)、rktlet(用于支持容器引擎rkt)等。
更普遍的場景,就是你需要在每臺宿主機(jī)上單獨(dú)安裝一個負(fù)責(zé)響應(yīng) CRI 的組件。這個組件,一般被稱作 CRI shim。顧名思義,CRI shim 的工作,就是扮演 kubelet 與容器項(xiàng)目之間的“墊片”(shim)。所以它的作用非常單一,那就是實(shí)現(xiàn) CRI 規(guī)定的每個接口,然后把具體的 CRI 請求“翻譯”成對后端容器項(xià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 兩個服務(wù),這兩個服務(wù)可以在一個 gRPC server 中實(shí)現(xiàn),也可以分開成兩個獨(dú)立服務(wù)。目前社區(qū)的很多運(yùn)行時都是將其在一個 gRPC server 里面實(shí)現(xiàn)。這其中包含了兩個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 是對 Kubernete Pod 的抽象,用來給容器提供一個隔離的環(huán)境(比如掛載到相同的 CGroup 下面),并提供網(wǎng)絡(luò)等共享的命名空間。PodSandbox 通常對應(yīng)到一個 Pause 容器或者一臺虛擬機(jī);
- Container 的管理接口:在指定的 PodSandbox 中創(chuàng)建、啟動、停止和刪除容器;
- Streaming API 接口:包括 Exec、Attach 和 PortForward 等三個和容器進(jìn)行數(shù)據(jù)交互的接口,這三個接口返回的是運(yùn)行時 Streaming Server 的 URL,而不是直接跟容器交互;
狀態(tài)接口:包括查詢 API 版本和查詢運(yùn)行時狀態(tài)。
ImageService
管理鏡像的 ImageService 提供了 5 個接口:
- 查詢鏡像列表;
- 拉取鏡像到本地;
- 查詢鏡像狀態(tài);
- 刪除本地鏡像;
- 查詢鏡像占用空間等。
這些都很容易映射到 Docker API 或者CRI上面。
CRI相關(guān)初始化
跟容器最相關(guān)的一個 Manager 是 Generic Runtime Manager,就是一個通用的運(yùn)行時管理器。我們可以看到目前 dockershim 還是存在于 Kubelet 的代碼中的,它是當(dāng)前性能最穩(wěn)定的一個容器運(yùn)行時的實(shí)現(xiàn)。remote 指的就是 CRI 接口。CRI 接口主要包含兩個部分:
- 一個是 CRI Server,即通用的比如說創(chuàng)建、刪除容器這樣的接口;
- 另外一個是流式數(shù)據(jù)的接口 Streaming Server,比如 exec、port-forward 這些流式數(shù)據(jù)的接口。
CNI(容器網(wǎng)絡(luò)接口)也是在 CRI 進(jìn)行操作的,因?yàn)槲覀冊趧?chuàng)建 Pod 的時候需要同時創(chuàng)建網(wǎng)絡(luò)資源然后注入到 Pod 中。接下來就是我們的容器和鏡像。我們通過具體的容器創(chuàng)建引擎來創(chuàng)建一個具體的容器。kubelet中CRI相關(guān)初始化邏輯如下:
(1)當(dāng)kubelet選用dockershim作為容器運(yùn)行時,則初始化并啟動容器運(yùn)行時服務(wù)端dockershim(初始化dockershim過程中也會初始化網(wǎng)絡(luò)插件CNI)。
- 如果是外部外部容器運(yùn)行時的時候,需要在每臺宿主機(jī)上單獨(dú)安裝一個負(fù)責(zé)響應(yīng) CRI 的組件。這個組件就是CRI shim,需要包含網(wǎng)絡(luò)插件CNI。比如支持containerd的CRI-Containerd的shim。到了 containerd 1.1 版本后就去掉了 CRI-Containerd 這個 shim,直接把適配邏輯作為插件的方式集成到了 containerd 主進(jìn)程中,所以我們現(xiàn)在可以直接使用--container-runtime-endpoint=unix:///run/containerd/containerd.sock這個套接字,就可以無縫切換的containerd。
(2)初始化容器運(yùn)行時CRI shim客戶端(用于調(diào)用CRI shim服務(wù)端:內(nèi)置的容器運(yùn)行時dockershim或remote容器運(yùn)行時);
(3)初始化Generic Runtime Manager,用于容器運(yùn)行時的管理。初始化完成后,后續(xù)kubelet對容器以及鏡像的相關(guān)操作都會通過該結(jié)構(gòu)體持有的CRI shim客戶端,與CRI shim服務(wù)端進(jìn)行通信來完成。
下面來簡單分析幾個比較重要的CRI相關(guān)啟動參數(shù):(1)--container-runtime:指定kubelet要使用的容器運(yùn)行時,可選值docker、remote、rkt (deprecated),默認(rèn)值為docker,即使用kubelet內(nèi)置的容器運(yùn)行時dockershim。當(dāng)需要使用外部容器運(yùn)行時,該參數(shù)配置為remote,并設(shè)置--container-runtime-endpoint參數(shù)值為監(jiān)聽的 unix socket位置。(2)--runtime-cgroups:容器運(yùn)行時使用的cgroups,可選值。(3)--docker-endpoint:docker暴露服務(wù)的socket地址,默認(rèn)值為unix:///var/run/docker.sock,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時有效。(4)--pod-infra-container-image:pod sandbox的鏡像地址,默認(rèn)值為k8s.gcr.io/pause:3.5,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時有效。(5)--image-pull-progress-deadline:容器鏡像拉取超時時間,默認(rèn)值為1分鐘,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時有效。(6)--experimental-dockershim:設(shè)置為true時,啟用dockershim模式,只啟動dockershim,默認(rèn)值為false,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時有效。(7)--experimental-dockershim-root-directory:dockershim根目錄,默認(rèn)值為/var/lib/dockershim,該參數(shù)配置當(dāng)且僅當(dāng)--container-runtime參數(shù)值為docker時有效。(8)--container-runtime-endpoint:容器運(yùn)行時的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)行時。
- 默認(rèn)是unix:///var/run/dockershim.sock,即默認(rèn)使用本地的docker作為容器運(yùn)行時。
(簡單介紹一下socket通信之Unix domain socket:Unix domain socket 又叫 IPC(inter-process communication 進(jìn)程間通信。用于實(shí)現(xiàn)同一主機(jī)上的進(jìn)程間通信。socket 原本是為網(wǎng)絡(luò)通訊設(shè)計的,但后來在 socket 的框架上發(fā)展出一種 IPC 機(jī)制,就是 UNIX domain socket。雖然網(wǎng)絡(luò) socket 也可用于同一臺主機(jī)的進(jìn)程間通訊(通過 loopback 地址 127.0.0.1),但是 UNIX domain socket 用于 IPC 更有效率:不需要經(jīng)過網(wǎng)絡(luò)協(xié)議棧,不需要打包拆包、計算校驗(yàn)和、維護(hù)序號和應(yīng)答等,只是將應(yīng)用層數(shù)據(jù)從一個進(jìn)程拷貝到另一個進(jìn)程。這是因?yàn)?,IPC 機(jī)制本質(zhì)上是可靠的通訊,而網(wǎng)絡(luò)協(xié)議是為不可靠的通訊設(shè)計的。)
(9)--image-service-endpoint:鏡像服務(wù)的endpoint,linux中默認(rèn)值為unix:///var/run/dockershim.sock。
當(dāng)前支持的CRI后端
我們最初在使用Kubernetes時通常會默認(rèn)使用Docker作為容器運(yùn)行時,其實(shí)從Kubernetes 1.5開始已經(jīng)開始支持CRI,目前是處于Alpha版本,通過CRI接口可以指定使用其它容器運(yùn)行時作為Pod的后端,docker、containerd、CRI-O、Frakti、pouch,它們銜接Kubelet與運(yùn)行時方式對比如下:
棄用 docker 后到底會產(chǎn)生什么影響
正常的 K8s 用戶不會有任何影響
生產(chǎn)環(huán)境中高版本的集群只需要把運(yùn)行時從 docker 切換到 containerd即可。containerd 是 docker 中的一個底層組件,主要負(fù)責(zé)維護(hù)容器的生命周期,跟隨 docker 經(jīng)歷了長期考驗(yàn)。同時 2019年初就從 CNCF 畢業(yè),可以單獨(dú)作為容器運(yùn)行時用在集群中。到了 containerd 1.1 版本后就去掉了 CRI-Containerd 這個 shim,直接把適配邏輯作為插件的方式集成到了 containerd 主進(jìn)程中,所以我們現(xiàn)在可以直接使用--container-runtime-endpoint=unix:///run/containerd/containerd.sock這個套接字,就可以無縫切換的containerd。因此把 runtime 從 docker 轉(zhuǎn)換到 containerd 是一個基本無痛的過程。
- 開發(fā)環(huán)境中通過docker build構(gòu)建出來的鏡像依然可以在集群中使用鏡像一直是容器生態(tài)的一大優(yōu)勢,雖然人們總是把鏡像稱之為“docker鏡像”,但鏡像早就成為了一種規(guī)范了。具體規(guī)范可以參考image-spec。在任何地方只要構(gòu)建出符合 Image Spec 的鏡像,就可以拿到其他符合 Image Spec 的容器運(yùn)行時上運(yùn)行。如果你是一名開發(fā)/運(yùn)維人員,你依舊可以繼續(xù)使用 Docker 來構(gòu)建鏡像,以相同的方式將鏡像推送到 Registry,并且將這些鏡像部署到你的 Kubernetes 中;如果你是運(yùn)行和操作集群的用戶,你只需要將 Docker 切換成你需要的containerd 容器運(yùn)行時即可。
- 在 Pod 中使用 DinD(Docker in Docker)的用戶會受到影響
有些使用者會把 docker 的 socket (/run/docker.sock)掛載到 Pod 中,并在 Pod 中調(diào)用 docker 的 api 構(gòu)建鏡像或創(chuàng)建編譯容器等,官方在這里的建議是使用 Kaniko、Img 或 Buildah。
2.我們可以通過把 docker daemon 作為 DaemonSet 或者給想要使用 docker 的 Pod 添加一個 docker daemon 的 sidecar 的方式在任意運(yùn)行時中使用 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ù)告
后期會圍繞runc,shim等探索容器的底層實(shí)現(xiàn)與管理API的暴露。敬請期待!!!!
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