Kubernetes复习总结(三):核心组件原理及源码:client-go、controller-runtime、Scheduler、API Server、kubelet

本文基于Kubernetes v1.22.4版本

5、client-go

1)、Informer机制

1)Informer整体架构

client-go的Informer主要包括以下组件:

  1. Reflector:Reflector从Kubernetes API Server中list&watch资源对象,然后调用DeltaFIFO的Add/Update/Delete/Replace方法将资源对象及其变化包装成Delta并将其丢到DeltaFIFO中
  2. DeltaFIFO:DeltaFIFO中存储着一个map和一个queue,即map[object key]Deltas以及object key的queue,Deltas为Delta的切片类型,Delta装有对象及对象的变化类型(Added/Updated/Deleted/Sync),Reflector负责DeltaFIFO的输入,Controller负责处理DeltaFIFO的输出
  3. Controller:Controller从DeltaFIFO的queue中pop一个object key出来,并获取其关联的Deltas出来进行处理,遍历Deltas,根据对象的变化更新Indexer中的本地内存缓存,并通知Processor,相关对象有变化事件发生
  4. Processor:Processor根据对象的变化事件类型,调用相应的ResourceEventHandler来处理对象的变化
  5. Indexer:Indexer中有Informer维护的指定资源对象的相对于etcd数据的一份本地内存缓存,可通过该缓存获取资源对象,以减少对Kubernetes API Server、对etcd的请求压力
  6. ResourceEventHandler:用户根据自身处理逻辑需要,注册自定义的ResourceEventHandler,当对象发生变化时,将触发调用对应类型的ResourceEventHandler来做处理

2)Informer调用流程

reflector.Run:

在这里插入图片描述

sharedIndexInformer.Run:

在这里插入图片描述

sharedProcessor数据处理流程:

在这里插入图片描述

processorListener.popp.addCh中的notification数据取出来,然后放到了p.nextCh

processorListener.run循环读取p.nextCh,判断对象类型,是updateNotification则调用p.handler.OnUpdate方法,是addNotification则调用p.handler.OnAdd方法,是deleteNotification则调用p.handler.OnDelete方法做处理

2)、resourceVersion

resourceVersion的作用:

  • 保证客户端数据一致性和顺序性
  • 乐观锁,实现并发控制

设置ListOptions时,resourceVersion有三种设置方法:

  • 不设置,此时会直接从etcd中读取,此时数据是最新的
  • 设置为"0",此时会从API Server Cache中获取数据
  • 设置为指定的resourceVersion,获取resourceVersion大于指定版本的所有资源对象
3)、自定义Controller的工作原理

在这里插入图片描述

Controller中主要使用到Informer和WorkQueue两个核心组件

Controller可以有一个或多个Informer来跟踪某一个或多个resource。Informer跟Kubernetes API Server保持通讯获取资源的最新状态并更新到本地的cache中,一旦跟踪的资源有变化,Informer就会调用callback把关心的变更的Object放到WorkQueue里面

Worker执行真正的业务逻辑,计算和比较WorkQueue里items的当前状态和期望状态的差别,然后通过client-go向Kubernetes API Server发送请求,直到驱动这个集群向用户要求的状态演化

6、controller-runtime

整体架构:

在这里插入图片描述

  1. Manager管理多个Controller的运行,负责初始化Cache、Client等公共依赖,并提供各个runnbale使用
  2. Client封装了对资源的CRUD操作,其中读操作实际查询的是本地Cache,写操作直接访问API Server
  3. Cache负责在Controller进程里面根据Scheme同步API Server中所有该Controller关心的资源对象,其核心是相关Resource的Informer,Informer会负责监听对应Resource的创建/删除/更新操作,以触发Controller的Reconcile逻辑
  4. Controller是控制器的业务逻辑所在的地方,一个Manager可能会有多个Controller,我们一般只需要实现Reconcile方法即可。上图的Predicate是事件过滤器,我们可以在Controller中过滤掉我们不关心的事件信息
  5. WebHook是我们准入控制实现的地方了,主要是有两类接口,一个是MutatingAdmissionWebhook需要实现Defaulter接口,一个是ValidatingAdmissionWebhook需要实现Validator接口

核心代码流程:

在这里插入图片描述

整体工作流程:

首先Controller会先向Informer注册特定资源的eventHandler;然后Cache会启动Informer,Informer向API Server发出请求,建立连接;当Informer检测到有资源变动后,使用Controller注册进来的eventHandler判断是否推入队列中;当队列中有元素被推入时,Controller会将元素取出,并执行用户侧的Reconciler

