本文基于Kubernetes v1.22.4版本
5、client-go
1)、Informer机制
1)Informer整体架构
![](https://i-blog.csdnimg.cn/blog_migrate/7bf86fe269b7b8bde8e7508b58ccadd9.png#pic_center)
client-go的Informer主要包括以下组件:
- Reflector:Reflector从Kubernetes API Server中list&watch资源对象,然后调用DeltaFIFO的Add/Update/Delete/Replace方法将资源对象及其变化包装成Delta并将其丢到DeltaFIFO中
- DeltaFIFO:DeltaFIFO中存储着一个map和一个queue,即map[object key]Deltas以及object key的queue,Deltas为Delta的切片类型,Delta装有对象及对象的变化类型(Added/Updated/Deleted/Sync),Reflector负责DeltaFIFO的输入,Controller负责处理DeltaFIFO的输出
- Controller:Controller从DeltaFIFO的queue中pop一个object key出来,并获取其关联的Deltas出来进行处理,遍历Deltas,根据对象的变化更新Indexer中的本地内存缓存,并通知Processor,相关对象有变化事件发生
- Processor:Processor根据对象的变化事件类型,调用相应的ResourceEventHandler来处理对象的变化
- Indexer:Indexer中有Informer维护的指定资源对象的相对于etcd数据的一份本地内存缓存,可通过该缓存获取资源对象,以减少对Kubernetes API Server、对etcd的请求压力
- ResourceEventHandler:用户根据自身处理逻辑需要,注册自定义的ResourceEventHandler,当对象发生变化时,将触发调用对应类型的ResourceEventHandler来做处理
2)Informer调用流程
reflector.Run:
sharedIndexInformer.Run:
sharedProcessor数据处理流程:
processorListener.pop
将p.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
整体架构:
- Manager管理多个Controller的运行,负责初始化Cache、Client等公共依赖,并提供各个runnbale使用
- Client封装了对资源的CRUD操作,其中读操作实际查询的是本地Cache,写操作直接访问API Server
- Cache负责在Controller进程里面根据Scheme同步API Server中所有该Controller关心的资源对象,其核心是相关Resource的Informer,Informer会负责监听对应Resource的创建/删除/更新操作,以触发Controller的Reconcile逻辑
- Controller是控制器的业务逻辑所在的地方,一个Manager可能会有多个Controller,我们一般只需要实现Reconcile方法即可。上图的Predicate是事件过滤器,我们可以在Controller中过滤掉我们不关心的事件信息
- WebHook是我们准入控制实现的地方了,主要是有两类接口,一个是MutatingAdmissionWebhook需要实现Defaulter接口,一个是ValidatingAdmissionWebhook需要实现Validator接口
核心代码流程:
整体工作流程:
![](https://i-blog.csdnimg.cn/blog_migrate/20f58b0036c40ca3aa75200df8d1472b.png#pic_center)
首先Controller会先向Informer注册特定资源的eventHandler;然后Cache会启动Informer,Informer向API Server发出请求,建立连接;当Informer检测到有资源变动后,使用Controller注册进来的eventHandler判断是否推入队列中;当队列中有元素被推入时,Controller会将元素取出,并执行用户侧的Reconciler
7、Scheduler
1)、调度流程
-
用户提交创建Pod的请求,可以通过API Server的REST API,也可用kubectl命令行工具
-
API Server处理用户请求,存储Pod数据到etcd
-
调度器监听API Server,获取到未调度的Pod列表(
spec.nodeName
为空),循环遍历为每个Pod尝试分配节点,这个分配过程分为两个阶段:- 预选阶段(Predicates):过滤掉不满足条件的节点,比如Pod设置了资源的request,那么可用资源比Pod需要的资源少的主机就会被过滤掉。这一阶段输出的所有满足要求的Node将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么Pod将会一直处于Pending状态,直到有节点满足条件,在这期间调度器会不断的重试
- 优选阶段(Priorities):为节点的优先级打分,将上一阶段过滤出来的Node列表进行打分,调度器会考虑一些整体的优化策略:比如把Deployment控制的多个Pod副本分布到不同的主机上、使用最低负载的主机等策略
经过上面的阶段过滤后选择打分最高的Node节点和Pod进行binding操作,请求API Server将结果存储到etcd中
-
最后被选择出来的Node节点对应的kubelet去执行创建Pod的相关操作
2)、调度框架
调度一个Pod的过程分为两个阶段:调度周期(Scheduling Cycle)和绑定周期(Binding Cycle)
调度周期为Pod选择一个合适的节点,绑定周期将调度过程的决策应用到集群中。调度周期和绑定周期一起被称为调度上下文(Scheduling Context)。调度过程和绑定过程遇到该Pod不可调度或存在内部错误,则中止调度或绑定周期,该Pod将返回队列并重试
调度周期是同步运行的,同一时间点只为一个Pod进行调度;绑定周期是异步执行的,同一时间点可并发为多个Pod执行绑定
3)、调度核心实现
1)调度器运行流程
![](https://i-blog.csdnimg.cn/blog_migrate/736509fe89cb8d071c23b05fb24f1182.png#pic_center)
- 调用
sched.NextPod()
从activeQ中获取一个优先级最高的待调度Pod,该过程是阻塞的,当activeQ中不存在任何Pod时,sched.NextPod()
处于等待状态 - 调用
sched.Algorithm.Schedule()
方法执行预选调度算法和优选调度算法,为Pod选择一个合适的节点 - 调用
sched.assume()
方法进行预绑定,为Pod设置nodeName字段,更新Scheduler缓存 - 调用
fwk.RunReservePluginsReserve()
方法运行Reserve插件的Reserve()
方法 - 调用
fwk.RunPermitPlugins()
方法运行Permit插件 - 调用
fwk.RunPreBindPlugins()
方法运行PreBind插件 - 调用
sched.bind()
方法进行真正的绑定,请求API Server异步处理最终的绑定操作,写入etcd - 绑定成功后,调用
fwk.RunPostBindPlugins()
方法运行PostBind插件
2)执行调度
- 快照Node信息,每次调度Pod时都会获取一次快照
- 进行Predicates阶段,找到所有满足调度条件的节点,不满足的就直接过滤
- 预选后没有合适的Node直接返回
- 当预选后只剩下一个node,就使用它,返回
- 进行Priorities阶段,执行优选算法,获得打分之后的Node列表
- 根据打分选择分数最高的Node
3)预选算法
4)优选算法
优选算法先通过prioritizeNodes()
方法获得打分之后的node列表,然后再通过selectHost()
方法选择分数最高的node,返回结果
prioritizeNodes()
通过运行评分插件对节点进行优先排序,这些插件从RunScorePlugins()
方法中为每个节点返回一个分数。每个插件的分数和Extender的分数加在一起,成为该节点的分数。整个流程如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/8869727b7d702a4e96805e6a1daf07e3.png#pic_center)
5)小结
调度器运行流程如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/e31a0fc20a5df9bb5f5300eaa28931a1.png#pic_center)
调度核心实现总结如下:
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值进行调度,可以考虑直接以实际的使用率进行调度,需要对调度器的调度策略进行扩展
相关资料:
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的结构大致如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/d885972d09985974db9dd07644f26bab.png#pic_center)
API Server由3个HTTP Server组成:
![](https://i-blog.csdnimg.cn/blog_migrate/2b0c6b741f97cb8bc2bd8f3f5558daee.png#pic_center)
- AggregatorServer:负责处理
apiregistration.k8s.io
组下的APIService资源请求,同时将来自用户的请求拦截转发给Aggregated Server(AA) - KubeAPIServer:负责处理Kubernetes内建资源(Pod、Deployment、Service等)的REST请求
- APIExtensionsServer:负责处理CustomResourceDefinition(CRD)和CustomResource(CR)的REST请求
![](https://i-blog.csdnimg.cn/blog_migrate/251ccfa68d00b089fc142d9ae6c3adf6.png#pic_center)
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中的主要的操作如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/58e79e495ac54ef8a98722c3410d4e9e.png#pic_center)
一个请求进入API Server后会经过如下几个处理过程:
-
Authentication Authorization:登录和鉴权,校验Request发送者是否合法
-
Decode & Conversion:Kubernetes中的多数resource都会有一个internal version(内部版本),因为在整个开发过程中一个resource可能会对应多个version,比如Deployment会有
extensions/v1beta1
和apps/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会把其设置为默认值
-
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中去的
-
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同步操作
![](https://i-blog.csdnimg.cn/blog_migrate/bd500e8c9d1c0561b89bf391afd6f114.png#pic_center)
如上图所示,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操作
![](https://i-blog.csdnimg.cn/blog_migrate/408dbd8eff98544154f4777a30d9c843.png#pic_center)
如上图所示,在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能支撑上万节点集群?
9、kubelet
1)、kubelet工作原理
![](https://i-blog.csdnimg.cn/blog_migrate/c64889e0c33f39ed982a8865279b2757.png#pic_center)
kubelet的工作核心就是一个控制循环,即:SyncLoop(图中的大圆圈)。而驱动这个控制循环运行的事件,包括:Pod更新事件、Pod生命周期变化、kubelet本身设置的执行周期、定时的清理事件
kubelet还负责维护着很多其他的子控制循环(也就是图中的小圆圈),叫做xxxManager
2)、kubelet中的核心模块
![](https://i-blog.csdnimg.cn/blog_migrate/7b85d17481ed1740ee75048d62c281e7.png#pic_center)
-
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接口来间接执行的
![](https://i-blog.csdnimg.cn/blog_migrate/e2c1f8222cea5a04f86c35173044d695.png#pic_center)
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接口核心方法如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/625ac7f4af565053b7c714fc0cca8283.png#pic_center)
CRI设计的一个重要原则,就是确保这个接口本身,只关注容器,不关注Pod,在CRI的设计里并没有一个直接创建Pod或者启动Pod的接口
PodSandboxManager中包含RunPodSandbox方法,这个PodSandbox对应的并不是Kubernetes里的Pod API对象,而只是抽取了Pod里的一部分与容器运行时相关的字段,比如HostName、DnsConfig、CgroupParent等。所以说,PodSandbox描述的其实是Kubernetes将Pod这个概念映射到容器运行时层面所需要的字段,或者说是一个Pod对象子集
![](https://i-blog.csdnimg.cn/blog_migrate/2b39432ea06efbfcc80923c0f81f8e11.png#pic_center)
比如,当执行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真实资源使用量越高则越优先驱逐
![](https://i-blog.csdnimg.cn/blog_migrate/245e06a903b68f9a18b71399ab374616.png#pic_center)
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机制。原理如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/743412337cce8648b90dc6f848f268e6.png#pic_center)
- 当对一个容器执行kubectl exec命令的时候,这个请求首先交给API Server,然后API Server就会调用kubelet的Exec API
- kubelet就会调用CRI的Exec接口,而负责响应这个接口的就是具体的CRI shim
- CRI shim并不会直接去调用后端的容器项目(比如Docker)来进行处理,而只会返回一个URL给kubelet。这个URL就是该CRI shim对应的Streaming Server的地址和端口
- 而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就支持原地升级的方式
相关资料:
推荐阅读:
client-go:
client-go源码学习(一):client-go源码结构、Client客户端对象
client-go源码学习(二):Reflector、DeltaFIFO
client-go源码学习(三):Indexer、SharedInformer
client-go源码学习(四):自定义Controller的工作原理、WorkQueue
controller-runtime:
Scheduler:
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: