本文内容节选自 《containerd 原理剖析与实战》
Kubernetes 与 CRI
Kubernetes 作为容器编排领域的事实标准,其优良的技术架构不仅可以满足弹性分布式系统的编排调度、弹性伸缩、滚动发布、故障迁移等能力,而且整个系统具有很高的扩展性,提供了各个层次的扩展接口,如 CSI、CRI、CNI 等,满足各种的定制化诉求。
其中,容器运行时作为 Kubernetes 运行容器的关键组件,承担着管理进程的 “worker” 使命。
那么 容器运行时是怎么接入到 Kubernetes 系统中的呢?
答案就是 容器运行时接口 (container runtime interface,CRI)。
下面介绍 Kubernetes 是如何通过 CRI 管理不同的容器运行时的。
Kubernetes 概述
Kubernetes 的整体架构如下图所示。
_图_ _Kubernetes_ _组件架构_
可以看到,Kubernetes 整体架构由 Master 节点和 多个Node 节点组成,Master 为控制节点,Node 为计算节点。Master 节点是整个集群的控制面,编排、调度、对外提供 API 等都是 Master 节点来负责的,Master 节点主要由四个组件组成:
-
**kube-apiserv****er :**该组件负责公开 Kubernetes 的 API,负责处理请求的工作,是资源操作的唯一入口。并提供认证、授权、访问控制、API 注册和发现等机制。
-
**kube-controller-manager:**包含了多种资源的控制器,负责维护集群的状态,例如故障检测、自动扩展、滚动更新等。
-
**kube-scheduler:**该组件主要负责资源的调度,将新建的 Pod 安排到合适的节点上运行。
-
**etcd:**是整个集群的持久化数据保存的地方,是基于 raft 协议实现的一个高可用的分布式 KV 数据库。
Node,也成为 Worker 节点,是主要干活的部分,负责管理容器的进程、存储、网络、设备等能力。Node节点主要由以下几种组件组成:
-
kube-proxy: 主要为 Service 提供 cluster 内部的服务发现和四层负载均衡能力。
-
Kubelet: Node 上最核心的组件,对上负责和 Master 通信,对下和容器运行时通信,负责容器的生命周期管理、容器网络、容器存储能力建设:
-
通过**容器运行时接口 (CRI,Container Runtime Interface)**与各种容器运行时通信,管理容器生命周期。
-
通过**容器网络接口 (CNI,Container Network Interface)**与容器网络插件通信,负责集群网络的管理。
-
通过**容器存储接口 (CSI,Container Storage Interface)**与容器存储插件通信,负责集群内容器存储资源的管理。
-
Network plugin: 网络插件,如 Flannel、Cilium、Calico则负责为容器配置网络,通过 CNI 接口被 kubelet 或者 CRI 的实现来调用,如 containerd 等。
-
Container Runtime: 容器运行时,如 containerd、docker 等,负责容器生命周期的管理,通过 CRI 接口被 kubelet 调用。Container Runtime 则通过 OCI 接口与操作系统交互,运行进程、资源隔离与限制等。
-
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 的架构经历了如下图所示的演进过程。
_图 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,如下图 所示。
_图 kubelet通过 cri-containerd 连接 containerd_
第二阶段: containerd 1.1 版本之后,将 CRI-Contianerd 作为插件集成在 containerd 进程中,如下图所示。
_图 cri-containerd 作为插件集成在 containerd 中_
在 kubelet 移除 dockershim 之后,通过 cri-dockerd + docker 创建容器的流程如下图所示。
_图 kubelet 通过 cri-dockerd 连接 docker_
通过 CRI-containerd 和 CRI-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的交互如下图所示。
_图 kubelet与 CRI 交互_
其中 CRI Server 则实现了 CRI 的接口,作为服务端,监听在本地的 unix socket 上,kubelet 中含有 CRI Client,作为客户端通过 grpc 与 CRI Server 交互。CRI Server 还负责容器网路的配置,不一定强制使用 CNI,只不过使用 CNI 规范可以与 Kubernetes 网络模型保持一致,从而支持社区众多的网络插件。
CRI 接口规范定义主要包含两部分,即 RuntimeService 和 ImageService 两个服务,如下图所示。
_图 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 中的 RuntimeService 、ImageService 相关服务。
1. RuntimeService
RuntimeService 主要负责 Pod 及 Container 生命周期的管理,包含四大类:
-
PodSandbox 管理: 跟 Kubernetes 中的 Pod 一一对应,主要为 Pod 运行提供一个隔离的环境,并准备该 Pod 运行所需的网络基础设施。在 runc 场景下对应一个 pause 容器,在 kata 或者 firecracker 场景下则对应一台物理机。
-
Container 管理: 用于在上述 Sandbox 中管理容器的生命周期,如创建、启动、销毁容器。属于容器粒度的接口。
-
Streaming API: 该接口主要用于 kubelet 进行
Exec、Attach、PortForward交互,该类接口返回给 kubelet的是 Streaming Server 的 Endpoint,用于接受后续 kubelet 的Exec、Attach、PortForward请求。 -
Runtime 接口: 主要是查询该 CRI Server 的状态例如 CRI、CNI 状态,以及更新 POD CIDR 配置等,该接口属于 Node 粒度的接口。
RuntimeService 接口详细介绍如下表所示(参考官方 API 定义[ github.com/Kubernetes/…
表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 定义[ github.com/Kubernetes/…
表 CRI Server 中的 ImageService 接口描述
| 分类 | 方法 | 说明 |
|---|---|---|
| 镜像相关 | ListImages | 列出当前存在的镜像 |
| ImageStatus | 返回镜像的状态,如果不存在,则 ImageStatusResponse.Image 则为 nil | |
| PullImage | 通过认证信息拉取镜像 | |
| RemoveImage | 移除镜像,该操作是幂等的 | |
| ImageFsInfo | 返回存储镜像所用的文件系统 |
在 CRI Container Runtime 中,除了 ImageService 和 RuntimeService 之外,通常情况下还需要实现 Streaming Server 的相关能力。
在 Kubernetes 中,通过 kubectl exec 、logs、attach、portforward 命令时需要 kubelet 在 apiserver 和容器运行时之间建立流量转发通道,Streaming API 就是 返回该流量转发通道的。
不同的容器运行时支持 exec、attach 等命令的方式是不一样的,例如 docker 、containerd 可以通过 nsenter socat 等命令来支持,而其他操作系统平台的运行时则不同,因此 CRI 定义了该接口,用于容器运行时返回 Streaming Server 的 Endpoint,以便 Kublet 将 kube-apiserver 发过来的请求重定向到 Streaming Server。
下面以 kubectl exec 流程为例介绍 Streaming API 和 Streaming Server,如图4.8所示。
*图 Kubernetes 架构中* *`exec` 命令的数据流架构图*
如图所示,kubectl exec 命令主要有以下几个步骤。
-
kubectl 发送 POST 请求
exec给 kube-apiserver,请求路径为"/api/v1/namespaces/<pod namespace>/<pod name>/exec?xxx"。 -
kube-apiserver 通过 CRI 接口向 CRI Server 调用 Exec 函数。
-
CRI Server 返回 Streaming Server 的 url 地址给 kubelet。
-
kubelet 返回给 kube-apiserver 重定向响应,将请求重定向到 Streaming Server 的 url。
-
kube-apiserver 重定向请求到 Streaming Server 的 url。
-
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 连接并行响应 stdin、stdout、stderr 多路请求,多个请求响应相互之间互不影响。
同时 kube-apiserver 也会将来自 kubectl 的请求升级为 SDPY 协议,用于响应多路请求。如下图所示。
*图 Kubernets exec 流程中的 streaming 请求*
Linux 进程中的标准输入 stdin、标准输出 stdout、标准错误 stderr 分别通过 Streaming Server 的 SPDY 连接暴露出来,继而与 kube-apiserver、kubectl 的分别基于 SPDY 建立三个 Stream 连接进行数据通信。
以上内容节选自 《containerd 原理剖析与实战》
containerd 系列文章参考
【史上最全】带你全方位了解containerd 的几种插件扩展模式
作为资深 CRUD Boy,你知道 containerd 是如何保存容器元数据的吗?
了解 containerd 中的 snapshotter,先从 native 开始
本文使用 文章同步助手 同步