基础
驱逐的方式
驱逐 Pod 的方式,主要有 2 大类:
-
基于驱逐 API 主动发起的驱逐行为。
-
kubelet 基于节点压力(kubelet 会不断检查节点资源,比如内存、CPU、磁盘),主动发起的驱逐行为,又分为两种
-
硬驱逐。
硬驱逐策略定义了节点上资源消耗的绝对限制。一旦实际使用量超过了这些限制,Kubelet 会立即采取行动终止 Pod,以恢复资源的利用率到可接受的水平。例如,您可以对内存使用量设置硬限制,一旦超出,立即触发 Pod 驱逐。
-
软驱逐。
软驱逐策略提供了一种更灵活的驱逐机制,允许配置阈值和一个宽限期。实际资源使用量超过软阈值时,Kubelet 将等待定义的宽限期,如果在宽限期结束时资源使用量仍然超标,则开始终止 Pod。如果期间该资源又恢复到低于阈值时,则不进行 Pod 驱逐。
-
驱逐的顺序
Kubelet 在决定驱逐哪些 Pod 时,会遵循一定的顺序或者策略,比如:
- 驱逐匿名 Pod(不属于任何 ReplicaSet、Deployment 或 Job 的 Pod)。
- 根据 Pod 的 Quality of Service(QoS)类别进行驱逐,顺序通常是 BestEffort、Burstable 到 Guaranteed
- Best-Effort(尽最大努力):Pod 中所有容器均未设置 requests 和 limits。
- Burstable(不稳定的):Pod 中只要有一个容器的 requests 和 limits 的设置不相同(其实简单来说就是除 guaranteed 和 best-effort 之外)
- Guaranteed(有保证的)
- Pod 中的所有容器都且仅设置了 CPU 和 Mem 的 limits。
- Pod 中的所有容器都设置了 CPU 和 Mem 的 requests 和 limits,且容器内的 requests==limits。
- 考虑 Pod 的优先级。
驱逐流程
-
Kubelet 启动 Eviction Manager 来管理 Pod 的驱逐。
-
Eviction Manager 会定时检查 Node 资源消耗情况,如内存、磁盘空间和 CPU。
-
如果 Node 资源消耗超过 Kubelet 配置的驱逐阈值,则会根据配置的策略和 Pod 的优先级、QoS 等来对要被驱逐的 Pod 进行排序。
-
对排序的 Pod 进行遍历,并尝试进行驱逐。如果一个 Pod 被驱逐成功,则此次驱逐流程结束。驱逐流程如下,
-
停止 Pod 中所有业务容器以及 Sandbox 容器。
-
将 pod.status.phase 值更新为 Failed,并附上驱逐 reason。
-
对于 Pod 由控制器控制的情况,如 ReplicationController、ReplicaSet、DaemonSet、StatefulSet 和 Job,它们将会作出响应,比如重新创建一个新的 Pod 来替代被驱逐的 Pod。
而对于不由控制器控制的 Pod 的话,则将直接调用 API Server,执行 DELETE 请求。
-
以内存为例,简单介绍一下 Pod 排序的过程:
OOM_ADJ 参数设置的越大,计算出来的 OOM 分数越高,表明该 Pod 的优先级就越低。当出现资源竞争时会越早被kill 掉,对于 OOM_ADJ 参数是 -999 则表示永远不会因为 OOM 被 kill 掉。
对于 Best-Effort 级别的 Pod,OOM_ADJ 参数设置为 1000;对于 Burstable 级别的 Pod,OOM_ADJ 参数取值从2到999;对于 Guaranteed 级别的 Pod,OOM_ADJ参数设置成了 -998;对于 kubelet、docker/containerd,OOM_ADJ 参数设置为 -999,表示不会被 OOM kill 掉。
源码解析
Kubelet 中负责压力驱逐的是 evictionManager,evictionManager 的启动代码位于如下位置,
Kubelet.Run()->Kubelet.updateRuntimeUp()->Kubelet.initializeRuntimeDependentModules()->Kubelet.evictionManager.Start(),也就是 managerImpl.Start()
Start() 中的代码如下所示,
- 拉起一个 goroutine,循环调用 m.synchronize() 方法执行驱逐逻辑。驱逐逻辑为:根据 kubelet 配置的驱逐策略,计算并判断是否符合驱逐条件,符合则根据一定的优先级来驱逐 Pod,然后返回被驱逐的pod。需要注意:每次调用 m.synchronize 方法最多只会驱逐一个pod。
- 如果被驱逐的 Pod 不为空,则调用 m.waitForPodsCleanup() 方法等待被驱逐的 Pod 删除成功。
- 如果没有 Pod 被驱逐,则 sleep monitoringInterval 之后再循环一次。monitoringInterval 默认为 10s。
// Start starts the control loop to observe and response to low compute resources.
func (m *managerImpl) Start(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc, podCleanedUpFunc PodCleanedUpFunc, monitoringInterval time.Duration) {
...
// start the eviction manager monitoring
go func() {
for {
if evictedPods := m.synchronize(diskInfoProvider, podFunc); evictedPods != nil {
m.waitForPodsCleanup(podCleanedUpFunc, evictedPods)
} else {
time.Sleep(monitoringInterval)
}
}
}()
}
其中,主要的是 synchronize() 方法,大概流程如下,
-
构建 Pod 的排序函数,返回软驱逐、硬驱逐中各个驱逐信号所对应的排序函数,排序函数用于后续计算被驱逐 Pod 的顺序。
同时构建节点资源回收函数,在后续驱逐 Pod 之前,先调用节点资源回收函数来回收资源,如果回收的资源足够,则不用进行驱逐。
-
获取会被驱逐的 Pod 列表---activePods。
-
获取节点上各个资源的总量以及使用情况、容器的资源声明及使用情况。
-
判断是否有达到阈值的资源情况。如果没有的话,则返回;如果有的话,则进行排序,并使用相应的资源回收函数回收资源。
-
如果回收资源之后,资源充足则返回;否则使用上述 Pod 的排序函数对 Pod 进行排序,再次得到 activePods。
-
之后遍历 activePods,调用 m.evictPod 方法进行驱逐。需要注意的是:最多只会驱逐一个 Pod,驱逐成功一个 Pod 则直接返回。
func (m *managerImpl) synchronize(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc) []*v1.Pod {
...
if m.dedicatedImageFs == nil {
...
m.signalToRankFunc = buildSignalToRankFunc(hasImageFs)
m.signalToNodeReclaimFuncs = buildSignalToNodeReclaimFuncs(m.imageGC, m.containerGC, hasImageFs)
}
activePods := podFunc()
updateStats := true
summary, err := m.summaryProvider.Get(ctx, updateStats)
// determine the set of thresholds met independent of grace period
thresholds = thresholdsMet(thresholds, observations, false)
sort.Sort(byEvictionPriority(thresholds))
thresholdToReclaim, resourceToReclaim, foundAny := getReclaimableThreshold(thresholds)
// rank the running pods for eviction for the specified resource
rank(activePods, statsFunc)
// we kill at most a single pod during each eviction interval
for i := range activePods {
...
if m.evictPod(pod, gracePeriodOverride, message, annotations, condition) {
return []*v1.Pod{pod}
}
}
return nil
}
m.evictPod 会调用 killPodFunc() 函数,最终调用的是 killPodNow() 中的函数,
- 这个函数是调用 podWorkers.UpdatePod() 方法来停止 Pod 中的所有业务容器以及 sandbox 容器。
- 将 Pod.Status.Phase 设置为 Failed,添加驱逐的原因。
func (m *managerImpl) evictPod(pod *v1.Pod, gracePeriodOverride int64, evictMsg string, annotations map[string]string, condition *v1.PodCondition) bool {
...
err := m.killPodFunc(pod, true, &gracePeriodOverride, func(status *v1.PodStatus) {
status.Phase = v1.PodFailed
status.Reason = Reason
status.Message = evictMsg
if condition != nil {
podutil.UpdatePodCondition(status, condition)
}
})
...
return true
}
func killPodNow(podWorkers PodWorkers, recorder record.EventRecorder) eviction.KillPodFunc {
return func(pod *v1.Pod, isEvicted bool, gracePeriodOverride *int64, statusFn func(*v1.PodStatus)) error {
...
ch := make(chan struct{}, 1)
podWorkers.UpdatePod(UpdatePodOptions{
Pod: pod,
UpdateType: kubetypes.SyncPodKill,
KillPodOptions: &KillPodOptions{
CompletedCh: ch,
Evict: isEvicted,
PodStatusFunc: statusFn,
PodTerminationGracePeriodSecondsOverride: gracePeriodOverride,
},
})
select {
case <-ch:
return nil
case <-time.After(timeoutDuration):
return fmt.Errorf("timeout waiting to kill pod")
}
}
}
更加详细的流程可参考源码,源码分析可参考:k8s 驱逐篇(3)-kubelet 节点压力驱逐-源码分析篇。
相关链接
k8s 驱逐篇(3)-kubelet 节点压力驱逐-源码分析篇:https://www.cnblogs.com/lianngkyle/p/16652129.html