7、Scheduler

1)、调度流程
  1. 用户提交创建Pod的请求,可以通过API Server的REST API,也可用kubectl命令行工具

  2. API Server处理用户请求,存储Pod数据到etcd

  3. 调度器监听API Server,获取到未调度的Pod列表(spec.nodeName为空),循环遍历为每个Pod尝试分配节点,这个分配过程分为两个阶段:

    • 预选阶段(Predicates):过滤掉不满足条件的节点,比如Pod设置了资源的request,那么可用资源比Pod需要的资源少的主机就会被过滤掉。这一阶段输出的所有满足要求的Node将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么Pod将会一直处于Pending状态,直到有节点满足条件,在这期间调度器会不断的重试
    • 优选阶段(Priorities):为节点的优先级打分,将上一阶段过滤出来的Node列表进行打分,调度器会考虑一些整体的优化策略:比如把Deployment控制的多个Pod副本分布到不同的主机上、使用最低负载的主机等策略

    经过上面的阶段过滤后选择打分最高的Node节点和Pod进行binding操作,请求API Server将结果存储到etcd中

  4. 最后被选择出来的Node节点对应的kubelet去执行创建Pod的相关操作

2)、调度框架

调度一个Pod的过程分为两个阶段:调度周期(Scheduling Cycle)和绑定周期(Binding Cycle)

在这里插入图片描述

调度周期为Pod选择一个合适的节点,绑定周期将调度过程的决策应用到集群中。调度周期和绑定周期一起被称为调度上下文(Scheduling Context)。调度过程和绑定过程遇到该Pod不可调度或存在内部错误,则中止调度或绑定周期,该Pod将返回队列并重试

调度周期是同步运行的,同一时间点只为一个Pod进行调度;绑定周期是异步执行的,同一时间点可并发为多个Pod执行绑定

3)、调度核心实现

1)调度器运行流程

  1. 调用sched.NextPod()从activeQ中获取一个优先级最高的待调度Pod,该过程是阻塞的,当activeQ中不存在任何Pod时,sched.NextPod()处于等待状态
  2. 调用sched.Algorithm.Schedule()方法执行预选调度算法和优选调度算法,为Pod选择一个合适的节点
  3. 调用sched.assume()方法进行预绑定,为Pod设置nodeName字段,更新Scheduler缓存
  4. 调用fwk.RunReservePluginsReserve()方法运行Reserve插件的Reserve()方法
  5. 调用fwk.RunPermitPlugins()方法运行Permit插件
  6. 调用fwk.RunPreBindPlugins()方法运行PreBind插件
  7. 调用sched.bind()方法进行真正的绑定,请求API Server异步处理最终的绑定操作,写入etcd
  8. 绑定成功后,调用fwk.RunPostBindPlugins()方法运行PostBind插件

2)执行调度

在这里插入图片描述

  1. 快照Node信息,每次调度Pod时都会获取一次快照
  2. 进行Predicates阶段,找到所有满足调度条件的节点,不满足的就直接过滤
    • 预选后没有合适的Node直接返回
    • 当预选后只剩下一个node,就使用它,返回
  3. 进行Priorities阶段,执行优选算法,获得打分之后的Node列表
  4. 根据打分选择分数最高的Node

3)预选算法

在这里插入图片描述

4)优选算法

优选算法先通过prioritizeNodes()方法获得打分之后的node列表,然后再通过selectHost()方法选择分数最高的node,返回结果

prioritizeNodes()通过运行评分插件对节点进行优先排序,这些插件从RunScorePlugins()方法中为每个节点返回一个分数。每个插件的分数和Extender的分数加在一起,成为该节点的分数。整个流程如下图:

5)小结

调度器运行流程如下图:

调度核心实现总结如下:

在这里插入图片描述

4)、调度器优化

1)性能优化

方案一:调节percentageOfNodesToScore参数

一个5000个节点的集群来进行调度的话,不进行控制时,每个Pod调度都需要尝试5000次的节点预选过程,是非常消耗资源的。可以通过调节percentageOfNodesToScore参数来控制每次参与预选过程的节点比例,详细逻辑如下:

  • 如果节点数小于minFeasibleNodesToFind(默认值100),那么全部节点参与调度

  • percentageOfNodesToScore参数值是集群中每次参与调度节点的百分比,范围是1到100之间。如果集群节点数>100,那么就会根据这个值来计算让合适的节点参与调度

    举个例子,如果一个5000个节点的集群,percentageOfNodesToScore为10,也就是每次500个节点参与调度

  • 如果计算后的参与调度的节点数小于minFeasibleNodesToFind,那么返回minFeasibleNodesToFind

