kubernetes CRI 前世今生


  在學習kubernetes的過程中,我們會遇到CRI、CNI、CSI、OCI 等術語,本文試圖先通過分析k8s目前默認的一種容器運行時架構,來幫助我們更好理解k8s 運行時背后設計邏輯。進而引出CRI、OCI的提出背景。

一、k8s 架構

  我們在構建k8s集群的時候首先需要搭建master節點、其次需要創建node節點並將node節點加入到k8s集群中。當我們構建好k8s集群后,我們可以通過kubectl create -f  nginx.yml 命令的方式來創建應用對應的pod。當我們執行命令

后,命令會提交給API server,它會解析yml文件,並將其以API對象的形式存到 etcd里。這時master組件中的Controller Manager會通過控制循環的方式來做編排工作,創建應用所需要的Pod。Scheduler 會 watch etcd中新Pod 的變化。如

果他發現有一個新的Pod 出現,Scheduler會運行調度算法,通過調度算法最終選擇出最佳的Node節點,並將這個Node節點的名字寫到pod對象的NodeName字段上面,這一步就是所謂的Bind Pod to Node(下圖的標注),然后把bind的結果寫回到etcd。

       其次,當我們在構建k8s集群的時候,默認每個節點上都會初始化創建一個kubelet進程,kubelet進程的會watch etcd中的pod的變化,當kubelet進程watch到pod的bind的更新操作,並且bind的節點是本節點時,它會接管接下來的

所做的事情,如鏡像下載,容器創建等。

二、k8s 默認容器運行時架構

  接下來將通過k8s默認集成的容器運行時架構來看kublete如何創建一個容器(如下圖所示)。

1. kubelet 通過 CRI(Container Runtime Interface) 接口(gRPC) 調用 dockershim, 請求創建一個容器, 這一步中, Kubelet 可以視作一個簡單的 CRI Client, 而 dockershim 就是接收請求的 Server.

2. dockershim 收到請求后, 通過適配的方式,適配成 Docker Daemon 的請求格式, 發到 Docker Daemon 上請求創建一個容器。在docker 1.12后版本中,docker daemon被拆分成dockerd和containerd,containerd負責操作容器。

3. dockerd收到請求后, 調用containerd進程去創建一個容器。

4. containerd 收到請求后, 並不會自己直接去操作容器, 而是創建一個叫做 containerd-shim 的進程, 讓 containerd-shim 去操作容器. 創建containered-shim的目的主要有:

1)讓containerd-shim做諸如收集狀態, 維持 stdin 等 fd 打開等工作. 

2)允許容器運行時(runC)啟動容器后退出,不必為每個容器一直運行一個容器運行時runC。

3)即使在 containerd 和 dockerd 都掛掉的情況下,容器的標准 IO 和其它的文件描述符也都是可用的。

4)向 containerd 報告容器的退出狀態

5)在不中斷容器運行的情況下升級或重啟 dockerd

5. 而containerd-shim 在這一步需要調用 runC 這個命令行工具, 來啟動容器,runC是OCI(Open Container Initiative, 開放容器標准) 的一個參考實現。主要用來設置 namespaces 和 cgroups, 掛載 root filesystem等操作。

6.runC啟動完容器后本身會直接退出, containerd-shim 則會成為容器進程的父進程, 負責收集容器進程的狀態, 上報給 containerd, 並在容器中 pid 為 1 的進程退出后接管容器中的子進程進行清理, 確保不會出現僵屍進程(關閉進程描述符等)。

 

 三、容器與容器編排背景簡述 

       從k8s的容器運行時可以看出,kubelet啟動容器的過程經過了很長的一段調用鏈路。這個是由於在容器及編排領域各大廠商與docker之間的競爭以及docker公司為了搶占paas領域市場,對架構做出的一系列調整。其實 k8s 最開始

的運行時架構鏈路調用沒有這么復雜: kubelet 想要創建容器直接通過 docker api 調用 Docker Daemon,Docker Daemon 調 libcontainer 這個庫來啟動容器。為了防止docker壟斷以及受控docker運行時, 各大廠商於是就聯合起來制定出開

放容器標准OCI(Open Containers Initiative).大家可以基於這個標准開發自己的容器運行時。Docker公司則把 libcontainer做了一層封裝, 變成 runC 捐獻給CNCF作為 OCI 的參考實現.

        接下來就是 Docker 要搞 Swarm 進軍 PaaS 市場, 於是做了個架構切分, 把容器操作都移動到一個單獨的 Daemon 進程 containerd 中去, 讓 Docker Daemon 專門負責上層的封裝編排. 最終swarm敗給了k8s, 於是

Docker 公司就把 containerd 捐給 CNCF ,專注於搞 Docker 企業版了. 

      與此同時,容器領域,core os公司推出了個rkt容器運行時。希望 k8s 原生支持 rkt 作為運行時, 由於core os與google的關系,最終rkt運行時的支持在2016年也被合並進kubelet主干代碼里. 這樣做后反而給k8s中負責維護 kubelet 的小

組 SIG-Node帶來了更大的負擔,每一次kubelet的更新都要維護docker和rkt兩部分代碼。與此同時,隨着虛擬化技術強隔離容器技術runV(Kata Containers前身,后與intel clear container 合並)的逐漸成熟。k8s上游對虛擬化容器的支持很

快被提上了日程。為了從集成每一種運行時都要維護一份代碼中解放出來,k8s SIG-Node工作組決定對容器的操作統一地抽象成一個接口,這樣kubelet只需要跟這個接口

打交道,而具體地容器運行時,他們只需要實現該接口,並對kubelet暴露gRPC服務即可。這個統一地抽象地接口就是k8s中俗稱的 CRI。

 

四、CRI(容器運行時接口)

       CRI 基於 gRPC 定義了 RuntimeService 和 ImageService 等兩個 gRPC 服務,分別用於容器運行時和鏡像的管理。如下所示:

// 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.
    // TODO: what must the runtime do 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) {}

    // 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) {}
}

 

 

具體容器運行時則需要實現 CRI 定義的接口(即 gRPC server,通常稱為 CRI shim)。容器運行時在啟動 gRPC server 時需要監聽在本地的 Unix Socket (Windows 使用 tcp 格式)。

 

五、 容器運行時實現

  除了上面介紹的默認的容器運行時的實現,目前容器運行時主要有:

 

參考文檔:

https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1alpha2/api.proto

https://feisky.gitbooks.io/kubernetes/plugins/CRI.html

https://aleiwu.com/post/cncf-runtime-landscape/

https://www.infoq.cn/article/r*ikOvovTHhADAWw1Hb1

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM