文章目录
  • 前言
  • kubelet 架构
  • kubelet 职责
  • Node管理(节点管理)
  • Pod管理
  • kubelet管理Pod
  • kubelet如何管理当前节点上所有Pod
  • kubelet三个端口
  • kubelet获取Pod清单
  • kubelet通过CRI接口管理Pod以及里面的容器
  • PodWorker的工作细节
  • PodWorker的工作细节
  • PLEG组件
  • PLEG报错
  • kubelet创建并启动Pod
  • Pod启动流程
  • 更加详细的Pod启动过程
  • kubelet CRI 容器运行时
  • CRI 容器运行时
  • CRI 容器运行时架构
  • kubele调动高层运行时通过 CRI 规范接口
  • 高层运行时调用低层运行时通过 OCI 规范接口
  • CRI 高层运行时和低层运行时
  • 高层运行时和低层运行时
  • 如何查看当前k8s集群的高层运行时和低层运行时
  • 高层运行时: 从docker到containerd
  • 三种高层运行时比较
  • Docker和Containerd的细节差异
  • Containerd和CRI-O的性能比较
  • 小结
  • 尾声


前言

kubelet 架构

controller-manager包含了一组控制器,它是整个集群的大脑,每个控制器会关注自己所关注的对象,这些控制器里面都会有一个固定的模式,每个控制器都会通过list watcher,去watch它所关注的对象,当这些对象发生变化以后,以事件通知的形式,告知controller,controller会注册这些事件的回调函数,并且将这些事件放在一个队列里面,同时会去启动一堆worker线程,来从队列里面取出这些对象,并且进行相关的处理。

也就是说,当读了任何一个controller manager的代码,可以将同样的思路套到其他的controller里面。在每个节点上面都会去运行一个组件叫做kubelet。

Kubelet架构

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_容器

它是承担的很多很多职责的这样一个组件。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_容器_02

最上层,它会提供一个api层,这个api里面就会有蛮多的职责,比如做自己的探活,还有业务指标如何上报,这些都是通过api,你可以看到它工作在不同的端口上面都有监听器,承担不同的职责。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_生命周期_03

中间就是在kubelet里面运行了一个一个的manager,你可以理解为一个一个独立职责的小的管理器。

第一列(从左到右):

  1. probemanager:就是为这个节点上面的pod做探活的这样一个管理器,liveless readyless probe,这些探活由kubelet发起,kubelet的probemanager会去读取当前节点上面所有probe的定义,然后它会去看这些probe有没有定义探活,如果定义了探活的属性,那么probemanager就会按照你定义的interval去做监听。
  2. oomwatcher:kubelet是整个节点的守护神,它是一个代理,它负责所有应用的生命周期,同时它有包含节点当前继续正常工作的职责,所以它由oomwatcher来监听,比如节点出现oom问题,比如某些进程出现了oom的错误,oomwathcer就会通过oom这个事件来获取这些异常,并且上报给kubelet。
  3. GPU manager:就是用来管理GPU等等这些扩展的device,当节点上有GPU卡的时候,GPUmanager就是用来管理这张GPU卡的。
  4. cadvisor:是独立开源的软件,kubelet里面会内嵌cadvisor,它其实基于cgroup技术去获取节点上运行应用的资源状态,整个集群的监控都是由cadvisor收集上来的,并且通过kubelet上报的。

第二列(从左到右):
5. syncLoop: syncLoop使用控制器模式,它要去watch一堆对象,也就是当前节点的pod对象,syncloop做完之后,下面内嵌了一堆的worker,pod worker就是当我的 sync group 接收到pod的变更通知的时候,那么这个 pod worker 就会去干活,也就是去维护pod的生命周期。kubelet 中的 syncLoop,要去监听apiserver去获取当前节点的pod清单,获取pod清单之后,然后获取的pod清单,每个pod都是一个一个的通知事件,然后podworker就会处理这些事件。
6. PodWorker: PodWorker会去调用cri,然后获取当前节点pod对应的容器是不是在启动的状态。如果没有启动,那么他就会去启动这些容器进程,把这个应用拉起来,它就是通过cri的接口。cri接口传统都是通过dockershim去做的,但是随着kubernetes架构不断升级,它废弃了docker-shim这条线,它决定不再支持比较臃肿的dokcer shim,而是通过remote container interface,比如通过containerd,或者cri来支持整个运行时。