为了让集群中所有节点都有公平的机会去运行这些Pod,调度器将会以轮询的方式覆盖全部的节点。将Node列表想象成一个数组,调度器从数组的头部开始筛选可调度节点,依次向后直到可调度节点的数量达到percentageOfNodesToScore参数的要求。在对下一个Pod进行调度的时候,前一个Pod调度筛选停止的Node列表的位置,将会来作为这次调度筛选Node开始的位置

方案二:多调度器支持

Kubernetes也支持在集群中运行多个调度器调度不同作业,例如可以在Pod的spec.schedulerName指定对应的调度器,也可以在Job的spec.template.spec.schedulerName指定调度器

方案三:等价类划分

等价类划分(Equivalence classes),典型的用户扩容请求为一次扩容多个容器,因此可以通过将pending队列中的请求划分等价类的方式,实现批处理,显著的降低Predicates/Priorities的次数,阿里在某一年的KubeCon上提出的一个优化方式

2)集群Node资源使用优化

方案一:descheduler

在新建Pod时,调度器可以根据当时集群状态选择最优节点进行调度,但集群内资源使用状况是动态变化的,集群在一段时间内就会出现不均衡的状态,需要descheduler将节点上已经运行的Pod迁移到其他节点,使集群内资源分布达到一个比较均衡的状态。Kubernetes孵化了descheduler工具来解决这个问题

有以下几个原因我们希望将节点上运行的实例迁移到其他节点:

  • 节点上Pod利用率的变化导致某些节点利用率过低或者过高
  • 节点标签变化导致Pod的亲和与反亲和策略不满足要求
  • 新节点上线与故障节点下线

descheduler会根据相关的策略挑选出节点需要迁移的实例然后删除实例,新实例会重新通过Scheduler进行调度到合适的节点上。descheduler迁移实例的策略需要与Scheduler的策略共同使用,二者是相辅相成的

使用descheduler的目的主要有两点,一是为了提升集群的稳定性,二是为了提高集群的资源利用率

相关资料:

Kubernetes中Descheduler组件的使用与扩展

方案二:根据实际资源使用率进行调度

目前调度器默认的调度仅根据Pod的request值进行调度,可以考虑直接以实际的使用率进行调度,需要对调度器的调度策略进行扩展

相关资料:

腾讯云DynamicScheduler

哈啰Kubernetes基于水位的自定义调度器落地之路

8、API Server

1)、API Server架构设计

API Server主要提供以下三种API:

  • core APIGroup:主要在/api/v1下;
  • 非core APIGroup:其path为/apis/$NAME/$VERSION
  • 暴露系统状态的一些API:如/metrics/healthz

API的URL大致以/apis/group/version/namespaces/my-ns/myresource组成,其中API的结构大致如下图所示:

API Server由3个HTTP Server组成:

  • AggregatorServer:负责处理apiregistration.k8s.io组下的APIService资源请求,同时将来自用户的请求拦截转发给Aggregated Server(AA)
  • KubeAPIServer:负责处理Kubernetes内建资源(Pod、Deployment、Service等)的REST请求
  • APIExtensionsServer:负责处理CustomResourceDefinition(CRD)和CustomResource(CR)的REST请求

3个HTTP Server的处理顺序如上图所示,当用户请求进来,先判断AggregatorServer能否处理,否则给KubeAPIServer,如果KubeAPIServer不能处理给APIExtensionsServer处理,APIExtensionsServer是Delegation的最后一环,如果对应请求不能被处理的话则会返回404

2)、API Server请求处理流程

API Server中一个请求完整的流程如下图:

在这里插入图片描述

以一次POST请求为例,当请求到达API Server时,API Server首先会执行在http filter chain中注册的过滤器链,该过滤器对其执行一系列过滤操作,主要有认证、鉴权等检查操作。当filter chain处理完成后,请求会通过route进入到对应的handler中,handler中的操作主要是与etcd的交互,在handler中的主要的操作如下所示:

一个请求进入API Server后会经过如下几个处理过程:

在这里插入图片描述

  1. Authentication Authorization:登录和鉴权,校验Request发送者是否合法

  2. Decode & Conversion:Kubernetes中的多数resource都会有一个internal version(内部版本),因为在整个开发过程中一个resource可能会对应多个version,比如Deployment会有extensions/v1beta1apps/v1。为了避免出现问题,API Server必须要知道如何在每一对版本之间进行转换(例如:v1⇔v1alpha1、v1⇔v1beta1、v1beta1⇔v1alpha1),因此其使用了一个特殊的internal version,internal version作为一个通用的version会包含所有version的字段,它具有所有version的功能

    Decoder会首先把creater object转换到internal version,然后将其转换为storage version,storage version是在etcd中存储时的另一个version

    在解码时,首先从HTTP Path中获取期待的version,然后使用scheme以正确的version创建一个与之匹配的空对象,并使用JSON或protobuf解码器进行转换,在转换的第一步中,如果用户省略了某些字段,Decoder会把其设置为默认值

  3. Admission:在解码完成后,需要通过验证集群的全局约束来检查是否可以创建或更新对象,并根据集群配置设置默认值。在k8s.io/kubernetes/plugin/pkg/admission目录下可以看到API Server可以使用的所有全局约束插件,API Server在启动时通过设置--enable-admission-plugins参数来开启需要使用的插件,通过ValidatingAdmissionWebhook或MutatingAdmissionWebhook添加的插件也都会在此处进行工作

    动态准入控制就是通过Webhook来实现准入控制,分为两种:

    Mutating Admission Webhook:在资源持久化到etcd之前进行修改,比如增加init container或者sidecar

    Validating Admission Webhook:在资源持久化到etcd之前进行校验,不满足条件的资源直接拒绝并给出相应信息

    Istio就是通过Mutating Admission Webhook来自动将envoy这个sidecar容器注入到Pod中去的

  4. ETCD:在handler中执行完以上操作后最后会执行与etcd相关的操作,POST操作会将数据写入到etcd中

以上在handler中的主要处理流程如下所示:

v1beta1 ⇒ internal ⇒    |    ⇒       |    ⇒  v1  ⇒ json/yaml ⇒ etcd
                     admission    validation
3)、API Server核心代码流程

在这里插入图片描述

4)、Kubernetes资源存储格式

Kubernetes资源在etcd中的保存路径为prefix + “/” + 资源类型 + “/” + namespace + “/” + 具体资源名(示例:/registry/deployments/default/nginx-deployment),结合etcd3的范围查询,可快速实现按namesapace、资源名称查询

按标签查询则是通过API Server遍历指定namespace下的资源实现的,若未从API Server的Cache中查询,请求较频繁,很可能导致etcd流量较大,出现不稳定

5)、API Server优化

在这里插入图片描述

1)减少expensive request

优化点1:分页

避免一次性读取数十万的资源操作,Kubernetes list接口支持分页特性,底层基于etcd v3中实现的指定返回limit数量的范围查询

在list接口的ListOption结构体中,limit和continue参数就是为了实现分页特性而增加的

limit表示一次list请求最多查询的对象数量,一般为500。如果实际对象数量大于limit,API Server则会更新ListMeta的continue字段,Client发起的下一个list请求带上这个字段就可获取下一批对象数量。直到API Server返回空的continue值,就获取完成了整个对象结果集

优化点2:资源按namespace拆分

避免同namespace存储大量资源,尽量将资源对象拆分到不同namespace下

Kubernetes资源对象存储在etcd中的key前缀包含namespace,因此它相当于是个高效的索引字段。etcd treeIndex模块从B-tree中匹配前缀时,可快速过滤出符合条件的key-value数据

优化点3:watch bookmark机制

API Server为每种类型资源(Pod、Node等)维护一个cyclic buffer,来存储最近的一系列变更事件实现

以Pod资源的历史事件滑动窗口为例,看下它在什么场景可能会触发Client全量list同步操作

如上图所示,API Server启动后,通过list机制,加载初始Pod状态数据,随后通过watch机制监听最新Pod数据变化。当不断对Pod资源进行增加、删除、修改后,携带新resourceVersion(简称RV)的Pod事件就会不断被加入到cyclic buffer。假设cyclic buffer容量为100,RV1是最小的一个watch事件的resourceVersion,RV100是最大的一个watch事件的resourceVersion

当版本号为RV101的Pod事件到达时,RV1就会被淘汰,API Server维护的Pod最小版本号就变成了RV2

然而在Kubernetes集群中,不少组件都只关心cyclic buffer中与自己相关的事件。比如图中的kubelet只关注运行在自己节点上的Pod,假设只有RV1是它关心的Pod事件版本号,在未实现bookmark特性之前,其他RV2到RV101的事件是不会推送给它的,因此它内存中维护的resourceVersion依然是RV1

