浅析Kubernetes CRI

1、概述

在 CRI 出现之前(也就是 Kubernetes v1.5 之前),Docker 作为第一个容器运行时,Kubelet 通过硬编码的方式直接调用 Docker API,进而达到一个面向终态的效果。

在这之后,又出现了一种新的容器运行时 rkt,它也想要成为 Kubernetes 支持的一个容器运行时,当时它也合到了 Kubelet 的代码之中。这两个容器运行时的加入使得 Kubernetes 的代码越来越复杂、难以维护。之后 hyber.sh 加入社区,也想成为第三个容器运行时。

此时就有人站出来说,我们能不能对容器运行时的操作抽象出一个接口,将 Kubelet 代码与具体的容器运行时的实现代码解耦开,只要实现了这样一套接口,就能接入到 Kubernetes 的体系中,这就是我们后来见到的 Container Runtime Interface (CRI)。CRI 标准由 Google 和红帽主导推出,用于将 Kubernetes 平台和特定的容器运行时解耦。

CRI(Container Runtime Interface 容器运行时接口)本质上就是 Kubernetes 定义的一组与容器运行时进行交互的接口,所以只要实现了这套接口的容器运行时都可以对接到 Kubernetes 平台上来不过 Kubernetes 推出 CRI 这套标准的时候还没有现在的统治地位,所以有一些容器运行时介绍可能不会自身就去实现 CRI 接口,于是就有了 shim(垫片), 一个 shim 的职责就是作为适配器将各种容器运行时本身的接口适配到 Kubernetes 的 CRI 接口上,其中 dockershim 就是 Kubernetes 对接 Docker 到 CRI 接口上的一个垫片实现。

从上图可以看到,CRI 主要有 gRPC client、gRPC Server 和具体的容器运行时三个组件。其中 Kubelet 作为 gRPC 的客户端来调用 CRI 接口;CRI shim 作为 gRPC 服务端负责将 CRI 请求的内容转换为具体的容器运行时 API并响应 CRI 请求,在 Kubelet 和容器运行时之间充当翻译的角色。具体的容器创建逻辑是,Kubernetes 在通过调度指定一个具体的节点运行 Pod,该节点的 Kubelet 在接到 Pod 创建请求后,调用一个叫作 GenericRuntime 的通用组件来发起创建 Pod 的 CRI 请求给 CRI shim;CRI shim 监听一个端口来响应 Kubelet,在收到 CRI 请求后,将其转化为具体的容器运行时指令,并调用相应的容器运行时来创建 Pod。

2、CRI规范

根据概述可知,任何容器运行时想要接入 Kubernetes,都需要实现一个自己的 CRI shim,来实现 CRI 接口规范。(说明:随着 CRI 方案的发展以及其他容器运行时介绍对 CRI 的支持越来越完善,很多容器运行时直接去掉了shim这个代理层,直接以插件的形式实现CRI接口,这样使得调用链变得更加简洁,比如 containerd 1.1 )

那么 CRI 有哪些接口需要实现呢?

最新的 CRI 定义位于 Kubernetes 源码包 kubernetes/api.proto,主要定义了两类接口 ImageService 和 RuntimeService。

  • ImageService 主要定义拉取镜像、查看和删除镜像等操作。ImageService 的操作比较简单,就是拉取、删除、查看镜像状态及获取镜像列表这几个操作,下面我们着重介绍下RuntimeService 。
// 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 定义了容器相关的操作,包括管理容器的生命周期,以及与容器交互的调用(exec/attach/port-forward)等操作。
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 synchronously.
    // If runtime fails to transactionally update the requested resources, an error is returned.
    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 sandbox. If the pod sandbox does not
    // exist, the call returns an error.
    rpc PodSandboxStats(PodSandboxStatsRequest) returns (PodSandboxStatsResponse) {}
    // ListPodSandboxStats returns stats of the pod sandboxes 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) {}

    // CheckpointContainer checkpoints a container
    rpc CheckpointContainer(CheckpointContainerRequest) returns (CheckpointContainerResponse) {}

    // GetContainerEvents gets container events from the CRI runtime
    rpc  GetContainerEvents(GetEventsRequest) returns (stream ContainerEventResponse) {}

    // ListMetricDescriptors gets the descriptors for the metrics that will be returned in ListPodSandboxMetrics.
    // This list should be static at startup: either the client and server restart together when
    // adding or removing metrics descriptors, or they should not change.
    // Put differently, if ListPodSandboxMetrics references a name that is not described in the initial
    // ListMetricDescriptors call, then the metric will not be broadcasted.
    rpc ListMetricDescriptors(ListMetricDescriptorsRequest) returns (ListMetricDescriptorsResponse) {}

    // ListPodSandboxMetrics gets pod sandbox metrics from CRI Runtime
    rpc ListPodSandboxMetrics(ListPodSandboxMetricsRequest) returns (ListPodSandboxMetricsResponse) {}
}

从接口中可以看出 RuntimeService 除了有 container 的管理接口外,还包含 PodSandbox 相关的管理接口和exec 、attach 等与容器交互的接口。

PodSandbox 这个概念对应的是 Kubernetes 里的 Pod,它描述了 Kubernetes 里的 Pod 与容器运行相关的属性或者信息,如 HostName、CgroupParent 等,设计这个的初衷是因为 Pod 里所有容器的资源和环境信息是共享的,但是不同的容器运行时实现共享的机制不同,如 Docker 中 Pod 会是一个 Linux 命名空间,各容器网络信息的共享通过创建pause 容器的方法来实现,而 Kata Containers 则直接将 Pod 具化为一个轻量级的虚拟机,将这个逻辑抽象为PodSandbox 接口,可以让不同的容器运行时在 Pod 实现上自由发挥,自己解释和实现 Pod 的逻辑。

Exec 、 Attach 和 PortForward 是三个和容器进行数据交互的接口,由于交互数据需要长链接来传输,这些接口被称为 Streaming API 。CRI shim 依赖一套独立的 Streaming Server 机制来实现客户端与容器的交互需求。长连接比较消耗网络资源,为了避免因长链接给 Kubelet 节点带来网络流量瓶颈,CRI 要求容器运行时启动一个对应请求的单独的流服务器,让客户端直接与流服务器进行连同交互。

2.1 通过 CRI 操作容器的生命周期

 

  • 比方说我们通过 kubectl 命令来运行一个 Pod,那么 Kubelet 就会通过 CRI 执行以下操作:
  • 首先调用 RunPodSandbox 接口来创建一个 Pod 容器,Pod 容器是用来持有容器的相关资源的,比如说网络空间、PID空间、进程空间等资源;
  • 然后调用 CreatContainer 接口在 Pod 容器的空间创建业务容器;
  • 再调用 StartContainer 接口启动容器,相对应的销毁容器的接口为 StopContainer 与 RemoveContainer。

2.2 CRI streaming 接口

这里介绍一下 CRI 的流式接口 exec,它可以用来在容器内部执行一个命令。它的特别之处在于,一个是节省资源,另一个是连接的可靠性。

上图所示, kubectl exec 命令实现过程如下:

  1. 客户端发送 kubectl exec 命令给 apiserver;
  2. apiserver 调用 Kubelet 的 Exec API;
  3. Kubelet 调用 CRI 的 Exec 接口(具体的执行者为实现该接口的 CRI Shim );
  4. CRI Shim 向 Kubelet 返回 Streaming Server 的地址和端口;
  5. Kubelet 以 redirect 的方式返回给 apiserver
  6. apiserver 通过重定向来向 Streaming Server 发起真正的 /exec/{token} 请求,与它建立长连接,完成 Exec 的请求和响应。

3、Kubelet CRI架构

Kubelet 在引入 CRI 之后,主要的架构如上图所示。