第三列(从左到右):
7. diskspacemanager:这个节点的磁盘空间大小,包括每个应用,你上去了临时空间,你是不是超了,由diskspacemanager来管理。
8. statusmanager:用来管理节点的状态的。
9. evictionmanager:kubelet是这个节点的守护神,它会去监听当前节点上所有pod所消耗的资源,比如说内存,内存是不可压缩资源,可压缩资源cpu,cpu是基于分时复用的,当你多个进程去抢cpu的时候,反正我们按照协商好的比例去分cpu时间片,当出现竞争的时候,顶多受损,不会异常退出。但是内存不一样,内存是不可压缩资源,针对内存,kubelet就会做比较激进的动作,eviction,evictionmanager就是承担这个职责的组件。它会去监听当前节点的资源使用情况,如果它发现内存已经达到了一个水位,比如10g内存使用了9g,如果不制止这种情况,那么可能整个节点都会受到影响。evictionmanager就会去监听这个水位,当这个内存达到一定水位的时候,它就会去做驱逐,它会去按照既定策略将低优先级的业务,而且占用内存又比较大的,超出了自己预设值的这些进程驱逐掉,也就是将pod从这个节点删除掉。通过这种极端的方式来保护整个节点。
10. volumemanager:pod启动要挂载磁盘,要挂载存储卷的,这个是由volumemanager去做的。

第四列(从左到右):
11. imagegc:如果我这个节点不断的启动应用,每个应用都需要拉取自己的image,如果我不做一些清理工作,就会导致这个节点的image会越来越多,把磁盘占满,所谓的imagegc就会去扫描一些不怎么活跃的镜像,把这些镜像清除掉。
12. containegc就是你的进程可能会退出,它会有一个exit的container,这个container其实它的文件层是没有清理掉的,所谓的container gc就是会清除这些退出的容器。
13. imagemanager:就是做镜像管理的。
14. certificatesmanager:kubelet有签证书的能力,当我们需要kubelet去管理证书的时候,certificatesmanager就可以起作用。

kubelet 职责

每个节点上都运行一个kubelet服务进程,默认监听10250端口,kubelet的职责包括:

  1. 管理Node(通过kubelet访问apiserver的方式): 每个kubelet进程会在APIServer上注册节点自身信息,定期向 master 节点汇报节点的资源使用情况,并通过 cAdvisor 监控节点和容器的资源。(pod健康状态通过pleg去做上报,节点的资源使用情况通过cadvisor上报)
  2. 管理Pod及Pod中的容器(通过kubelet watch apiserver的方式): 接收并执行master apiserver发来的指令(其实是用watch的机制去监听的,如果这些pod发生了变化,他就会去执行某些操作,具体执行什么样的操作就是管理pod当中的容器)

Node管理(节点管理)

kubelet本身有节点管理的功能,包括注册节点,更新节点状态,所以在启动kubelet的时候,如果加了register-node,那么就会自动的将当前节点注册到apiserver。

节点管理最主要的两个是节点自注册和节点状态更新∶

  1. kubelet 可以通过设置启动参数–register-node 来确定是否向API Server注册自己
  2. 如果 kubelet 没有选择自注册模式,则需要用户自己配置Node 资源信息,同时需要告知 kubelet集群上的APIServer的位置
  3. kubelet在启动时通过APIServer注册节点信息,并定时向APIServer发送节点新消息,APIServer在接收到新消息后,将信息写入etcd。

Pod管理

比节点管理相关更加重要的是pod的管理,因为节点的注册管理是一次性的,然后由kublet不断的上报状态,更重要的是应用的管理,也即是pod。

kubelet如何管理pod?它首先要知道节点上要启动哪些pod,这就涉及到如何获取pod清单,kubelet有几种方式去获取pod清单,第一种就是文件的方式,也就是static pod,扫描到目录下有pod清单文件,就将这些pod启动起来。

staticPodPath: /etc/kubernetes/manifests
  • 1.

第二种方式是通过http endpoint来加载pod,你可以给它一个启动参数叫做manifest-url,这个url是http-endpoint,你把所有pod清单放在那,他也会去读取那个url来加载pod清单。

第三种方式是通过apiserver了,当apiserver启动之后,它自己会去启动apiserver,kubelet连接到apiserver,会去监听apiserver上面所有的pod,当然是和这个节点相关的所有pod,经过调度scheduler之后的所有pod。(kubelet会去watch apiserver,apiserver那边有pod创建出来了,并且和这个节点相关,这个节点上的kubelet就会去工作)

