kubernetes源码-kubelet 原理和源码分析(五)
kubelet 驱逐管理器,驱逐 pod 回收资源,源码为 kubernetes 的 release-1.26 分支 .
写在前面
-
kubernetes 日常维护中,或多或少会碰到 pod 被驱逐的情况, pod 为什么会被驱逐呢?如果我们给节点设置驱逐阈值,那么当节点资源使用到了一定的阈值的时候, kubelet 就会帮我们把低优先级的 pod 从本节点驱逐出去,从而释放出资源供其他高优先级的 pod 使用。
-
驱逐分 2 种,软驱逐和硬驱逐,从字面来看,软驱逐就是设置一个时间范围,超过这个时间范围才被视为是需要触发驱逐的操作的,相反,硬驱逐就是立刻驱逐。
-
kubelet 的驱逐和通过 api-server 接口调用的驱逐还有一定的区别,通过 api-server 调用接口发生的驱逐,实际就等于执行了正常的 delete 操作,具体可以看官方网站的 API 发起的驱逐。
那么接下来,我们来看看 kubelet 的驱逐源码,看看它的逻辑是什么样的。
函数入口
在 pkg/kubelet/kubelet.go 文件下,我们可以在构建 kubelet 的构造函数中看到 evictionManager 的生成,然后在初始化依赖的时候,将 evictionManager 启动。
|
|
启动函数
-
启动函数比较重要的方法就是 synchronize() 。
-
我们可以看到它有 2 个地方触发驱逐,一个是驱逐管理器监控,每间隔 10s 执行一次,一个是 notifier.Start() ,他们的底层都是去调用 synchronize() 。
|
|
synchronize()
我们先看看 synchronize() 的逻辑,然后再去看看 notifier.Start() 是实现了什么。
-
根据分区构建排名函数:
-
检查容器运行时存放容器镜像的所在分区。
-
检查 kubelet root-dir 目录所在的分区。
-
如果运行时的镜像存储分区和 kubelet 目录所在的分区不是同一分区,则 nodefs 根据 pod 的日志和本地存储的使用量进行排名,imagefs 根据容器的可写层使用量进行排名。反之,他们共享共同的排名函数。
-
-
构建节点资源回收函数,套路跟上面一样,判断是否同个分区,然后进行构建。
-
构建函数会根据上面的分区检查 kubelet 和 运行时的存储目录是否在同一个分区,并决定是否清理相应的数据。
-
获取所有处于活跃状态的 pod ,过滤掉处于退出状态和正在退出状态的 pod 。
-
获取来自 kubelet 对节点的统计信息汇总摘要,如果 updateStats 为 true,则将获取过程中也会更新一些统计信息。
-
获取观察结果并获取一个函数来导出与这些观察结果相关的 Pod 使用统计数据,这个函数接受一个 pod 参数,返回该 pod 的资源使用情况。
-
notifier.UpdateThreshold() 根据提供的指标更新内存 cgroup 阈值。 使用最新的指标调用 UpdateThreshold 使 ThresholdNotifier 更准确地触发驱逐。
-
thresholdsMet() 计算并返回一个切片,记录哪些资源的阈值和最小驱逐回收的值的和大于他们的可用容量。
- 比如 evictionHard: nodefs.available: “1Gi” 最小驱逐回收 evictionMinimumReclaim: nodefs.available: “500Mi” ,则 kubelet 会回收资源,直到信号达到 1GiB 的条件, 然后继续回收至少 500MiB 直到信号达到 1.5GiB。返回的结果记录的是哪些资源还没回收资源至驱逐阈值的水位线。
-
记录最后一次观察到驱逐阈值的时间。
-
设置跟驱逐阈值相关联的节点状态,如:DiskPressure ,MemoryPressure ,PIDPressure ,并记录这些状态的最后一次观察到的时间。
-
在某些情况下,节点在软驱逐条件上下振荡,而没有保持定义的宽限期。 这会导致报告的节点条件在 true 和 false 之间不断切换,从而导致错误的驱逐决策。为了防止振荡,你可以使用 eviction-pressure-transition-period 标志, 该标志控制 kubelet 在将节点条件转换为不同状态之前必须等待的时间。 过渡期的默认值为 5m。
-
localStorageEviction() 检查每个 Pod 的 EmptyDir 卷使用情况,并确定其是否超出指定限制并需要被驱逐。 它还会检查 pod 中的每个容器,如果容器可写层使用量超过限制,pod 也会被驱逐。
-
对资源类型进行排序,内存优先于其他所有资源类型,优先需要被回收。
-
reclaimNodeLevelResources() 在最终驱逐用户 Pod 之前检查是否有节点级别的资源可以回收以减轻节点压力,这里分别是调用 containerGC.DeleteAllUnusedContainers, imageGC.DeleteUnusedImages 回收没有在使用的容器和镜像,如果在这一步能回收足够的资源,则无需驱逐 pod ,否则代码逻辑则需要继续往下走。
-
接下来,根据需要回收的资源类型,获取资源排名函数,rank(activePods, statsFunc) 。传入 activePods, statsFunc ,对 pod 进行优先级排序。
- 这里拿内存来看看它怎么排序的, orderedBy(exceedMemoryRequests(stats), priority, memory(stats)).Sort(pods) ,我们可以看到它先检查 pod 内存有没有超过 requests ,再根据优先级,最后是内存的使用情况。
-
for 循环对 activePods 执行遍历,选择最优 pod kill ,每个驱逐周期只驱逐一个 pod 。
|
|
evictPod()
对 pod 进行驱逐,kubelet 调用的 evictPod() 方法。我们看看它的逻辑。
他的实现实际是使用 killPodFunc() 函数,我们看看 killPodFunc() 。
|
|
killPodFunc()
该函数在构建驱逐管理器的时候,由 kubelet 传给驱逐管理器的。
函数位于 pkg/kubelet/pod_workers.go 文件下。
具体逻辑实际就是调用 podWorkers.UpdatePod() 函数,对 pod 下发 kill 的更新指令,触发 syncTerminatingPod() 方法对 pod 进行 kill ,statusManager 设置 pod 状态,然后进入下一个阶段的 pod 处理,释放 pod 资源。
|
|
notifier.Start()
-
notifier.Start() 这里比较不太好理解,理解起来比较绕。
-
在前面的 UpdateThreshold() 逻辑里面,最后有一段 m.notifier.Start(m.events) ,实际就是启动监听内核事件的代码逻辑,为下面的消费内核事件做生产者,通过通道通信。
-
然后 notifier.Start() 去消费内核事件,检测到内核事件时,执行 synchronize() 。
-
每 10s 检查一次是否有事件更新,通过 epoll 来监听上述的 eventfd,当监听到内核发送的事件时,说明使用的内存已超过阈值。
pkg/kubelet/eviction/memory_threshold_notifier.go 文件是 notifier.Start() 的源码位置。
|
|
pkg/kubelet/eviction/threshold_notifier_linux.go 文件是 m.notifier.Start(m.events) 的源码位置。
|
|
waitForPodsCleanup()
执行完 synchronize() 会看到它需要等 pod 清理完成,我们看看 waitForPodsCleanup() 到底清理了什么。
-
他调用 podCleanedUpFunc() 对 pod 进行检查。
-
然后通过 kubelet 的 statusManager 检查能否获取到 pod 的状态,如果获取不到,则调用 PodResourcesAreReclaimed() 方法进一步做检查。
-
这里对 pod 做以下检查:
-
是否有容器正在运行。
-
检查存储卷是否还存在。
-
检查 sandbox 容器是否已清理。
-
-
如果 podCleanedUpFunc() 返回 false 说明没有完成清理,退出 for 循环等待下一次检查,默认周期是 1s ,直到检查完成或 30s 超时,退出 waitForPodsCleanup() 。
|
|
总结
-
kubelet 驱逐这一块的源码,阅读起来相当困难,第一是因为它穿插调用很多地方的函数,然后参数又多,而且参数大部分是自定义结构体类型,还有伴随着方法,理解起来比较绕比较费脑,也不好写笔记。
-
还需要配合 podworker 的代码一起看,因为它 kill 的过程就是通过 podworker 执行的。
-
里面还有一段逻辑判断 pod IsStaticPod 是否静态 pod, IsMirrorPod 是否镜像 pod , IsCriticalPodBasedOnPriority 是否 CriticalPod,如果不想 pod 被驱逐,可以通过把 pod 设置为 CriticalPod ,避免被驱逐。