跟容器最相关的一个 Manager 是 Generic Runtime Manager,就是一个通用的运行时管理器,负责发送容器创建、删除等CRI 请求;Container Runtime Interface(CRI) 负责定义 CRI 接口规范,具体的 CRI 实现可分为两种:Kubelet 内置的 dockershim 和  remote,remote 指的就是 CRI 接口。

CRI 接口主要包含两个部分:

  • 一个是 CRI Server,即通用的比如说创建、删除容器这样的接口;
  • 另外一个是流式数据的接口 Streaming Server,比如 exec、port-forward 这些流式数据的接口。

这里需要注意的是,我们的 CNI(容器网络接口)也是在 CRI 进行操作的,因为我们在创建 Pod 的时候需要同时创建网络资源然后注入到 Pod 中。接下来就是我们的容器和镜像。我们通过具体的容器创建引擎来创建一个具体的容器。

注意 1: dockershim 是 Kubernetes 自己实现的适配 Docker接口的 CRI 接口实现,主要用来将 CRI 请求里的内容组装成 Docker API 请求发给 Docker Daemon。在k8s 1.20之前,dockershim作为性能最稳定的一个容器运行时的实现一直存在于 Kubelet 源码中;在1.20 版本中将 Kubelet 中内置的 dockershim 代码分离,将内置的 dockershim 标记为维护模式;在在k8s 1.24版本,dockershim 从 Kubelet 源码中移除。

注意 2:远端的 CRI shim 主要是用来匹配其他的容器运行时工具到Kubelet。CRI shim 主要负责响应 Kubelet 发送的 CRI 请求,并将请求转化为具体的运行时命令发送给具体的运行时(如 runc、kata 等);Stream Server 用来响应客户端与容器的交互。常用的远端 CRI 的实现有 CRI-Containerd、CRI-O 等。

注意 3:随着 CRI 方案的发展以及其他容器运行时介绍对 CRI 的支持越来越完善,很多容器运行时直接去掉了shim这个代理层,直接以插件的形式实现CRI接口,这样使得调用链变得更加简洁,比如 containerd 1.1 

4、Kubernetes为何抛弃Docker

4.1 从Kubernetes源码中移除dockershim模块

正常是各容器运行时自己去实现CRI接口,然后对接到Kubernetes平台上面来,不过这里同样也有一个例外,那就是 Docker,由于 Docker 当时的江湖地位很高,Kubernetes 是直接内置了 dockershim 在 Kubelet 中的,所以如果你使用的是 Docker 这种容器运行时介绍的话是不需要单独去安装配置适配器之类的,当然这个举动似乎也麻痹了 Docker 公司。

 

现在如果我们使用的是 Docker 的话,当我们在 Kubernetes 中创建一个 Pod 的时候,首先就是 Kubelet 通过 CRI 接口调用 dockershim,请求创建一个容器,Kubelet 可以视作一个简单的 CRI Client, 而 dockershim 就是接收请求的 Server,不过他们都是在 Kubelet 内置的。

dockershim 收到请求后, 转化成 Docker Daemon 能识别的请求, 发到 Docker Daemon 上请求创建一个容器,请求到了 Docker Daemon 后续就是 Docker 创建容器的流程了,去调用 containerd,然后创建 containerd-shim 进程,通过该进程去调用 runc 去真正创建容器。

其实我们仔细观察也不难发现使用 Docker 的话其实是调用链比较长的,真正容器相关的操作其实 containerd 就完全足够了,Docker 太过于复杂笨重了,当然 Docker 深受欢迎的很大一个原因就是提供了很多对用户操作比较友好的功能,但是对于 Kubernetes 来说压根不需要这些功能,因为都是通过接口去操作容器的,所以自然也就可以将容器运行时介绍切换到 containerd 来。

切换到 containerd 可以消除掉中间环节,操作体验也和以前一样,但是由于直接用容器运行时调度容器,所以它们对 Docker 来说是不可见的。 因此,你以前用来检查这些容器的 Docker 工具就不能使用了。

你不能再使用 docker ps 或 docker inspect 命令来获取容器信息。由于不能列出容器,因此也不能获取日志、停止容器,甚至不能通过 docker exec 在容器中执行命令。

