掘金 后端 ( ) • 2024-04-26 17:59

本文内容节选自 《containerd 原理剖析与实战》

Kubernetes 与 CRI

Kubernetes 作为容器编排领域的事实标准,其优良的技术架构不仅可以满足弹性分布式系统的编排调度、弹性伸缩、滚动发布、故障迁移等能力,而且整个系统具有很高的扩展性,提供了各个层次的扩展接口,如 CSI、CRI、CNI 等,满足各种的定制化诉求。

其中,容器运行时作为 Kubernetes 运行容器的关键组件,承担着管理进程的 “worker” 使命。

那么 容器运行时是怎么接入到 Kubernetes 系统中的呢?

答案就是 容器运行时接口 (container runtime interface,CRI)

下面介绍 Kubernetes 是如何通过 CRI 管理不同的容器运行时的。

Kubernetes 概述

Kubernetes 的整体架构如下图所示。

1.jpg

_图_ _Kubernetes_ _组件架构_

可以看到,Kubernetes 整体架构由 Master 节点和 多个Node 节点组成,Master 为控制节点,Node 为计算节点。Master 节点是整个集群的控制面,编排、调度、对外提供 API 等都是 Master 节点来负责的,Master 节点主要由四个组件组成:

  1. **kube-apiserv****er :**该组件负责公开 Kubernetes 的 API,负责处理请求的工作,是资源操作的唯一入口。并提供认证、授权、访问控制、API 注册和发现等机制。

  2. **kube-controller-manager:**包含了多种资源的控制器,负责维护集群的状态,例如故障检测、自动扩展、滚动更新等。

  3. **kube-scheduler:**该组件主要负责资源的调度,将新建的 Pod 安排到合适的节点上运行。

  4. **etcd:**是整个集群的持久化数据保存的地方,是基于 raft 协议实现的一个高可用的分布式 KV 数据库。

Node,也成为 Worker 节点,是主要干活的部分,负责管理容器的进程、存储、网络、设备等能力。Node节点主要由以下几种组件组成:

  1. kube-proxy: 主要为 Service 提供 cluster 内部的服务发现和四层负载均衡能力。

  2. Kubelet: Node 上最核心的组件,对上负责和 Master 通信,对下和容器运行时通信,负责容器的生命周期管理、容器网络、容器存储能力建设:

  3. 通过**容器运行时接口 (CRI,Container Runtime Interface)**与各种容器运行时通信,管理容器生命周期。

  4. 通过**容器网络接口 (CNI,Container Network Interface)**与容器网络插件通信,负责集群网络的管理。

  5. 通过**容器存储接口 (CSI,Container Storage Interface)**与容器存储插件通信,负责集群内容器存储资源的管理。

  6. Network plugin: 网络插件,如 Flannel、Cilium、Calico则负责为容器配置网络,通过 CNI 接口被 kubelet 或者 CRI 的实现来调用,如 containerd 等。

  7. Container Runtime: 容器运行时,如 containerd、docker 等,负责容器生命周期的管理,通过 CRI 接口被 kubelet 调用。Container Runtime 则通过 OCI 接口与操作系统交互,运行进程、资源隔离与限制等。

  8. Device Plugin: Device Plugin 是 kubernets 提供的一种 设备插件框架,通过该接口可将硬件资源发布到 kubelet。如管理 GPU、高性能网卡、FPGA 等。

CRI 与 containerd 在 Kubernetes 生态中的演进

1. kubelet中 CRI 的演进过程

在 Kubernetes 架构中,kubelet作为整个系统的 worker,承担着容器生命周期管理的重任,涉及到最基础的计算、存储、网络以及各种外设设备的管理。

对于容器生命周期管理而言,最初 kubelet 对接底层容器运行时并没有通过 CRI 接口来交互,而是通过代码内嵌的方式将 Docker 集成进来,在 Kubernetes 1.5 之前,Kubernetes 内置了两个容器运行时,一个是 Docker, 另一个是来自自家投资公司 CoreOS 的 rocket

kubelet 以代码内置的方式支持两种不同的运行时,因此无论对于社区 Kubernetes 开发人员的维护工作,还是 Kubernetes 用户想定制开发支持自己的容器运行时来说,都带来了极大的困难。

因此,社区在 2016 年 由 Google 和 Redhat 主导下,**在 Kubernetes 1.5 中重新设计了 CRI 标准,**通过 CRI 抽象层消除了这些障碍,使得无需修改 kubelet就可以支持运行多种容器运行时。内置的 dockershim 和 rkt 也逐渐在 Kubernetes 主线中完全移除。 从最初的内置 Docker Client 到最终实现 CRI 完全移除 dockershim,kubelet 的架构经历了如下图所示的演进过程。

2.jpg