若此kubelet随后与API Server连接出现异常,它将使用版本号RV1发起watch重连操作。但是API Server cyclic buffer中的Pod最小版本号已是RV2,因此会返回"too old resource version"错误给Client,Client只能发起List操作,在获取到最新版本号后,才能重新进入监听逻辑

bookmark机制的核心思想就是在Client与Server之间保持一个心跳,即使队列中无Client需要感知的更新,Reflector内部的版本号也需要及时的更新。通过新增一个bookmark类型的事件来实现的,API Server会通过定时器将各类型资源最新的resourceVersion推送给Client,在Client与API Server网络异常重连等场景,大大降低了Client重建watch的开销,减少了relist expensive request

优化点4:更高效的watch恢复机制

在API Server重启、滚动更新时,依然还是有可能导致大量的relist操作

如上图所示,在API Server重启后,kubelet等Client会立刻带上resourceVersion发起重建watch的请求。问题就在API Server重启后,watchCache中的cyclic buffer是空的,此时watchCache中的最小resourceVersion(listResourceVersion)是etcd的最新全局版本号,也就是图中的RV200

在不少场景下,Client请求重建watch的resourceVersion是可能小于listResourceVersion的

如上图所示,集群内Pod稳定运行未发生变化,kubelet假设收到了最新的RV100事件。然而这个集群其他资源如ConfigMap,被管理员不断的修改,它就会导致导致etcd版本号新增,ConfigMap滑动窗口也会不断存储变更事件,从图中可以看到,它记录最大版本号为RV200

因此API Server重启后,Client请求重建Pod watch的resourceVersion是RV100,而Pod watchCache中的滑动窗口最小resourceVersion是RV200。显然RV100不在Pod watchCache所维护的滑动窗口中,resourceVersion就会返回"too old resource version"错误给Client,Client只能发起relist expensive request操作同步最新数据

为了进一步降低API Server重启对client watch中断的影响,Kubernetes在1.20版本中又进一步实现了更高效的watch恢复机制它通过etcd watch机制的Notify特性,实现了将etcd最新的版本号定时推送给API Server。API Server在将其转换成resourceVersion后,再通过bookmark机制推送给Client,避免了API Server重启后Client可能发起的list操作

2)控制etcd db size

kubelet组件会每隔10秒上报一次心跳给API Server,Node资源对象因为包含若干个镜像、数据卷等信息,导致Node资源对象会较大,一次心跳消息可能高达15KB以上。而且,etcd是基于Copy-on-write机制实现的MVCC数据库,每次修改都会产生新的key-value,若大量写入会导致db size持续增长

早期Kubernetes集群由于以上原因,当节点数成千上万时,kubelet产生的大量写请求就较容易造成db大小达到配额,无法写入

本质上还是Node资源对象大的问题。实际上需要更新的仅仅是Node资源对象的心跳状态,而在etcd中存储的是整个Node资源对象,并未将心跳状态拆分出来

因此Kuberentes的解决方案就是将Node资源进行拆分,把心跳状态信息从Node对象中剥离出来,通过的Lease对象来描述它

因为Lease对象非常小,更新的代价远小于Node对象,所以这样显著降低了API Server的CPU开销、etcd db size,Kubernetes 1.14版本后已经默认启用Node心跳切换到Lease API

3)优化key-value大小

etcd适合存储较小的key-value数据,etcd本身也做了一系列硬限制,比如key的value大小默认不能超过1.5MB

在成千上万个节点的集群中,一个服务可能背后有上万个Pod。而服务对应的Endpoints资源含有大量的独立的endpoints信息,这会导致Endpoints资源大小达到etcd的value大小限制,etcd拒绝更新。

另外,kube-proxy等组件会实时监听Endpoints资源,一个endpoint变化就会产生较大的流量,导致API Server等组件流量超大、出现一系列性能瓶颈

Kubernetes设计了EndpointSlice概念,每个EndpointSlice最大支持保存100个endpoints,成功解决了key-value过大、变更同步导致流量超大等一系列瓶颈

4)etcd优化

Kubernetes社区在解决大集群的挑战的同时,etcd社区也在不断优化、新增特性,提升etcd在Kubernetes场景下的稳定性和性能

优化点1:并发读特性

为什么etcd无法支持大量的read expensive request呢?

除了一直强调的容易导致OOM、大流量导致丢包外,etcd根本性瓶颈是在etcd 3.4版本之前,expensive read request会长时间持有MVCC模块的buffer读锁RLock。而写请求执行完后,需升级锁至Lock,expensive request导致写事务阻塞在升级锁过程中,最终导致写请求超时