获取Pod清单∶

(1) 文件∶启动参数–config指定的配置目录下的文件(默认/etc/Kubernetes/manifests/)。该文件每20秒重新检查一次(可配置)。
(2) HTTP endpoint(URL)∶启动参数–manifest-url设置。每20秒检查一次这个端点(可配置)。
(3) APIServer∶通过APIServer监听etcd目录,同步Pod清单。
(4) HTTP Server∶ kubelet 侦听 HTTP 请求,并响应简单的 API 以提交新的 Pod 清单。

kubelet管理Pod

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_容器

从上到下分为三层,如下:
最上一层:kubelet三个端口
次一层:kubelet获取Pod清单
第三层:kubelet通过CRI接口管理Pod以及里面的容器

下面分层介绍。

kubelet如何管理当前节点上所有Pod

kubelet三个端口

kubelet api 包含三个端口

(1) 10250 端口: 业务api, 上面阐述的节点管理和Pod管理通过 10520 端口的api来实现的

(2) 10250 端口: 只读api

(3) 10248 端口: 健康检查api

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_容器_05

查看 kubelet 三个端口

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubernetes_06


测试 健康检查接口 (apiserver和kubelet都有健康检查接口)

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_Pod_07

kubelet获取Pod清单

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_生命周期_03

中间还有一个syncloop和PodWorkder

所谓的syncloop又是控制器模式了,它要去watch一堆对象,也就是当前节点的pod对象,sync loop做完之后,下面内嵌了一堆的worker,pod worker就是当我的sync loop接收到pod的变更通知的时候,那么这个pod worker就会去干活,也就是去维护pod的生命周期。

kubelet要去监听apiserver去获取当前节点的pod清单,获取pod清单之后,然后获取的pod清单,每个pod都是一个一个的通知事件,然后podworker就会处理这些事件。

kubelet通过CRI接口管理Pod以及里面的容器

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_Pod_09

kubelet (本质是kubelet中的podworker) 会去调用cri,然后获取当前节点pod对应的容器是不是在启动的状态。如果没有启动,那么他就会去启动这些容器进程,把这个应用拉起来,它就是通过cri的接口。

cri接口传统都是通过dockershim去做的,但是随着kubernetes架构不断升级,它废弃了docker-shim这条线,它决定不再支持比较臃肿的dokcer shim,而是通过remote container interface,比如通过containerd,或者cri来支持整个运行时。

PodWorker的工作细节

PodWorker的工作细节

上面将kubelet三层结构中,包括 kubelet 三个接口、kubelet 获取Pod清单、kubelet 通过 CRI 接口管理Pod以及里面的容器。这里详细讲解一下 kubelet 通过 syncLoop 获取 Pod 清单列表,这个 syncLoop 底层怎么工作的。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubelet_10

步骤1:kubelet中的synloop监听pod的状态变化
kubelet它本身的synloop,就是去监听pod的状态变化,这个pod的状态变化就是一个一个的pod的update,或者是add事件,接收到这些事件之后,它就会将事件存在updateoption里面,那么不同的worker,在kubelet里面有多个worker,每个worker都会从队列里面获取这些pod变更事件的清单。

事件就是Event,类似于mysqll中的 crud 语句,记录着数据操作,APIServer审计日志会记录所有的事件的

步骤2:针对每一个pod执行sync pod的操作
针对每一个pod,它就会去执行sync pod的操作,sync pod里面最重要的行为就是 compute pod actions,也就是针对这个pod采取什么样的行为,它会去比对当前节点上面的已经在运行的容器进程,然后去判断说,如果pod是新的我就create,如果pod是已经存在的,比如说是delete事件,那么我就应该去删除,如果是一个更新事件,那么我就要去判断运行的容器进程和你当前的pod是不是匹配的,比如它的哈希值变的话,那么我就要重建,这就是 compute pod actions所做的事情。它算完了之后就需要通过cri的接口,去create这些进程,或者kill这些进程。

步骤3:kubelet中的pleg将每一个pod生命周期上报给apiserver
右边还有个组件叫做pleg,它主要是pod lifecycle event的一个汇聚器,就是pod运行完之后,上面左边部分是用来管理pod的生命周期,管理完之后这些pod的状态如何上报,那么就是通过pleg组件去上报的。kubelet在pleg组件里面维护了一个pod cache,它本地的一个缓存,这个pleg会去定期的往container runtime里面去发一个list的操作,去获取当前节点上pod的清单,正在运行的pod清单,那么这里就会把当前所有在运行pod状态在pleg这里汇聚,由pleg 通过pod lifecycle event发回上报给apiserver,apiserver那端就知道这个node节点上面所有pod的状态了。