当然我们仍然可以下载镜像,或者用 docker build 命令构建镜像,但用 Docker 构建、下载的镜像,对于容器运行时介绍和 Kubernetes,均不可见。为了在 Kubernetes 中使用,需要把镜像推送到镜像仓库中去。

从上图可以看出在 containerd 1.0 中,对 CRI 的适配是通过一个单独的 CRI-Containerd 进程来完成的,这是因为最开始 containerd 还会去适配其他的系统(比如 swarm),所以没有直接实现 CRI,所以这个对接工作就交给 CRI-Containerd 这个 shim 了。

然后到了 containerd 1.1 版本后就去掉了 CRI-Containerd 这个 shim,直接把适配逻辑作为插件的方式集成到了 containerd 主进程中,现在这样的调用就更加简洁了。

与此同时 Kubernetes 社区也做了一个专门用于 Kubernetes 的 CRI 运行时 CRI-O,直接兼容 CRI 和 OCI 规范。 

这个方案和 containerd 的方案显然比默认的 dockershim 简洁很多,不过由于大部分用户都比较习惯使用 Docker,所以大家还是更喜欢使用 dockershim 方案。

注意 1:据说cri-o的性能没有containerd好,并且containerd是已经在生产里经受住了考验,至少目前不推荐使用cri-o,建议使用containerd;

随着 CRI 方案的发展,以及其他容器运行时介绍对 CRI 的支持越来越完善,Kubernetes 社区在2020年7月份就开始着手移除 dockershim 方案了:现在的移除计划是在 1.20 版本中将 Kubelet 中内置的 dockershim 代码分离,将内置的 dockershim 标记为维护模式,当然这个时候仍然还可以使用 dockershim,在 1.24 版本已经移出了dockershim 代码。

4.2 Kubernetes 1.24之后的如何使用docker作为容器运行时

从Kubernetes源码中移除dockershim模块是否就意味这 Kubernetes 不再支持 Docker 了呢?当然不是的,这只是废弃了内置的 dockershim 功能而已,Docker 和其他容器运行时介绍将一视同仁,不会单独对待内置支持。如果我们还想直接使用 Docker 这种容器运行时应该怎么办呢?

  1. 可以将 dockershim 的功能单独提取出来独立维护一个 cri-dockerd 即可,就类似于 containerd 1.0 版本中提供的 CRI-Containerd,目前github上已经实现了 cri-dockerd 项目 GitHub - Mirantis/cri-dockerd 。
  2. 当然还有一种办法就是 Docker 官方社区将 CRI 接口内置到 Dockerd 中去实现。但是我们也清楚 Dockerd 也是去直接调用的 Containerd,而 containerd 1.1 版本后就内置实现了 CRI,所以 Docker 也没必要再去单独实现 CRI 了。当 Kubernetes 不再内置支持开箱即用的 Docker 的以后,最好的方式当然也就是直接使用 Containerd 这种容器运行时,而且该容器运行时介绍也已经经过了生产环境实践的。

在Kubernetes使用docker作为容器运行时缺点(我本人也是不建议在Kubernetes使用docker作为容器运行时的):

  1. docker太重了,它有一个非常厚重的docker daemon, 其稳定性不是很好,所以现在既然它已经走了标准协议了,那么k8s所谓的不支持docker,其实,它可以通过,更轻量级,性能更好,更稳定的容器运行时介绍,例如containerd来替换docker,即docker本身已经在k8s这里已经没有存在的必要了。

  2. Docker内部调用链比较复杂,多层封装和调用,导致性能降低、提升故障率、不易排查;

  3. Docker还会在宿主机创建网络规则、存储卷,也带来了安全隐患;