为了解决此问题,etcd 3.4版本实现了并发读特性。核心解决方案是去掉了读写锁,每个读事务拥有一个buffer。在收到读请求创建读事务对象时,全量拷贝写事务维护的buffer到读事务buffer中

通过并发读特性,显著降低了List Pod和CRD等expensive read request对写性能的影响,延时不再突增、抖动

优化点2:改善Watch Notify机制

为了配合Kubernetes社区实现更高效的watch恢复机制,etcd改善了Watch Notify机制,早期Notify消息发送间隔是固定的10分钟

在etcd 3.4.11版本中,新增了--experimental-watch-progress-notify-interval参数使Notify间隔时间可配置,最小支持为100ms,满足了Kubernetes业务场景的诉求

5)API Server负载不均衡问题

Client与API Server是使用HTTP2协议连接,HTTP2的多个请求都会复用底层的同一个TCP连接并且长时间不断开。而在API Server滚动升级或者某个API Server实例重启时,LoadBalance没有及时的将所有副本挂载完毕,Client能敏感的感知到连接的断开并立刻发起新的请求,这时候很容易引起较后启动(或者较后挂载LoadBalance)的API Server没有一点流量,并且可能永远都得不到负载均衡

在Kubernetes 1.18版本中,新增了GOAWAY Chance新特性。增加了一种通用的HTTP filter,API Server概率性(建议1/1000)的随机关闭和Client的链接(向Client发送GOAWAY)。关闭是优雅的关闭,不会影响API Server和Client正在进行中的长时间请求(如watch等),但是收到GOAWAY之后,Client新的请求就会重新建立一个新的TCP链接去访问API Server从而能让LoadBalance再做一次负载均衡

6)API Server参数调整

  • --max-mutating-requests-inflight:限制同时进行的变更(创建、更新或删除资源的)请求的数量
  • --max-requests-inflight:限制同时处理的所有请求的数量
  • --watch-cache-sizes:API Server对etcd中watch事件的缓存大小。当有新的事件发生时,API服务器会将这些事件缓存在watch缓存中,以便在有订阅者时能够快速地将事件推送给客户端

7)etcd多实例支持

对于不同Object进行分库存储,首先应该将数据与状态分离,即将Event放在单独的etcd实例中,在API Server的配置中加上--etcd-servers-overrides=/events#https://xxx:3379;https://xxx:3379;https://xxx:3379;https://xxxx:3379;https://xxx:3379,后期可以将Pod、Node等Object也分离在单独的etcd实例中

相关资料:

19 | Kubernetes基础应用:创建一个Pod背后etcd发生了什么?

20 | Kubernetes高级应用:如何优化业务场景使etcd能支撑上万节点集群?

腾讯大规模云原生平台稳定性实践-唐聪.pdf

字节跳动在 k8s 的性能优化实践-陈逸翔

解决Kubernetes APIServer流量不均衡问题

9、kubelet

1)、kubelet工作原理

kubelet的工作核心就是一个控制循环,即:SyncLoop(图中的大圆圈)。而驱动这个控制循环运行的事件,包括:Pod更新事件、Pod生命周期变化、kubelet本身设置的执行周期、定时的清理事件

kubelet还负责维护着很多其他的子控制循环(也就是图中的小圆圈),叫做xxxManager