小结:apiserver将Pod 事件Event发送给kubelet,kubelet将 Pod 生命周期 lifecycle 发送给apiserver
这就是 apiserver 与 kubelet 相互的业务沟通,之前学习证书的时候,apiserver和kubelet需要相互调用,各自保留对方的客户端证书

PLEG组件

PLEG 是 kubelet 用来检查容器 runtime 的健康检查机制。这件事情本来可以由 kubelet 使用 polling 的方式来做。但是 polling 有其成本上的缺陷,所以PLEG 应用而生。PLEG 尝试以一种“中断”的形式,来实现对容器 runtime 的健康检查,虽然实际上,它同时用了 polling 和”中断”两种机制。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubelet_11

PLEG报错

如果contained runtime不响应了,那么relist就会失败,relist失败,那么这些状态就上报不了,那么最终这个状态无法上报,那么kubernets就会认为你整个节点有问题。所以runtime本身不响应,那么这个节点会变的不正常。

还有就是bug,退出容器的gc没有做好,就节点上出现了几千上万的exit container在那里,pleg再去跟runtime relist 这些container的时候,他就会去遍历所有的已经退出的容器,就会导致时间非常的长,它没有办法及时返回所有容器的清单,没有在它规定的时间内返回,那么整个pleg的操作就超时了,那么就会导致整个节点的状态发生变化。

当集群节点进入 NotReady 状态的时候,我们需要做的第一件事情,肯定是检查运行在节点上的 kubelet 是否正常。在这个问题出现的时候,使用 systemctl 命令查看 kubelet 状态,发现它作为 systemd 管理的一个 daemon,是运行正常的。当我们用 journalctl 查看 kubelet 日志的时候,发现下边的错误。

journalctl -u kubelet.service
  • 1.

journalctl命令是用于查看和管理systemd日志的命令行工具。systemd是Linux系统的系统管理和初始化系统,journalctl命令可以用来查看systemd的日志信息。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubelet_12

这个报错很清楚的告诉我们,容器 runtime 是不工作的 [container runtime is down],且 PLEG 是不健康的 [PLEG is not healthy]。这里容器 runtime 指的就是 docker daemon。Kubelet 通过直接操作 docker daemon 来控制容器的生命周期。而这里的 PLEG,指的是 pod lifecycle event generator。

基本上看到上边的报错,我们可以确认,容器 runtime 出了问题。在有问题的节点上,通过 docker 命令尝试运行新的容器,命令会没有响应。这说明上边的报错是准确的。

kubelet创建并启动Pod

Pod启动流程

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubernetes_13

高层运行时有两种,一种docker,一种containerd,上图是以containerd来说明,更加轻量。

步骤1:用户去创建pod,那么pod的请求会发给apiserver,apiserver接收到请求之后,会把对象存储到etcd里面 [吻合: apiserver与etcd的关系是apiserver单向调用etcd],存储完成之后,调度器scheduler 就去watch到pod的创建时间,并且完成调度,绑定pod [scheduler完成了将这个新建的Pod放到哪个Node上]。

步骤2:绑定之后,也就是更新,继续去etcd里面做持久化,持久化完成之后,kubelet就watch到已经绑定的pod。[此时开始kubelet管理所在Node上的新建的这个Pod]

kubelet在启动pod的时候,它启动的不是一个容器进程,而是多个容器进程,不仅仅启动了应用进程,同时还启动了pause容器镜像的实例,容器进程启动了之后可以把它和某个网络namespace产生关联关系,把它放在某个网络namespace下面,在这个独立的网络namesapce下面,它可以有独立的网络配置,如果直接将容器进程放到网络namesapce里面会出现什么样的问题呢?

kubelet两个参数
–network-plugin=cni 表示Pod之间使用cni通信
–pod-infra-container-image=k8s.gcr.io/pause:3.4.1 表示Pod使用pause镜像启动一个pause容器,维持Pod生命周期,并且把这个pause容器放在一个network ns里面

