Skip to content

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

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

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

今天我们就来了解一下。

容器运行时 (Container Runtime) ​

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

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

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

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

图 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 的接口:

java
// 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,主要负责容器镜像的生命周期管理,比如拉取镜像、删除镜像、查询镜像等等,如下所示:

java
// 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 是如何和容器运行时进行交互的,见下图:

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

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

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

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

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

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

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

图 5:部署 containerd

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

写在最后 ​

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

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

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

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