《containerd 系列》一文缕清 CRI 的发展脉络
本文内容节选自 《containerd 原理剖析与实战》
Kubernetes 与 CRI
Kubernetes 作为容器编排领域的事实标准,其优良的技术架构不仅可以满足弹性分布式系统的编排调度、弹性伸缩、滚动发布、故障迁移等能力,而且整个系统具有很高的扩展性,提供了各个层次的扩展接口,如 CSI、CRI、CNI 等,满足各种的定制化诉求。
其中,容器运行时作为 Kubernetes 运行容器的关键组件,承担着管理进程的 “worker” 使命。
那么 容器运行时是怎么接入到 Kubernetes 系统中的呢?
答案就是 容器运行时接口 (container runtime interface,CRI)。
下面介绍 Kubernetes 是如何通过 CRI 管理不同的容器运行时的。
Kubernetes 概述
Kubernetes 的整体架构如下图所示。
<p align=center>图 Kubernetes 组件架构</p>
可以看到,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 的架构经历了如下图所示的演进过程。
<p align=center>图 kubelet 与 CRI 架构的演进过程</p>
如图所示,在 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,如下图 所示。
<p align=center>图 kubelet 通过 cri-containerd 连接 containerd</p>
第二阶段: containerd 1.1 版本之后,将 CRI-Contianerd 作为插件集成在 containerd 进程中,如下图所示。
<p align=center>图 cri-containerd 作为插件集成在 containerd 中</p>
在 kubelet 移除 dockershim 之后,通过 cri-dockerd + docker 创建容器的流程如下图所示。
<p align=center>图 kubelet 通过 cri-dockerd 连接 docker</p>
通过 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 的交互如下图所示。
<p align=center>图 kubelet 与 CRI 交互</p>
其中 CRI Server 则实现了 CRI 的接口,作为服务端,监听在本地的 unix socket 上,kubelet 中含有 CRI Client,作为客户端通过 grpc 与 CRI Server 交互。CRI Server 还负责容器网路的配置,不一定强制使用 CNI,只不过使用 CNI 规范可以与 Kubernetes 网络模型保持一致,从而支持社区众多的网络插件。
CRI 接口规范定义主要包含两部分,即 RuntimeService 和 ImageService 两个服务,如下图所示。
<p align=center>图 CRI Server 中的 RuntimeService 与 ImageService</p>
这两个服务可以在一个 gRPC Server 中实现,也可以在两个独立的 gRPC Server 中实现。对应的 kubelet 中的设置如下。
【注意】
如果 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 定义[ https://github.com/Kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto\\])。
表 4.1 RuntimeService 接口描述
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 接口描述
在 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 所示。
<p align=center>图 Kubernetes 架构中 exec
命令的数据流架构图</p>
如图所示,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 协议,用于响应多路请求。如下图所示。
<p align=center>图 Kubernets exec 流程中的 streaming 请求</p>
Linux 进程中的标准输入 stdin
、标准输出 stdout
、标准错误 stderr
分别通过 Streaming Server 的 SPDY 连接暴露出来,继而与 kube-apiserver、kubectl 的分别基于 SPDY 建立三个 Stream 连接进行数据通信。
以上内容节选自 《containerd 原理剖析与实战》
本文使用 文章同步助手 同步
版权声明: 本文为 InfoQ 作者【公众号:云原生Serverless】的原创文章。
原文链接:【http://xie.infoq.cn/article/610b3c6cdb1aee5f0af80ffe6】。文章转载请联系作者。
评论