你的容器进程可靠吗?会不会有错误,会不会异常退出,oom,空指针,等等,当出现这些问题的时候,容器进程就退出了,退出之后容器进程就和这个网络namespace之间的绑定关系就消失了,如果不做一些措施的话,那是不是每次容器进程退出都需要重新为这个容器进程配置网络,那么这样的效率不会很高,如果容器频繁重启,那么就会导致节点上面多了很多不必要的系统操作。

有什么办法让容器进程退出的时候,网络,存储这些东西都不发生变化,这就是为什么需要pause容器。在容器启动之前,网络就需要就绪,网络是要在容器之前就绪的,这就需要额外的容器进程先启动起来。这个也是pause的功能。

pause容器里面entrypoint其实就是pause,它永远sleep,它是一个不会退出,不消耗资源的一个稳定的进程,那么所有的网络就可以挂载在这个pause上面,当容器应用进程启动的时候,我只需要将应用进程的网络namespace挂载在pause上面就行了。

即使容器出现各种问题重启,没关系,因为网络是挂载在其下面的。所以它就提供了非常稳定的基座。

解释: pause容器里面entrypoint其实就是pause, pause容器永远运行
在Kubernetes中,Pause容器是一个特殊的容器,它被用作Pod中其他容器的父容器。Pause容器不执行任何操作,而只是保持运行状态,以保持Pod的生命周期。当Pod中的所有其他容器退出运行时,Pause容器仍然保持运行状态。
而entrypoint是容器在启动时运行的可执行程序或命令。对于Pause容器来说,entrypoint就是容器启动时运行的空命令(例如/pause),而这个空命令并不会做任何实际的工作。它只是保持容器的运行状态,以确保Pod在存在其他容器运行时保持处于活跃状态。
总之,Pause容器的entrypoint就是一个空命令,它的作用是保持容器的运行状态,以保持Pod的生命周期,并在其他容器退出运行时继续保持Pod的活跃状态。

更加详细的Pod启动过程

复述一下Pod启动:pod启动的时候最先启动pause容器,启动之后containerd就会去调用cni的插件去setup pod,其实它会将ADD的命令告诉cni插件帮我setup网络,cni这边setup网络之后会去将pod信息返回给runtime,然后这个信息会返回给kubelet,那么这个时候其实pod就有IP了,这个时候才会去进行下面的用户应用容器的启动。

可以看到在启动容器的时候,网络是已经就绪的,所以有网络需求就可以满足了,整个启动完之后就会将状态回写到Apiserver里面,那么整个的启动过程就完成了。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubernetes_14

上面是更加详细的Pod启动流程框图,最左边是kubelet 中间是CRI 右边是CNI

kubelet 最重要的功能是 syncPod ,在这个 syncPod 操作之前,包括五个步骤

步骤1. Check Admit:kubelet在sync pod也就是做pod同步的时候,首先也会去做准入,比如它要去启动pod,如果节点资源不够,你非要将nodename设置为该节点,因为和节点产生绑定关系,那么就需要启动这个容器了,启动的时候我要去check一下,也需要去做准入的,要去看你的cpu的需求满足不满足,如果不满足就直接报错了,out of cpu的error,写回到这个pod状态里面。

步骤2. Check network plugin:然后会去监听当前节点上网络插件的情况,如果网络插件不就绪,那么pod是启动不起来的,所以这里也会直接报错。

步骤3. Update cgroup:namspace cgroup这些技术要去启动容器进程的时候,要将容器对应的cgroup文件配置好。

步骤4. Makepoddir:pod需要存储日志,容器的日志需要在主机上有个目录,它会去将数据目录创建起来。

步骤5. Wait For attach and mount:你的pod是需要一些存储,比如configmap轻量级存储只需要将文件下载下来,然后mount到容器当中就行了,如果是更高要求的存储,比如网络存储,那么需要去创建volume,然后和这个节点产生attach关系,然后再mount到容器里面,其实就是等待存储就绪。

问题:CRI CNI CSI,它们之间关系是怎么样的呢?在启动pod的时候谁先启动,谁后启动?
回答:CRI (Container Runtime Interface)、CNI (Container Network Interface) 和 CSI (Container Storage Interface) 是 Kubernetes 中的三个关键组件,分别负责容器运行时、容器网络和容器存储。它们之间的关系是:
CRI:负责与容器运行时(如Docker、Containerd等)进行通信,以创建和管理容器。
CNI:负责为容器提供网络连接,包括为容器分配IP地址、配置网络路由等。
CSI:负责为容器提供持久化存储功能,包括挂载存储卷、快照、克隆等。
在启动Pod时,先启动CRI来创建和管理容器,然后CNI为容器分配网络连接,最后CSI为容器提供持久化存储。因此,启动顺序是先启动CRI,然后启动CNI,最后启动CSI。这样确保容器能够正常运行并连接到网络和存储资源。