_图 kubelet与 CRI 架构的演进过程_

如图所示,在 kubelet架构演进中,总体上分为以下四个阶段。

(1)第一阶段:

在 Kubernetes 早期版本(v1.5 以前),通过代码内置了 docker 和 rocket 的 client sdk,分别对接 docker 和 rockt。

并通过 CNI 插件为容器配置容器网络。这时候如果用户想要支持自己的容器运行时是相当困难的,需要 Fork 社区代码进行修改,并且自己维护。而社区 Kubernetes 维护人员也要同时维护 rocket 和 docker 两份代码,也是相当痛苦。

(2)第二阶段:

在 Kubernetes 1.5 版本中增加了 CRI 接口,通过定义一层容器运行时的抽象层屏蔽底层运行时的差异。kubelet 通过 gRPC 与 **CRI Server(也叫 CRI Shim)**交互,管理容器的生命周期和网络配置,此时开发者支持自定义的容器运行时就简单多了,只需要实现自己的 CRI Server 即可。

由于 rocket 是自家产品,1.5 版本之后,rocket 的具体逻辑就迁移到了外部独立仓库 rktlet (由于活跃度不高,该项目已于 2019 年 12 月 19 日进行了归档,当前为只读状态)中,kubelet 中的 rkt 则处于 弃用状态,直到 Kubernetes v1.11 版本完全移除。

而 Docker 由于是默认的容器运行时,在此阶段则迁移到了 kubelet 内置的 CRI 接口下,封装了 dockershim 来对接 Docker Client,此时还是 Kubernetes 开发人员在维护。

(3)第三阶段:

在 Kubernetes v1.11 版本中,rocket 代码完全移除,另外 CNI 的实现迁移到了 dockershim 中。

除了 Docker 之外,其他的所有容器运行时都通过 CRI 接口接入,对于外部的 CRI Server(Shim),除了实现 CRI 接口外,也包含了容器网络的配置,一般使用 CNI,当然也可以自己选择。

此阶段 kubelet对接两个 CRI Server,一个是 kubelet 内置的 dockershim,一个是外部的 CRI Server。无论是内置还是外置 CRI Server,均包含了容器生命周期管理容器网络配置两大功能。

(4)第四阶段:

在 Kubernetes v1.24 版本中,kubelet 完全移除了 dockershim,此前在 v1.20 版本中,Kubernetes 就开始宣布要弃用 Docker。

此时, kubelet只通过 CRI 接口与容器运行时交互,dockershim 移除后,若想继续使用 docker,则可以通过 cri-dockerd 来实现,cri-dockerd 是 Mirantis(Docker 的收购方) 和 Docker 共同维护的基于 Docker 的 CRI Server。

至此,kubelet完成了最终的 CRI 架构的演进。

容器运行时开发者若想适配自己的运行时,只需要实现 CRI Server ,以 CRI 接口接入到 kubelet 即可,大大提高了适配和维护效率。

CRI 的推出给容器社区带来了容器运行时的第二次繁荣,包括 containerd、crio、Frakti、Virtlet等。

2. containerd 的演进过程

随着 CRI 接口的逐渐成熟,containerd 与 CRI 的交互在演进中也变得越来越简单和直接:

第一阶段: containerd 1.0 版本中,通过 一个单独的二进制进程来适配 CRI,如下图 所示。

3.jpg

_图 kubelet通过 cri-containerd 连接 containerd_

第二阶段: containerd 1.1 版本之后,将 CRI-Contianerd 作为插件集成在 containerd 进程中,如下图所示。

4.jpg

_图 cri-containerd 作为插件集成在 containerd 中_

在 kubelet 移除 dockershim 之后,通过 cri-dockerd + docker 创建容器的流程如下图所示。

5.jpg

_图 kubelet 通过 cri-dockerd 连接 docker_

通过 CRI-containerdCRI-Dockerd 作为 CRI Server 对比来看,二者都是通过 containerd 作为容器生命周期管理的容器运行时,但是 CRI-Dockerd 方式却多了 cri-dockerd 和 docker 两层 “shim”

相比之下 kubelet 直接调用 containerd 的方案比 cri-dockerd 的方案简洁的多,这也是越来越多的云厂商采用 containerd 作为 Kubernetes 默认容器运行时的原因

CRI 概述

CRI 定义了容器和镜像服务的接口,该接口基于 gRPC,使用 Protocol Buffer 协议。该接口定义了 kubelet 与不同容器运行时交互的规范,接口包含客户端(CRI Client)服务端(CRI Server)。kubelet与 CRI的交互如下图所示。

6.jpg

_图 kubelet与 CRI 交互_