2)、kubelet中的核心模块
  • PLEG(Pod Lifecycle Event Generator):PLEG会一直调用container runtime获取本节点containers/sandboxes的信息,并与自身维护的pods cache信息进行对比,生成对应的PodLifecycleEvent,然后输出到plegCh中,最终由syncLoop进行消费,然后由syncPod来触发Pod同步处理过程,最终达到用户的期望状态

  • probeManager:probeManager依赖于statusManager、livenessManager、readinessManager,会定时去监控Pod中容器的健康状况,当前支持两种类型的探针:livenessProbe和readinessProbe

    • livenessProbe:用于判断容器是否存活,如果探测失败,kubelet会kill掉该容器,并根据容器的重启策略做相应的处理
    • readinessProbe:用于判断容器是否启动完成,将探测成功的容器加入到该Pod所在Service的endpoints中,反之则移除
  • statusManager:statusManager负责维护状态信息,并把Pod状态更新到API Server,但是它并不负责监控Pod状态的变化,而是提供对应的接口供其他组件调用,比如probeManager

  • containerRefManager:容器引用的管理,用来报告容器的创建、失败等事件,通过定义map来实现了containerID与v1.ObjectReferece容器引用的映射

  • evictionManager:当节点的内存、磁盘等不可压缩的资源不足时,达到了配置的evict策略,Node会变为pressure状态,此时kubelet会按照qosClass顺序来驱逐Pod,以此来保证节点的稳定性。可以通过配置kubelet启动参数--eviction-hard=来决定evict的策略值

  • imageManager:imageManager调用containerRuntime提供的PullImage/GetImageRef/ListImages/RemoveImage/ImageStates方法来保证Pod运行所需要的镜像

  • volumeManager:volumeManager负责Node节点上Pod所使用Volume的管理,Volume与Pod的生命周期关联,负责Pod创建删除过程中Volume的mount/umount/attach/detach流程,Kubernetes采用Volume Plugins的方式,实现存储卷的挂载等操作

  • containerManager:containerManager负责Node节点上运行的容器的cgroup配置信息,kubelet启动参数如果指定--cgroups-per-qos的时候,kubelet会启动goroutine来周期性的更新Pod的cgroup信息,维护其正确性,该参数默认为true,实现了Pod Guaranteed/BestEffort/Burstable三种级别的Qos

  • containerRuntime:containerRuntime负责kubelet与不同的runtime实现进行对接,实现对于底层container的操作,初始化之后得到的runtime实例将会被之前描述的组件所使用

  • podManager:podManager提供了接口来存储和访问Pod的信息,podManager会被statusManager/volumeManager/runtimeManager所调用,podManager的接口处理流程里面会调用secretManager以及configMapManager

3)、CRI与容器运行时

kubelet调用下层容器运行时的执行过程,并不会直接调用Docker的API,而是通过一组叫作CRI(Container Runtime Interface,容器运行时接口)的gRPC接口来间接执行的

CRI shim负责响应CRI请求,扮演kubelet与容器项目之间的垫片(shim)。CRI shim实现了CRI规定的每个接口,然后把具体的CRI请求翻译成对后端容器项目的请求或者操作

每一种容器项目都可以自己实现一个CRI shim,自行对CRI请求进行处理,这样,Kubernetes就有了一个统一的容器抽象层,使得下层容器运行时可以自由地对接进入Kubernetes当中

如果使用Docker的话,dockershim负责处理CRI请求,然后组装成Docker API请求发给Docker Daemon

CRI接口可以分为两组:

  • RuntimeService:主要是跟容器相关的操作,比如创建、启动、删除容器,执行exec命令等
  • ImageManagerService:主要是容器镜像相关的操作,比如拉取镜像、删除镜像等

CRI接口核心方法如下图:

CRI设计的一个重要原则,就是确保这个接口本身,只关注容器,不关注Pod,在CRI的设计里并没有一个直接创建Pod或者启动Pod的接口

PodSandboxManager中包含RunPodSandbox方法,这个PodSandbox对应的并不是Kubernetes里的Pod API对象,而只是抽取了Pod里的一部分与容器运行时相关的字段,比如HostName、DnsConfig、CgroupParent等。所以说,PodSandbox描述的其实是Kubernetes将Pod这个概念映射到容器运行时层面所需要的字段,或者说是一个Pod对象子集

比如,当执行kubectl run创建了一个名叫foo的、包括了A、B两个容器的Pod之后。如果是Docker项目,dockershim就会创建出一个名叫foo的Infra容器(pause容器)用来hold住整个Pod的Network Namespace

4)、kubelet创建Pod流程

kubelet创建Pod流程如下图

在这里插入图片描述

kubelet核心流程如下图

在这里插入图片描述

5)、kubelet驱逐

对于一些不可压缩的资源,如内存、磁盘,kubelet都会监控其指标来触发Pod的驱逐,kubelet依据Pod的资源消耗和优先级来驱逐Pod进行资源的回收:

  • 如果Pod资源使用量超过资源请求值,则优先驱逐
  • 根据Pod Priority驱逐
  • Pod真实资源使用量越高则越优先驱逐

kubelet驱逐会根据三个Qos级别来做不同的处理,驱逐顺序是BestEffort>Burstable>Guaranteed

  • BestEffort:容器必须没有任何内存或者CPU的request或limit
  • Burstable:Pod里至少有一个容器有内存或者CPU请求且不满足Guarantee等级的要求,即内存/CPU的request和limit值设置的不同
  • Guaranteed:Pod里的每个容器都必须有内存/CPU的request和limit,而且值必须相等