上面可以很清晰看到 CSI 容器存储 就在kubelet这部分运行,就是在pod后续加载还没有启动的时候,我就得先去将存储挂载好,并且mount进来。如果这一步不过的话,它会一直卡在这里,接下来才会去做syncpod。

syncpod里面就是来计算sandbox和容器变化,如果sandbox发生变化了,就是已经在运行的容器和我pod本身不匹配了,那就是重建了,这里面其实就是computer pod action的动作,如果这个pod已经不存在了,你的容器还在,那么就需要将容器kill掉。

然后你要启动一个新的容器,那么会去经历启动的动作。之前讲了第一步就是createpodsandbox,它会去生成这些sandbox的config,它有一些mainfest,要将这些配置文件生成出来。然后pod的日志目录要创建好,然后要去调用runtime的sandbox。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubernetes_15

以containerd为例,它的cri本身又是一个grpc的服务,kubelet会去调用grpc服务,这个时候整个请求就转到containerd进程里面,kubelet就要暂时等待它的返回结果了。然后就是 create sandbox container,然后一步一步的往下走,最后由cri去调用cni的接口。

针对这种创建网络的请求,其实就是setup网络的过程,在cni里面是add network这种方法去实现的,上面就是整个pod清单的获取,一直到pod容器进程启动的这样一个过程。至于用户容器启动就不多说了。

kubelet CRI 容器运行时

CRI 容器运行时

kubelet在启动容器进程的时候,要真正的启动这些容器进程,随着版本的迭代,它逐渐将启动过程当中这些标准的行为,抽象为一个一个的接口。它的好处是kubelet就不需要和具体的runtime产生绑定关系了,kubelet自己是一些代码框架,它定义好这些接口,它只需要调用这些接口,由不同的容器运行时提供商来实现这些接口。(让运行时可以适配我的标准,这样可以使用你也可以不使用你,这就是 SPI 机制)

SPI 我定义,我使用,别人实现
API 我定义,我实现,别人使用
对于任何一个程序来说(也包括),最上层提供给别人使用的是自己的API接口,最下层依赖/使用别人的SPI机制

这样我们就可以选择是使用docker还是containerd呢还是kata其他类型的容器?这样就使得kubernetes就不和某一个运行时有具体的强绑定关系,防止被某个厂商锁死。

容器运行时(Container Runtime),运行于Kubernetes(K8s)集群的每个节点中,负责容器的整个生命周期。其中Docker是目前应用最广的。随着容器云的发展,越来越多的容器运行时涌现。为了解决这些容器运行时和Kubernetes的集成问题,在Kubernetes1.5版本中,社区推出了CRI(Container Runtime Interface,容器运行时接口)以支持更多的容器运行时。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubelet_16

如上图,CRI就是将容器运行时的这些实现标准的接口都抽象出来了,kubelet要去操作任何的容器进程都是通过CRI的接口,然后不同的容器运行时会实现这些CRI接口,最终去操作这些容器进程。

CRI 容器运行时架构

整体流程:kubele调动高层运行时通过 CRI 规范接口,高层运行时调用低层运行时通过 OCI 规范接口

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubernetes_17

kubelet是客户端,可以看到在kubelet里面有grpc client,它会去基于grpc框架去调用运行时服务,类似docker containerd这类的服务。它是运行在kubelet之外的,在安装kubernetes的时候,会先去安装docker containerd,安装完之后再去安装kubelet。

这样的话kubelet作为客户端来调用runtime的接口。这些runtime就要按照CRI的规范去实现那些接口。

容器运行时最终的目的是要去启动一个一个的容器进程,所以你可以理解为容器运行时它本身是一个中间层,它向上面对的是kubelet,向下面对的是这些容器进程。所以它分为high-level-runtime和low-level-runtime,high-level-runtime是对外提供的这些grpc服务。由客户端grpc client来调用这些服务,它接受到这些请求需要通过low-level的api去启动和操作这些容器。它(指高层运行时)有点像一层代理,它只是做命令的转发,接受来自客户端的请求,然后再去操作下面的容器进程。它所谓的low-level-runtime,容器的话就是runc。