其中 CRI Server 则实现了 CRI 的接口,作为服务端,监听在本地的 unix socket 上,kubelet 中含有 CRI Client,作为客户端通过 grpc 与 CRI Server 交互。CRI Server 还负责容器网路的配置,不一定强制使用 CNI,只不过使用 CNI 规范可以与 Kubernetes 网络模型保持一致,从而支持社区众多的网络插件。

CRI 接口规范定义主要包含两部分,即 RuntimeServiceImageService 两个服务,如下图所示。

7.jpg

_图 CRI Server 中的 RuntimeService 与 ImageService_

这两个服务可以在一个 gRPC Server 中实现,也可以在两个独立的 gRPC Server 中实现。对应的 kubelet中的设置如下。

kubelet xxx 
  --container-runtime-endpoint=< CRI Server 的 Unix Socket 地址,>
  --image-service-endpoint=< CRI Server 的 Unix Socket 地址>

【注意】

如果 RuntimeService 和 ImageService 两个服务是在一个gRPC Server 中实现的,只需要配置 container-runtime-endpoint 即可,当image-service-endpoint 为空时,默认使用和 container-runtime-endpoint 一致的地址。当前社区中实现的 Container Runtime 多为两种服务在一个 gRPC Server 中实现。

另外需要注意的是,如果是 Kubernetes v1.24 以前的版本使用 CRI Server , kubelet 中需要设置 container-runtime=remote(自从 v1.24 版本中 kubelet 中移除了 dockershim 之后,该参数已被废弃),否则,该参数默认为 container-runtime=docker,将使用 kublet 内置的 dockershim 作为 CRI Server。

接下来介绍 CRI Server 中的 RuntimeServiceImageService 相关服务。

1. RuntimeService

RuntimeService 主要负责 Pod 及 Container 生命周期的管理,包含四大类:

  1. PodSandbox 管理: 跟 Kubernetes 中的 Pod 一一对应,主要为 Pod 运行提供一个隔离的环境,并准备该 Pod 运行所需的网络基础设施。在 runc 场景下对应一个 pause 容器,在 kata 或者 firecracker 场景下则对应一台物理机。

  2. Container 管理: 用于在上述 Sandbox 中管理容器的生命周期,如创建、启动、销毁容器。属于容器粒度的接口。

  3. Streaming API: 该接口主要用于 kubelet 进行 ExecAttachPortForward 交互,该类接口返回给 kubelet的是 Streaming Server 的 Endpoint,用于接受后续 kubelet 的 ExecAttachPortForward 请求。

  4. Runtime 接口: 主要是查询该 CRI Server 的状态例如 CRI、CNI 状态,以及更新 POD CIDR 配置等,该接口属于 Node 粒度的接口。

