25-稳定基石:带你剖析容器运行时以及 CRI 原理.md

发布时间 2024-01-11 13:49:26作者: miao酱

当一个 Pod 在 Kube-APIServer 中被创建出来以后,会被调度器调度,然后确定一个合适的节点,最终被这个节点上的 Kubelet 拉起,以容器状态运行。

那么 Kubelet 是如何跟容器打交道的呢,它是如何进行创建容器、获取容器状态等操作的呢?

今天我们就来了解一下。

容器运行时 (Container Runtime)

Kubelet 负责运行具体的 Pod,并维护其整个生命周期,为 Pod 提供存储、网络等必要的资源。但 Kubelet 本身并不负责真正的容器创建和逻辑管理,这些全部都是通过容器运行时(Container Runtime)完成的。大家平常熟知的 Docker 其实就是一种容器运行时,除此之外,还有containerdcri-okatagVisor 等等。

下图就是 Kubelet 跟容器运行时进行的交互图:

Drawing 0.png

图 1:Kubelet 跟容器运行的交互图

Kubelet 负责跟 kube-apiserver 进行数据同步,获取新的 Pod,并上报本机 Pod 的各个状态数据。Kubelet 通过调用容器运行时的接口完成容器的创建、容器状态的查询等工作。下图就是使用 Docker 作为容器的运行时。

Drawing 1.png
图 2:使用 Docker 作为容器的运行

Docker 作为 Kubelet 内置支持的主要容器运行时,也是目前使用最为官方的容器运行时之一。

除了 Docker,在 Kubernetes v1.5 之前,Kubelet 还内置了对 rkt 的支持。在这个阶段,如果我们想要自己去定义容器运行时,或者更改容器运行时的部分逻辑行为,是非常痛苦的,需要通过修改 Kubelet 的代码来实现。这些改动如果更新到上游社区,也会给社区造成很大的困扰,毕竟 Kubelet 自身的稳定性关乎着整个集群的稳定性。因此,这些改动在上游社区的合并通常都很谨慎,往往就需要开发者自己维护这些代码,维护成本非常高,也不方便升级。

介于这一点,很多开发者都希望 Kubernetes 可以支持更多的容器运行时。因此,从 v1.5 版本开始,社区引入了 CRI(Container Runtime Interface)来解决这个问题。

CRI 接口的引入带来了两个好处:一是它很好地将 Kubelet 与容器运行时进行了解耦,这样我们每次对容器运行时进行更新升级等操作时,都再不需要对 Kubelet 做任何的更改;二是解放了 Kubelet,减少了 Kubelet 的负担,能够保证 Kubernetes 的代码质量和整个系统的稳定性。

下面我们就来了解一下CRI。

CRI

CRI 接口可以分为两部分。

一个是容器运行时服务 RuntimeService,它主要负责管理 Pod 和容器的生命周期,比如创建容器、删除容器、查询容器状态等等。下面就是用Protocol Buffers定义的 RuntimeService 的接口:

// 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) {}
    // Start a sandbox pod which was forced to stop by external factors.
    // Network plugin returns same IPs when input same pod names and namespaces
    rpc StartPodSandbox(StartPodSandboxRequest) returns (StartPodSandboxResponse) {}
    // 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) {}
    // PauseContainer pauses the container.
    rpc PauseContainer(PauseContainerRequest) returns (PauseContainerResponse) {}
    // UnpauseContainer unpauses the container.
    rpc UnpauseContainer(UnpauseContainerRequest) returns (UnpauseContainerResponse) {}
    // 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,主要负责容器镜像的生命周期管理,比如拉取镜像、删除镜像、查询镜像等等,如下所示:

// 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 shim,即完成对 CRI 这个抽象接口的具体实现。这样容器运行时就可以接收来自 Kubelet 的请求。

我们现在就来看看有了 CRI 接口以后,Kubelet 是如何和容器运行时进行交互的,见下图:

Drawing 2.png

图 3:Kubelet 与容器运行时的交互)

从上图可以看出,新增的 CRI shim 是 Kubelet 和容器运行时之间的交互纽带,Kubelet 只需要跟 CRI shim 进行交互。Kubelet 调用 CRI shim 的接口,CRI shim 响应请求后会调用底层的运行容器时,完成对容器的相关操作。

这里我们需要将 Kubelet、CRI shim 以及容器运行时都部署在同一个节点上。一般来说,大多数的容器运行时都默认实现了 CRI 的接口,比如containerd

目前 Kubelet 内部内置了对 Docker 的 CRI shim 的实现,见下图:

Drawing 4.png
图 4:Kubelet 内置对 CRI shim 的实现

而对于其他的容器运行时,比如containerd,我们就需要配置 kubelet 的 --container-runtime 参数为 remote,并设置 --container-runtime-endpoint 为对应的容器运行时的监听地址。

Kubernetes 自 v1.10 版本已经完成了和 containerd 1.1版本 的 GA 集成,你可以直接按照这份文档来部署 containerd 作为你的容器运行时。

Drawing 5.png

图 5:部署 containerd

containerd 1.1 版本已经内置了对 CRI 的实现,比直接使用 Docker 的性能要高很多。

写在最后

Kubernetes 作为容器编排调度领域的事实标准,其优秀的架构设计还体现在其可扩展接口上。比如 CRI 提供了简单易用的扩展接口,方便各个容器运行时跟 Kubelet 进行交互接入,极大地方便了用户进行定制化。

通过 CRI 对容器运行时进行抽象,我们无须修改 Kubelet 就可以天然地支持多种容器运行时,这极大地方便了开发者的对接,也减少了升级和维护成本。CRI 的出现也促进了容器运行时的繁荣,也为强隔离、多租户等复杂的场景带来了更多的选择。

除了 CRI 以外,在 Kubernetes 中还可以为不同的 Pod 设置不同的容器运行时(Container Runtime),以提供性能与安全性之间的平衡。从 1.12 版本开始,Kuberentes 就提供了 RuntimeClass 来实现这个功能。你可以阅读这份官方文档,来学习如何使用 RuntimeClass。

如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。