kubele调动高层运行时通过 CRI 规范接口

Dockershim, containerd和CRI-O都是遵循CRI的容器运行时, 我们称他们为高层级运行时(High-level Runtime)。kubele调动高层运行时通过 CRI 规范接口。

CRI是Kubernetes定义的一组gRPC服务。kubelet作为客户端,基于gRPC框架,通过Socket和容器运行时通信。CRI 包括两类服务∶镜像服务(Image Service)和运行时服务(Runtime Service)。

(1) 镜像服务:提供下载、检查和删除镜像的远程程序调用。
(2) 运行时服务:包含用于管理容器生命周期,以及与容器交互的调用(exec/attach/port-forward)的远程程序调用。

imageservice提供了很多接口,主要是对image相关的操作。
runtimeservice同样通过了很多接口,可以看到对sandbox有很多操作,以及和对用户容器相关的。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_容器_18

高层运行时调用低层运行时通过 OCI 规范接口

容器运行时最终遵循了oci的一个标准,oci定义了容器相关的行业标准。这个行业标准主要分为了三大类:定义了镜像如何打包、如何解压的这样一个规范、通过镜像如何去运行容器进程这样的一个规范。

OCI(Open Container Initiative,开放容器计划)定义了创建容器的格式和运行时的开源行业标准,包括镜像规范(Image Specification)和运行时规范(Runtime Specification)。

镜像规范定义了OCI镜像的标准。高层级运行时将会下载一个OCI镜像,并把它解压成 OCI运行时文件系统包(filesystem bundle)(还有存储镜像文件到指定的目录下)。

运行时规范则描述了如何从OCI运行时文件系统包运行容器程序,并且定义它的配置、运行环境和生命周期。如何为新容器设置命名空间(namepsaces)和控制组(cgroups),以及挂载根文件系统等等操作,都是在这里定义的。它的一个参考实现是runC。我们称其为低层级运行时(Low-level R untime)。除runC以外,也有很多其他的运行时遵循OCI标准,例如kata-runtime。

CRI 高层运行时和低层运行时

高层运行时和低层运行时

容器运行时是真正管理容器的组件,分为高层运行时和低层运行时。高层运行时由 Docker、containerd或CRI-O 充当, 低层运行时由 runc、kata 或 gVisor 充当。低层运行时 kata 和 gVisor 都处于小规模落地或者实验阶段,其生态成熟度和使用案例都比较欠缺,所以除非有特殊的需求,否则 runc 几乎是必然的选择。因此对于容器运行时的选择上,主要聚焦与高层运行时的选择。

容器运行时包括高层运行时和低层运行时,流程是 kubelet 调用高层运行时,高层运行时调用低层运行时,低层运行时调用具体业务容器。在整个业务流程中,kubelet和具体业务流程是不可选择的,但是高层运行时和低层运行时是可选择的。
高层运行时有 Docker containerd 和 cri-o 三种可以选择,具体选择那种,下面会讨论这三种的优劣,一般是 containerd 和 cri-o 比较好。
低层运行时目前只有 runc 一种可用,那就那没得选,就是 runc。

如何查看当前k8s集群的高层运行时和低层运行时

kubectl get nodes -o jsonpath=‘{range .items[*]}{@.metadata.name}{“\n”}{@.status.nodeInfo.containerRuntimeVersion}{“\n”}{@.status.nodeInfo.containerRuntimeVersion}{end}’

该命令将返回一个包含每个节点名称和容器运行时版本的列表。通常,如果你安装了Docker,那么结果应该显示Docker的版本号。如果你使用其他容器运行时,你将看到相应的版本号。

kubectl get nodes 获取节点的信息。这将返回所有集群中的节点列表。
-o jsonpath 使用 JSON 路径格式化输出结果。 [-o 表示 output, 后面的 jsonpath 表示输出格式]
‘…’ JSON 路径的模板包裹在引号中。其中使用 {} 定义占位符来引用节点属性。
{range .items[*]} 是一个迭代器,用于遍历每个节点的列表。
@.metadata.name 引用节点的名称。
{“\n”} 是一个换行符,用于在每个节点信息之间添加换行。
@.status.nodeInfo.containerRuntimeVersion 引用节点的容器运行时版本。
最后,{end} 表示结束迭代。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_Pod_19

{@.metadata.name}{“\n”} 输出节点名称,这里为 m
{@.status.nodeInfo.containerRuntimeVersion}{“\n”} 输出该节点的容器运行时版本
{@.status.nodeInfo.containerRuntimeVersion}{end} 输出该节点的容器运行时版本