RuntimeService 接口详细介绍如下表所示(参考官方 API 定义[ https://github.com/Kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto\\])。

表4.1 RuntimeService 接口描述

分类 方法 说明 Sandbox 相关 RunPodSandbox 启动 Pod 级别的沙箱功能,包含 Pod 网络基础设施的初始化 StopPodSandbox 停止 Sandbox 相关进程,回收网络基础设施资源(如 IP 等),该操作是幂等的;kubelet 在调用 RemovePodSandbox 之前至少会调用一次StopPodSandbox RemovePodSandbox 删除 Sandbox,以及 Sandbox 内的相关容器 PodSandboxStatus 返回 PodSandbox 的状态 ListPodSandbox 获取 PodSandbox 列表 Container 相关 CreateContainer 在指定的 Sandbox 中创建新的 Container StartContainer 启动 Container StopContainer 在一定的时间内(timeout)停止 一个正在 Runing 的 container,操作是幂等的;在超过 grace period 后,必须要强制杀掉改 container RemoveContainer 清理 Container,如果 Container 在 Running,则强制清理掉该 container,该操作也是幂等的 ListContainers 通过 filter 获取所有的 Container ContainerStatus 获取 Container 的状态,如果 Container 不存在,则报错 UpdateContainerResources 更新 container 的 ContainerConfig ContainerStats 获取 Continer 的统计数据,如 cpu,memory 使用状态 ListContainerStats 获取所有运行 Container 的统计数据(cpu,memory) Runtime 相关 UpdateRuntimeConfig 更新 Runtime 的配置,当前 containerd 只支持处 PodCIDR 的变更 Status 获取 Runtime 的状态(CRI + CNI 的状态),只要 CRI plugin 能正常响应,则 CRI 为 Ready,CNI 要看 CNI 插件的状态 Version 获取 Runtime 的名称、版本、API 版本等 Container 管理 ReopenContainerLog ReopenContainerLog会请求 Runtime 重新打开 Container 的 stdout/stderr ;通常会在 日志文件被 rotate之后被调用,如果 Container 没在运行,则 runtime 会创建一个新的 log file 或者返回 nil,或者 返回 error(返回 error 的情况下,log file 不应该创建) ExecSync 在 Container 内同步执行一个命令 Streaming API Exec 准备一个 Streaming endpoint 在 Container 中执行一个命令。会连接到容器,可以像SSH一样进入容器内部,进行操作,可以通过exit退出容器,不影响容器运行。 Attach 准备一个 Streaming endpoint attach 到指定 container。attach:会通过连接stdin,连接到容器内输入输出流,会在输入exit后终止进程。 PortForward 准备一个 Streaming endpoint 来转发到 container 中的端口,如 kubectl port-forward pods/xxxx 10000:8080将本地端口 10000:转发到容器内的 8080 端口

2.ImageService

Image Service 相对来说就比较简单了,主要是运行容器所需的几个镜像接口,例如拉取镜像,删除镜像,查询镜像信息,查询镜像列表,以及查询镜像的文件系统信息等,注意镜像接口没有推送镜像,因为容器运行只需要将镜像拉到本地即可,推送镜像并不是 CRI Server 必须的能力。

下表是 CRI Server 中的 ImageService 接口及详细描述(_参考官方 API 定义[ https://github.com/Kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto\\]_)。

表 CRI Server 中的 ImageService 接口描述

分类 方法 说明 镜像相关 ListImages 列出当前存在的镜像 ImageStatus 返回镜像的状态,如果不存在,则 ImageStatusResponse.Image 则为 nil PullImage 通过认证信息拉取镜像 RemoveImage 移除镜像,该操作是幂等的 ImageFsInfo 返回存储镜像所用的文件系统

CRI Container Runtime 中,除了 ImageServiceRuntimeService 之外,通常情况下还需要实现 Streaming Server 的相关能力。

在 Kubernetes 中,通过 kubectl execlogsattachportforward 命令时需要 kubelet 在 apiserver 和容器运行时之间建立流量转发通道,Streaming API 就是 返回该流量转发通道的。

不同的容器运行时支持 exec、attach 等命令的方式是不一样的,例如 dockercontainerd 可以通过 nsenter socat 等命令来支持,而其他操作系统平台的运行时则不同,因此 CRI 定义了该接口,用于容器运行时返回 Streaming Server 的 Endpoint,以便 Kublet 将 kube-apiserver 发过来的请求重定向到 Streaming Server。

下面以 kubectl exec 流程为例介绍 Streaming APIStreaming Server,如图4.8所示。

8.jpg

*图 Kubernetes 架构中* *`exec` 命令的数据流架构图*

如图所示,kubectl exec 命令主要有以下几个步骤。

  1. kubectl 发送 POST 请求 exec 给 kube-apiserver,请求路径为 "/api/v1/namespaces/<pod namespace>/<pod name>/exec?xxx"

  2. kube-apiserver 通过 CRI 接口向 CRI Server 调用 Exec 函数。

  3. CRI Server 返回 Streaming Server 的 url 地址给 kubelet。

  4. kubelet 返回给 kube-apiserver 重定向响应,将请求重定向到 Streaming Server 的 url。

  5. kube-apiserver 重定向请求到 Streaming Server 的 url。

  6. Streaming Server 响应该请求,注意,Streaming Server 会返回一个 http 协议升级(101 Switching Protocols ) 的响应给 kube-apiserver,告诉 kube-apiserver 已切换到 SPDY 协议。

Upgrade 是 HTTP 1.1 提供的一种特殊机制,允许将一个已经建立的连接升级成新的,不相容的协议。

SPDY 是 Google 开发的基于 TCP 的会话层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。SPDY 协议支持多路复用,在一个 SPDY 连接内可以有无限个并行请求,即允许多个并发 HTTP 请求共用一个 TCP会话。

对于 exec 流请求来讲,可以基于一个 TCP 连接并行响应 stdinstdoutstderr 多路请求,多个请求响应相互之间互不影响。

同时 kube-apiserver 也会将来自 kubectl 的请求升级为 SDPY 协议,用于响应多路请求。如下图所示。

图片

*图 Kubernets exec 流程中的 streaming 请求*

Linux 进程中的标准输入 stdin、标准输出 stdout、标准错误 stderr 分别通过 Streaming ServerSPDY 连接暴露出来,继而与 kube-apiserver、kubectl 的分别基于 SPDY 建立三个 Stream 连接进行数据通信。

以上内容节选自 《containerd 原理剖析与实战》

containerd 系列文章参考

【史上最全】带你全方位了解containerd 的几种插件扩展模式

作为资深 CRUD Boy,你知道 containerd 是如何保存容器元数据的吗?

了解 containerd 中的 snapshotter,先从 native 开始

一文了解 containerd 中的 snapshot

一文读懂 containerd 中的 NRI 机制

一文了解 containerd 中的镜像加解密

本文使用 文章同步助手 同步