在Kubernetes使用docker作为容器运行时优点:

  1. 熟悉性和迁移成本:如果您已经熟悉使用Docker作为容器化技术,并且您的现有应用程序和工作流程已经在Docker上运行良好,那么继续使用Docker作为容器运行时可能能够减少迁移成本和学习曲线。

  2. 工具和生态系统支持:Docker是目前最流行的容器技术之一,有着广泛的工具和生态系统支持。如果您依赖于特定的Docker工具、监控、日志记录、调试等方面的功能,那么继续使用Docker作为容器运行时可能会更加方便和无缝集成。

  3. 社区支持和文档资源:由于Docker的流行度,它拥有庞大的社区支持和丰富的文档资源。这意味着您可以更容易地找到解决问题、获取支持和参考文档。

  4. 稳定性和可靠性:尽管Kubernetes不再默认使用Docker作为容器运行时,但Docker仍然是一个经过验证和稳定的容器化技术。如果您已经在生产环境中使用Docker并且它能够满足您的需求,您可能会选择继续使用它以确保稳定性和可靠性。

4.3 如何应对?

在未来的 Kubernetes 版本彻底放弃 Docker 支持之前,引入受支持的容器运行时介绍。

除了docker之外,CRI还支持很多容器运行时介绍,例如:

  • containerd:containerd与Docker相兼容,相比Docker轻量很多,目前较为成熟
  • cri-o:cri-o支持OCI的容器镜像格式,可以从容器镜像仓库中下载镜像。CRI-O支持runC和Kata Containers这两种低层容器运行时。

5、CRI 相关工具

下面给大家介绍一下 CRI 相关的工具。这几个工具都在特别兴趣小组的一个项目里面。

  • crictl:它是一个类似 docker 的命令行工具,用来操作 CRI 接口。它能够帮助用户和开发者调试容器问题,而不是通过 apply 一个 yaml 到 apiserver、再通过 Kubelet 操作的方式来调试。这样的链路太长,而这个命令行工具可以直接操作 CRI。
  • critest:用于验证 CRI 接口行为是否是符合预期的。
  • 性能工具:还有一些性能工具用来测试接口性能。

6、修改节点上所使用的容器运行时

通过CRI规范我们可以知道,CRI主要定义了两类接口 ImageService 和 RuntimeService。可以通过 Kubelet 中的标志 --container-runtime-endpoint 和 --image-service-endpoint 来配置这两个服务的套接字。下面给出个2个示例,来具体演示下如何配置这两个服务的套接字。

示例 一:修改节点Kubelet配置,将节点Runtime由默认的Docker修改为containerd。

修改 Kubelet 配置,将容器运行时配置为 containerd,编辑/var/lib/kubelet/kubeadm-flags.env 文件,在该文件中可以添加kubelet 启动参数:

KUBELET_KUBEADM_ARGS="--cgroup-driver=cgroupfs --container-runtime=remote --container-runtime-endpoint=unix:///run/containerd/containerd.sock --pod-infra-container-image=harbor.xxx.xxx:443/xxx/pause:3.4.1"
  • --container-runtime :指定使用的容器运行时的,可选值为 docker 或者 remote,默认是 docker,除 docker 之外的容器运行时都应该指定为 remote。
# 默认docker配置
KUBELET_KUBEADM_ARGS="--cgroup-driver=cgroupfs --network-plugin=cni --pod-infra-container-image=pause:3.4.1"
  • --container-runtime-endpoint:是用来指定远程的运行时服务的 endpiont 地址的,在 Linux 系统中一般都是使用 unix 套接字的形式,unix:///run/containerd/containerd.sock。
  • --image-service-endpoint:指定远程 CRI 的镜像服务地址,如果没有指定则默认使用 --container-runtime-endpoint 的值了,因为一般 CRI 都会实现容器和镜像服务的,本示例中的容器运行时 containerd 是实现容器和镜像服务的。

修改完Kubelet配置后,重启节点Kubelet服务即可修改节点使用的容器运行时。

示例 二:Kubernetes1.24之后版本节点使用docker作为容器运行时

安装cri-docker,确保节点已提前装好docker容器运行时。

wget https://github.com/Mirantis/cri-dockerd/releases/download/v0.3. 2/cri-dockerd-0.3.2-3.el7.x86_64.rpm 
rpm -ivh cri-dockerd-0.3.2-3.el7.x86_64.rpm 
systemctl daemon-reload 
systemctl enable cri-docker --now 
systemctl start cri-docker