直接使用 kubectl get nodes -o json 也可以看到

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubernetes_20

使用 kubectl describe nodes m 也可以看到

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_Pod_21

另外,你还可以通过逐个检查每个节点上运行的容器运行时进程来确定。连接到每个节点,并使用以下命令检查运行的容器运行时进程:

ps -ef | grep containerd
  • 1.

这将在节点上查找名为"containerd"的进程。如果存在,则使用Containerd作为高层运行时。同样,你还可以使用其他容器运行时的进程名称进行检查,如Docker(“docker”)或CRI-O(“crio”)等。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_容器_22


Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubernetes_23

高层运行时: 从docker到containerd

Docker 内部关于容器化运行时功能的核心组件是 containerd ,后来 containerd 也可以直接和 kubelet 通过 CRI SPI 对接,独立在 k8s 中使用。(即 containerd 可以独立作为一个进程运行,不一定需要放在 docker 进程里面)

相对于 docker 而言,containerd 减少了 docker 所需的处理模板 dockerd 和 docker-shim ,并且对 docker 支持的存储驱动进行了优化,因此在容器的创建爱你、启动、停止、删除,已经对镜像的拉取上,都具有性能上的优势。

架构的简化同时也带来了维护的遍历。当然 docker 也具有很多 containerd 不具有的功能 (因为 containerd 是docker的简化),尽管如此,containerd 目前也基本能够满足容器的众多管理需求,所以将它作为运行时的也越来越多。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_生命周期_24

containerd是能够取代docker的,因为docker本身内嵌了containerd的,docker是一个独立的产品,我们可以通过docker的命令去管理自己的网络,管理自己的存储,所以docker大部分臃肿的逻辑都在docker daemon它自身的逻辑上面,这些逻辑处理完之后也是将请求发给自带内嵌的containerd里面去, 这样其实真正执行的逻辑是在containerd里面。

存储,网络是kubernetes和docker之间的竞争,在kubernetes里面用的都是kubernetes管理的这套体系,那么docker的那部分其实是冗余的。不再使用docker自身的daemon了,但是可以用你docker自身所带的containerd的。

三种高层运行时比较

上面说的,高层运行时包括三种,所以有选择的余地,低层运行时只有一种,就是runc,所以只能选它。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_kubelet_25


上面是三个常用的运行时,最上面的路径其实是最长的。

(1) 如果是docker运行时,那么是kubelet调用dockeshim,调用docker再去调用containerd,containerd再通过runc的接口去调用底层的容器进程。
(2) 如果是containerd那么就没有docker-shim和docker,直接由kubelet发起命令到containerd,然后containerd到runc。
(3) 如果是crio那么更加轻量级,kubelet调用crio,然后直接调用runc。

越来越简洁。

Docker和Containerd的细节差异

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_容器_26

方式1(图片上半部分):如果是通过kubelet去调用docker的话,这个请求是在kubelet的cri接口调用docker-shim,docker-shim再连接到docker-daemon,docker-daemon里面红圈的这部分才是有用的,也就是和image相关的操作,以及正真去去调用containerd的这部分操作是有用的,上面存储网络,已经docker提供的cli这些都是不需要的,那么这些都是冗余的。

方式2(图片下半部分):如果将docker换掉的话,那么红线外的部分就没有了,那么复杂性就大大降低的,直接使用cri的接口调用containerd,移除了这些不必要的调用转发环节,这样整个系统架构更加简单。

上面就是理解去掉docker的意义,这是一个必要的动作。

Containerd和CRI-O的性能比较

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_容器_27


在选型的时候一个是架构的简单程度,稳定性,还有它的性能。

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)_Pod_28

小结

问题:kubelet是如何调用容器的?
回答:kubelet 通过基于 SPI 机制,通过 CRI 接口调用 高层运行时,然后高层运行时调用低层运行时,然后低层运行时调用 Container 容器。高层运行时和低层运行时都是逻辑上的概念,需要具体实现的中间件。当前的高层运行时只有 dockershim containerd cri-o 三种选择,低层运行时只有 runC 可以选择。

注意:kubelet是如何和docker打交道的 kubelet和docker的cgroupDriver必须相同

尾声

参考资料:kubelet 架构kubelet 职责kubelet 管理 Podkubelet 创建并启动 Podkubelet CRI 容器运行时