6)、kubectl exec工作原理

CRI shim需要实现exec、logs等接口。这些gRPC接口调用期间,kubelet需要跟容器项目维护一个长连接来传输数据。这种API称之为Streaming API。CRI shim里对Streaming API的实现,依赖于一套独立的Streaming Server机制。原理如下图:

  1. 当对一个容器执行kubectl exec命令的时候,这个请求首先交给API Server,然后API Server就会调用kubelet的Exec API
  2. kubelet就会调用CRI的Exec接口,而负责响应这个接口的就是具体的CRI shim
  3. CRI shim并不会直接去调用后端的容器项目(比如Docker)来进行处理,而只会返回一个URL给kubelet。这个URL就是该CRI shim对应的Streaming Server的地址和端口
  4. 而kubelet在拿到这个URL之后,就会把它以Redirect的方式返回给API Server。这时候,API Server就会通过重定向来向Streaming Server发起真正的/exec请求,与它建立长连接

这个Streaming Server本身是需要通过使用SIG-Node维护的Streaming API库来实现的。并且,Streaming Server会在CRI shim启动时就一起启动。此外,Stream Server这一部分具体怎么实现,完全可以由CRI shim的维护者自行决定。比如,对于Docker项目来说,dockershim就是直接调用Docker的Exec API来作为实现的

7)、kubelet优化

1)单机Pod个数

最好按默认110个配置,因为如果单个节点配置的Pod数量太多,容易引起PLEG 、磁盘IO压力或者cpu负载高等问题

2)驱逐策略

kubelet监控集群节点的内存、磁盘空间和文件系统的inode等资源,根据kubelet启动参数中的驱逐策略配置,当这些资源中的一个或者多个达到特定的消耗水平,kubelet会主动地驱逐节点上一个或者多个Pod,以回收资源,降低节点资源压力

kubelet的默认硬驱逐条件:

  • memory.available<100Mi
  • nodefs.available<10%
  • imagefs.available<15%
  • nodefs.inodesFree<5%(Linux节点)

3)使用node lease减少心跳上报频率

在大规模场景下,大量Node的心跳汇报严重影响了Node的watch,API Server处理心跳请求也需要非常大的开销。而开启nodeLease之后,kubelet会使用非常轻量的nodeLease对象(0.1 KB)更新请求替换老的Update Node Status方式,这会大大减轻API Server的负担

4)原地升级

Kubernetes默认只要Pod的spec信息有改动,例如镜像信息,此时Pod的hash值就会改变,然后会导致Pod的销毁重建,一个Pod中可能包含了主业务容器,还有不可剥离的依赖业务容器,以及SideCar组件容器等,这在生产环境中代价是很大的,一方面ip和hostname可能会发生改变,Pod重启也需要一定的时间,另一方面频繁的重建也给集群管理带来了更多的压力,甚至还可能导致无法调度成功。为了解决该问题,就需要支持容器的原地升级

原地升级是一种升级应用容器镜像甚至环境变量的全新方式。它只会用新的镜像重建Pod中的特定容器,整个Pod以及其中的其他容器都不会被影响。因此它带来了更快的发布速度,以及避免了对其他Scheduler、CNI、CSI等组件的负面影响,例如OpenKruise就支持原地升级的方式

相关资料:

OpenKruise

推荐阅读:

client-go:

client-go源码学习(一):client-go源码结构、Client客户端对象

client-go源码学习(二):Reflector、DeltaFIFO

client-go源码学习(三):Indexer、SharedInformer

client-go源码学习(四):自定义Controller的工作原理、WorkQueue

controller-runtime:

controller-runtime源码学习

Scheduler:

Kubernetes调度器源码学习(一):调度器工作原理、调度器启动流程、调度队列

Kubernetes调度器源码学习(二):调度核心实现

Kubernetes调度器源码学习(三):Preempt抢占机制、调度失败与重试处理

API Server:

Kubernetes API Server源码学习(一):API Server架构设计、API Server启动过程、APIObject的装载、Scheme详解、GenericAPIServer

Kubernetes API Server源码学习(二):OpenAPI、API Resource的装载、HTTP Server具体是怎么跑起来的?

Kubernetes API Server源码学习(三):KubeAPIServer、APIExtensionsServer、AggregatorServer

Kubernetes API Server源码学习(四):Admission机制的实现、HttpReq的处理过程、Authentication与Authorization

kubelet:

kubelet源码学习(一):kubelet工作原理、kubelet启动过程

kubelet源码学习(二):kubelet创建Pod流程

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值