修改 Kubelet 配置,将容器运行时配置为 containerd,编辑/var/lib/kubelet/kubeadm-flags.env 文件,在该文件中可以添加kubelet 启动参数:

KUBELET_KUBEADM_ARGS="--cgroup-driver=cgroupfs --container-runtime=remote --container-runtime-endpoint=unix:///var/run/cri-dockerd.sock --pod-infra-container-image=harbor.xxx.xxx:443/xxx/pause:3.4.1"

修改完Kubelet配置后,重启节点Kubelet服务即使用docker作为节点的容器运行时。

注意 1:此示例使用k8s 1.18的版本,此版本能正常使用kubelet内置的dockershim,k8s 1.24以后由于dockershim的移除,就不能使用默认值了。

注意 2:具体编辑哪个配置文件,通过 systemctl cat kubelet查找。

注意 3:确保修改节点已安装containerd组件,并且版本大于等于1.1,否则需要安装单独的 CRI-Containerd 进程,具体详情参见下文。

7、总结

通过此博文结合《浅析开源容器标准——OCI》和《浅析容器运行时》这两篇博文再结合K8s和容器云运行时历史,来简单总结下Kubernetes CRI和容器运行时的协作机制。

  1. Kubernetes v1.5 之前,Docker 作为第一个容器运行时,Kubelet 通过硬编码的方式直接调用 Docker API。
  2. Kubernetes v1.5 之后引入了CRI接口,通过dockershim(垫片)的方式接入docker,shim程序一般由容器厂商根据CRI规范自己开发,实现方式可以自己定义(即CRI规范定义了要做什么,怎么做可以基于自己的理解实现)。而dockershim由于历史原因,还是Kubernetes项目组来做的,这部分代码也包含在Kubelet代码里面,但架构上是分开的。
  3. Kubernetes v1.6,Docker 公司把容器云运行时项目 containerd 项目捐献给了 CNCF 基金会(2017年3月 ),我们知道早在2015年6月,Docker 将 libcontainer 项目被捐赠给OCI,成为独立的容器运行时项目 runc,所以也就在Docker把containerd 项目捐献给了 CNCF 基金会后,Dokcer公司已经将它们的底层运行时和高层运行时都捐赠出去了。
  4. 在Docker捐赠containerd后,Kubernetes也顺应潮流,孵化了cri-containerd项目(作为一个单独的进程),用于直接和containerd对接,这样就不需要在源码中内置dockershim了,可以完全摆脱docker了。
  5. 在containerd的1.1版本中,甚至已经去除了cri-containerd这样一个守护进程,cri接口以插件的形式直接整合在了containerd项目里面。
  6. 同时,Kubernetes也孵化了cri-o项目,则连containerd都绕过了,直接使用runc去创建容器(你可以把cri-o看作是k8生态里面的containerd)。
  7. 随时CRI的成熟,在Kubernetes v1.20 版本中将 Kubelet 中内置的 dockershim 代码分离,将内置的 dockershim 标记为维护模式,当然这个时候仍然还可以使用 dockershim,在Kubernetes v1.24 版本移出了dockershim 代码。

注意 1:Docker 1.11版本为了做容器编排,调整Docker架构, 让 Docker Daemon 专门去负责上层的封装编排,将容器操作都迁移到 containerd 中,但是结果很惨,Docker Swarm直接惨败给Kubernetes,之后在2017年3月, Docker 公司就把 containerd 项目捐献给了 CNCF 基金会。

8、转载声明:

本文大部分内容主要参考此篇博文。

  • 版权声明:本文为CSDN博主「一念一生~one」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
  • 原文链接:容器运行时与k8s概述

其他参考文章:从零开始入门 K8s | 理解容器运行时接口 CRI

posted @ 2023-06-21 08:21  人艰不拆_zmc  阅读(606)  评论(0编辑  收藏  举报