当节点资源不足时,kubelet 会主动终止优先级较低的 Pod 以回收资源防止饥饿,kubernetes 将这个过程称为Eviction ,本文将结合源码探究它的运作机制。

分析基于:kubernetes-1.28.4

由于 Eviction 中有很多概念,为了方便理解驱逐的设计思路,在正式开始分析源码前我们先来了解下它们。

Eviction signals

驱逐信号,如果按字面意思的好像是在说驱逐后产生的信号,其实应该理解成输入给 Eviction 控制器的信号,用来告诉控制器应该关注哪些指标,当发现它们超门限后好采取行动,对于Linux系统共有6种信号指标。

Eviction Signal Description
memory.available memory.available := node.status.capacity[memory] - node.stats.memory.workingSet
nodefs.available nodefs.available := node.stats.fs.available
nodefs.inodesFree nodefs.inodesFree := node.stats.fs.inodesFree
imagefs.available imagefs.available := node.stats.runtime.imagefs.available
imagefs.inodesFree imagefs.inodesFree := node.stats.runtime.imagefs.inodesFree
pid.available pid.available := node.stats.rlimit.maxpid - node.stats.rlimit.curproc

分别表示节点内存磁盘磁盘 inode镜像磁盘镜像磁盘 inodePID 剩余情况,它们在代码中的定义

Eviction thresholds

驱逐阈值或门限,告诉驱逐控制器在满足什么条件下应该驱逐 Pod,其形式为[eviction-signal][operator][quantity]

  • eviction-signal 指标;
  • operator 运算符,用来判断是否达到阈值;
  • quantity 阈值,可以是具体数值,也可以是百分比的形式。

源码内部用 Threshold 表示。

根据是否有宽限期,驱逐门限分两种,Soft eviction thresholdsHard eviction thresholds

Soft eviction thresholds

指标达到阈值后并不会立即开始驱逐流程,而是根据配置设置一个宽限期,如果在宽限期指标降到阈值下则不进行驱逐,否则如果在宽限期到了后指标仍然没有降到阈值以下则会开始驱逐流程。

相关配置:

  • eviction-soft: 阈值

  • eviction-soft-grace-period: 宽限期,表示达到阈值后到开始驱逐动作之前的时间。

  • eviction-max-pod-grace-period: 满足驱逐条件需要 Kill Pod时等待Pod 优雅退出的时间。

    The maximum allowed grace period (in seconds) to use when terminating pods in response to a soft eviction threshold being met.

    官方对这个参数的命名和解释是有争议的,一眼没法弄清它使用和``的区别,有个与此相关的Issue,我个人第一看到这个参数和解释时也是一脸蒙逼,控制满足软驱逐条件到正式驱逐Pod的最大宽限时间?真到看源码我才弄明白它的作用。

Hard eviction thresholds

硬驱逐门限没有宽限期,指标达到门限后便会立即开始驱逐流程。相关配置:

  • memory.available
  • nodefs.available
  • imagefs.available
  • nodefs.inodesFree

除此之外,kubelet 还提供了其他的驱逐参数:

  • **eviction-pressure-transition-period:**驱逐等待时间。当节点出现资源饥饿时,节点需要等待一定的时间才一更新节点 Conditions ,然后才开启驱逐 Pod,默认为5分钟。该参数可以防止在某些情况下,节点在软驱逐条件上下振荡而出现错误的驱逐决策。
  • **eviction-minimum-reclaim:**表示每一次驱逐必须至少回收多少资源。该参数可以避免在某些情况下,驱逐 Pod 只会回收少量的资源,导致反复触发驱逐。

Threshold Notifier

什么是 Notifier 呢?

有了指标,有了门限,怎么知道指标是否超门限呢?

通常会用于一个后台进程不断轮询获取指标当前值然后和阈值比较判断是否超门限。

没错驱逐控制器就是这么干的,那除了被动获取外,能不能有一种机制能在指标超门限后主动通知控制器呢,没错,有的,这就是Threshold Notifier

怎么实现呢,背景知识:

cgroups 通知API允许用户空间应用程序接收有关 cgroup 状态变化的通知,但目前,通知API只支持监控内存溢出(OOM),关于通知API可以看下 Using the Notification API

kubenretes 依靠 Using the Notification API 能实现内存 Threshold Notifier,相关逻辑下边两个文件中:

对如何实现感兴越的话,可以自行看下这两个文件。

有了上边这些知识点后接下来我们进入主题,分析驱逐控制器是如何工作的?

synchronize

m.synchronizekubelet 驱逐 Pod 的核心方法,因此我们重点来看它。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// synchronize is the main control loop that enforces eviction thresholds.
// Returns the pod that was killed, or nil if no pod was killed.
func (m *managerImpl) synchronize(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc) []*v1.Pod {
	ctx := context.Background()
	// 各种指标阈值,由kubelet 启动时指定,包括soft 和 hard 门限
	// if we have nothing to do, just return
	thresholds := m.config.Thresholds
	if len(thresholds) == 0 && !m.localStorageCapacityIsolation {
		return nil
	}

	klog.V(3).InfoS("Eviction manager: synchronize housekeeping")
	// build the ranking functions (if not yet known)
	// TODO: have a function in cadvisor that lets us know if global housekeeping has completed
	if m.dedicatedImageFs == nil {  // 是否有独立 imageFs 磁盘
		hasImageFs, ok := diskInfoProvider.HasDedicatedImageFs(ctx)
		if ok != nil {
			return nil
		}
		m.dedicatedImageFs = &hasImageFs
		// 准备当超门限时,signals 需要给 Pod 排序的函数, 先驱逐谁后驱逐谁就是这么来的
		m.signalToRankFunc = buildSignalToRankFunc(hasImageFs)
		// 为支持回收的资源(主要是磁盘资源)关联回收函数,用于后边需要回收资源时调用
		m.signalToNodeReclaimFuncs = buildSignalToNodeReclaimFuncs(m.imageGC, m.containerGC, hasImageFs)
	}

	// 获取节点上当前 active 的 Pod
	activePods := podFunc()
	updateStats := true

	// 统计节点各种资源使用情况,默认来源 cadvisor
	summary, err := m.summaryProvider.Get(ctx, updateStats)
	if err != nil {
		klog.ErrorS(err, "Eviction manager: failed to get summary stats")
		return nil
	}

	// notifierRefreshInterval memcg 通知间隔时间,为10s,避免频繁
	if m.clock.Since(m.thresholdsLastUpdated) > notifierRefreshInterval {
		m.thresholdsLastUpdated = m.clock.Now()
		for _, notifier := range m.thresholdNotifiers {
			//  使用 Cgroups Notification API 更新内存资源使用情况,实时性会高一些
			if err := notifier.UpdateThreshold(summary); err != nil {
				klog.InfoS("Eviction manager: failed to update notifier", "notifier", notifier.Description(), "err", err)
			}
		}
	}

	// 根据上边获取到的各种资源使用情况组装成 signalObservations 方便后边使用
	// statsFunc 主要是用来返回指定 Pod 的资源使用情况
	// make observations and get a function to derive pod usage stats relative to those observations.
	observations, statsFunc := makeSignalObservations(summary)
	debugLogObservations("observations", observations)

	// 将观察到的资源使用情况同阈值比较找出超门限的
	// determine the set of thresholds met independent of grace period
	thresholds = thresholdsMet(thresholds, observations, false)
	debugLogThresholdsWithObservation("thresholds - ignoring grace period", thresholds, observations)

	// determine the set of thresholds previously met that have not yet satisfied the associated min-reclaim
	if len(m.thresholdsMet) > 0 { // 对于上一次 loop 时筛选出来的超限资源,这次还要考虑 --eviction-minimum-reclaim
		thresholdsNotYetResolved := thresholdsMet(m.thresholdsMet, observations, true)
		thresholds = mergeThresholds(thresholds, thresholdsNotYetResolved) // 没达到最小回收要求的也加入超限列表
	}
	debugLogThresholdsWithObservation("thresholds - reclaim not satisfied", thresholds, observations)

	// track when a threshold was first observed
	now := m.clock.Now()
	// 确定已经超限列表中的 thresholds 它们的首次观察到超限开始时间
	thresholdsFirstObservedAt := thresholdsFirstObservedAt(thresholds, m.thresholdsFirstObservedAt, now)

	// 根据当前超限情况准备 node conditions ,用于更新 node status
	// the set of node conditions that are triggered by currently observed thresholds
	nodeConditions := nodeConditions(thresholds)
	if len(nodeConditions) > 0 {
		klog.V(3).InfoS("Eviction manager: node conditions - observed", "nodeCondition", nodeConditions)
	}

	// 确定各 NodeConditionType 上一次观察的时间,目的用于下边判断是否满足 config.PressureTransitionPeriod
	// track when a node condition was last observed
	nodeConditionsLastObservedAt := nodeConditionsLastObservedAt(nodeConditions, m.nodeConditionsLastObservedAt, now)

	// m.config.PressureTransitionPeriod 为 --eviction-pressure-transition-period 指定的值,
	// 该标志控制kubelet在将节点条件转换为不同状态之前必须等待的时间,避免节点 condition 震荡
	// node conditions report true if it has been observed within the transition period window
	nodeConditions = nodeConditionsObservedSince(nodeConditionsLastObservedAt, m.config.PressureTransitionPeriod, now)
	if len(nodeConditions) > 0 {
		klog.V(3).InfoS("Eviction manager: node conditions - transition period not met", "nodeCondition", nodeConditions)
	}

	// 筛选出超限且持续了 grace periods 的 thresholds,对于软驱逐由 --eviction-soft-grace-period 指定,硬驱逐为0
	// determine the set of thresholds we need to drive eviction behavior (i.e. all grace periods are met)
	thresholds = thresholdsMetGracePeriod(thresholdsFirstObservedAt, now)
	debugLogThresholdsWithObservation("thresholds - grace periods satisfied", thresholds, observations)

	// update internal state
	m.Lock()
	m.nodeConditions = nodeConditions
	m.thresholdsFirstObservedAt = thresholdsFirstObservedAt
	m.nodeConditionsLastObservedAt = nodeConditionsLastObservedAt
	m.thresholdsMet = thresholds

	// 由于各种资源的使用情况并不是实事更新的,也是定时轮训获取的,所以可能会出现上次 loop 已经驱逐过一个 Pod,但是这
	// 次 loop 由于时资源统计还没更新,观察判断仍然超限,这样是不准确的,因为忽略掉。
	// thresholdsUpdatedStats 的作用就是去除掉这些资源状态末刷新的超限。
	// determine the set of thresholds whose stats have been updated since the last sync
	thresholds = thresholdsUpdatedStats(thresholds, observations, m.lastObservations)
	debugLogThresholdsWithObservation("thresholds - updated stats", thresholds, observations)

	m.lastObservations = observations  // 将本次观察到的超限列表存起来,用于下一次运行时比较
	m.Unlock()

	// 本地临时存储容量隔离 特性
	// evict pods if there is a resource usage violation from local volume temporary storage
	// If eviction happens in localStorageEviction function, skip the rest of eviction action
	if m.localStorageCapacityIsolation {
		if evictedPods := m.localStorageEviction(activePods, statsFunc); len(evictedPods) > 0 {
			return evictedPods
		}
	}

	if len(thresholds) == 0 {
		klog.V(3).InfoS("Eviction manager: no resources are starved")
		return nil
	}

	// Signals 背后对应内存,磁盘,inode,pids 各种资源,它们分可压缩资源和不可压缩资源,可压缩资源是可以回收的,
	// 所以需要给它们排个序,后边会按这个顺序回收资源,内存会排在其它资源前。
	// 为什么内存会排在最前边呢,明明它是不可回收资源啊,下边又最先选择它回收,乍一看矛盾了。
	// 其实这里逻辑比较巧妙,正因为内存是不可压缩资源,当它超限后,应该赶紧驱逐,相反如果把其它可压缩的资源放在前边
	// 先回收它们骚操作一通内存可是一点也没减,耽误了时间可正事是一点也没干。
	// 它这样把内存放到最前边,表面是资源回收是先走它,但是因为它不支持回收,没有回收函数,会很快略过,
	// 逻辑值得学习。
	// rank the thresholds by eviction priority
	sort.Sort(byEvictionPriority(thresholds))
	// 排好序后筛选出一种指标资源准备回收
	thresholdToReclaim, resourceToReclaim, foundAny := getReclaimableThreshold(thresholds)
	if !foundAny {
		return nil
	}
	klog.InfoS("Eviction manager: attempting to reclaim", "resourceName", resourceToReclaim)

	// 事件记录
	// record an event about the resources we are now attempting to reclaim via eviction
	m.recorder.Eventf(m.nodeRef, v1.EventTypeWarning, "EvictionThresholdMet", "Attempting to reclaim %s", resourceToReclaim)

	// 开始回收节点资源,如果回收完资源降到门限以下,则函数直接返回,表示资源已经不再饥饿,已经不需要驱逐了
	// check if there are node-level resources we can reclaim to reduce pressure before evicting end-user pods.
	if m.reclaimNodeLevelResources(ctx, thresholdToReclaim.Signal, resourceToReclaim) {
		klog.InfoS("Eviction manager: able to reduce resource pressure without evicting pods.", "resourceName", resourceToReclaim)
		return nil
	}

	klog.InfoS("Eviction manager: must evict pod(s) to reclaim", "resourceName", resourceToReclaim)

	// 回收完仍然不能避免饥饿,先准备驱逐了。
	// 这里准备给回收过仍不能避免饥饿的资源选取 Pod 的排序函数
	// rank the pods for eviction
	rank, ok := m.signalToRankFunc[thresholdToReclaim.Signal]
	if !ok {
		klog.ErrorS(nil, "Eviction manager: no ranking function for signal", "threshold", thresholdToReclaim.Signal)
		return nil
	}

	// 没有可供驱逐的 Pod 直接返回进入下一次轮询
	// the only candidates viable for eviction are those pods that had anything running.
	if len(activePods) == 0 {
		klog.ErrorS(nil, "Eviction manager: eviction thresholds have been met, but no pods are active to evict")
		return nil
	}

	//  给 Pod 排序,后边按这个顺序遍历选择 Pod 驱逐
	// rank the running pods for eviction for the specified resource
	rank(activePods, statsFunc)

	klog.InfoS("Eviction manager: pods ranked for eviction", "pods", klog.KObjSlice(activePods))

	//record age of metrics for met thresholds that we are using for evictions.
	for _, t := range thresholds {
		timeObserved := observations[t.Signal].time
		if !timeObserved.IsZero() {
			metrics.EvictionStatsAge.WithLabelValues(string(t.Signal)).Observe(metrics.SinceInSeconds(timeObserved.Time))
		}
	}

	// we kill at most a single pod during each eviction interval
	for i := range activePods {
		pod := activePods[i]
		// 硬驱逐 优雅退出为0
		gracePeriodOverride := int64(0)
		if !isHardEvictionThreshold(thresholdToReclaim) {
			// 软驱逐 则会使用 eviction-max-pod-grace-period 配置的值,默认为 0
			gracePeriodOverride = m.config.MaxPodGracePeriodSeconds
		}
		// 准备 Pod Condition
		message, annotations := evictionMessage(resourceToReclaim, pod, statsFunc, thresholds, observations)
		var condition *v1.PodCondition
		if utilfeature.DefaultFeatureGate.Enabled(features.PodDisruptionConditions) {
			condition = &v1.PodCondition{
				Type:    v1.DisruptionTarget,
				Status:  v1.ConditionTrue,
				Reason:  v1.PodReasonTerminationByKubelet,
				Message: message,
			}
		}
		// 驱逐 Pod,如果 Pod 成功驱逐则 return 返回,进入下一轮询过程,即每次 loop 最多只驱逐一个 Pod
		// 为什么每次只驱逐一个 Pod 呢?背后的考量让人暖心,做到尽可能的少 kill, 虽然每次驱逐一个 Pod 可能资源降不到门限
		// 以下,大不了再 loop 一次。还有就是每个超限的 threshold 都是独立的,内存我只看内存超限,但一个Pod占用的资源并不是单一的,
		// 内存,磁盘都会占用,当我因为内存超门限驱逐它以后,硬盘占用也会减少,这时应该重新 observation 资源使用情况,而不能静态的使
		// 用已经 observation 过的资源使用情况。总结就是: 驱逐 -> 观察 -> 驱逐 -> ...
		if m.evictPod(pod, gracePeriodOverride, message, annotations, condition) {
			metrics.Evictions.WithLabelValues(string(thresholdToReclaim.Signal)).Inc()
			return []*v1.Pod{pod}
		}
	}
	klog.InfoS("Eviction manager: unable to evict any pods from the node")
	return nil
}

函数主要逻辑:

  • 准备各种Eviction Signal 的阈值,后边会用到,根据它们来判断是否超门限,它们从哪获取呢?由kubelet 启动时通过启动参数或你配置文件指定;

  • 准备给 Pod 排序的函数,需要考虑 imagFs 是否是独立磁盘的情况。什么作用呢?当某个资源指标超门限后,我怎么知道要先驱哪个 Pod,后驱逐哪个 Pod。每种 Signal 都有自己的排序函数,具体看buildSignalToRankFunc

  • 硬盘等能压缩的资源支持回收,为它们准备回收函数,后回收资源时会用到,同样需要考虑 imagFs 是否是独立磁盘的情况。

  • 获取节点上当前 active 的 Pod 列表,后边会从它们中最多选取一个驱逐;

  • 使用 cadvisor 采集的数据汇总各种资源统计信息 ;

  • 使用 Cgroups Notification API 更新内存资源统计信息;

  • 根据上边获取到的各资源统计信息组装 signalObservations 方便后边会用到;

  • thresholdsMet 将获取到的资源统计信息同阈值比较筛选出超门限的,得到 Threshold 列表;

  • 对于上一次轮训时超标记为超限资源,本次还要考虑--eviction-minimum-reclaim 配置的最小回收情况。

  • 确定超门限 Threshold 列表它们第一次超门限的时间,用于后边判断宽限期;

  • 根据当前超限情况准备 node conditions ,用于更新 node status;

  • 确定各 NodeConditionType 上一次观察的时间,目的用于下边判断是否满足 config.PressureTransitionPeriod;

  • 判断节点 Condition 是否满足等待时间,避免 Condition 状态震荡;

  • 筛选出超限且持续了 grace periods 的 thresholds,对于软驱逐由 –eviction-soft-grace-period 指定,硬驱逐为0;

  • 保存 nodeConditions 、thresholdsFirstObservedAt 、nodeConditionsLastObservedAt、thresholds

  • 由于各种资源的统计也不是实事更新的,也是定时轮训获取的,所以可能会出现上次轮询时已经驱逐过一个 Pod,但是本次轮询由于资源统计还没更新,观察判断仍然超限,这样是不准确的,需要忽略掉。thresholdsUpdatedStats 的作用就是去除掉这些资源状态末刷新的超限。

  • 本地临时存储容量隔离特性

  • Signals 背后对应内存,磁盘,inode,pids 各种资源,它们分可压缩资源和不可压缩资源,可压缩资源是可以回收的,所以需要按是否支持回收给它们排个序,后边按这个顺序回收资源,内存资源会排在其它资源前。为什么内存会排在最前边呢,明明它是不可回收资源啊?不支持回收反而在最前边,乍一看矛盾了。其实这里逻辑比较巧妙,正因为内存是不可压缩资源,回收对它没啥效果,所以当它发生饥饿时应该立即开始驱逐。相反如果把其它可压缩的资源放在前边,先回收它们会出现骚操作一通内存可是一点也没减的情况。它这样把内存放到最前边,表面看是资源回收时先走它,但是因为它不支持回收,没有回收函数,会快速略过;

  • getReclaimableThreshold 筛选出准备回收资源的 Threshold

  • reclaimNodeLevelResources 回收节点资源

  • 准备给 Pod 排序的函数;

  • 如果 activePods 为空,没有可供驱逐的 Pod 则直接返回进入下一次轮询;

  • 给 activePods 排序;

  • 遍历 activePods 开始驱逐 Pod,如果 Pod 成功驱逐则 return 返回,进入下一轮询过程,即每次轮询最多只驱逐一个 Pod。

    为什么每次只驱逐一个 Pod 呢?背后的考量让人暖心,做到尽可能的少 kill, 虽然每次驱逐一个 Pod 可能资源降不到门限以下,大不了再 loop 一次。还有就是每个超限的 threshold 都是独立的,内存我只看内存超限,但一个Pod占用的资源并不是单一的,内存,磁盘都会占用,当我因为内存超门限驱逐它以后,硬盘占用也会减少,这时应该重新 observation 资源使用情况,而不能静态的使用已经 observation 过的资源使用情况。总结就是: 驱逐 -> 观察 -> 驱逐 -> …

k8s-kubelet-eviction

Pod 选择排序

每种资源都有对应的排序方法,它们的定义在 buildSignalToRankFunc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// buildSignalToRankFunc returns ranking functions associated with resources
func buildSignalToRankFunc(withImageFs bool) map[evictionapi.Signal]rankFunc {
	signalToRankFunc := map[evictionapi.Signal]rankFunc{
		evictionapi.SignalMemoryAvailable:            rankMemoryPressure,
		evictionapi.SignalAllocatableMemoryAvailable: rankMemoryPressure,
		evictionapi.SignalPIDAvailable:               rankPIDPressure,
	}
	// usage of an imagefs is optional
	if withImageFs {
		// with an imagefs, nodefs pod rank func for eviction only includes logs and local volumes
		signalToRankFunc[evictionapi.SignalNodeFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)
		signalToRankFunc[evictionapi.SignalNodeFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)
		// with an imagefs, imagefs pod rank func for eviction only includes rootfs
		signalToRankFunc[evictionapi.SignalImageFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot}, v1.ResourceEphemeralStorage)
		signalToRankFunc[evictionapi.SignalImageFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot}, resourceInodes)
	} else {
		// without an imagefs, nodefs pod rank func for eviction looks at all fs stats.
		// since imagefs and nodefs share a common device, they share common ranking functions.
		signalToRankFunc[evictionapi.SignalNodeFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)
		signalToRankFunc[evictionapi.SignalNodeFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)
		signalToRankFunc[evictionapi.SignalImageFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)
		signalToRankFunc[evictionapi.SignalImageFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)
	}
	return signalToRankFunc
}

具体按什么来排序的呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// rankMemoryPressure orders the input pods for eviction in response to memory pressure.
// It ranks by whether or not the pod's usage exceeds its requests, then by priority, and
// finally by memory usage above requests.
func rankMemoryPressure(pods []*v1.Pod, stats statsFunc) {
	orderedBy(exceedMemoryRequests(stats), priority, memory(stats)).Sort(pods)
}

// rankPIDPressure orders the input pods by priority in response to PID pressure.
func rankPIDPressure(pods []*v1.Pod, stats statsFunc) {
	orderedBy(priority, process(stats)).Sort(pods)
}

// rankDiskPressureFunc returns a rankFunc that measures the specified fs stats.
func rankDiskPressureFunc(fsStatsToMeasure []fsStatsType, diskResource v1.ResourceName) rankFunc {
	return func(pods []*v1.Pod, stats statsFunc) {
		orderedBy(exceedDiskRequests(stats, fsStatsToMeasure, diskResource), priority, disk(stats, fsStatsToMeasure, diskResource)).Sort(pods)
	}
}

rankMemoryPressurerankPIDPressurerankDiskPressureFunc 分别对应内存、PID、硬盘,当对应资源发生饥饿时就会使用对应排序函数选择 Pod Kill。

总结

  • 软驱逐会等待一个宽限期,如果在宽限期内资源使用降到门限以下,资源不再饥饿则不驱逐;驱逐时会等待 Pod 优雅退出,其时间为 eviction-max-pod-grace-period 为配置的值,其实际是覆盖了 pod.Spec.TerminationGracePeriodSeconds
  • 硬驱逐没有宽限期,且Kill Pod 不会等待 Pod 优雅退出,即 grace Period 为0;
  • 驱逐前会回收资源,如果资源仍然饥饿则会开始驱逐 Pod;
  • 对于本此超门限的资源,下次运行判断是否超门限时还会把 Minimum eviction reclaim 最小回收配置考虑进去;
  • 为了避免 Node conditions 状态震荡,节前资源饥饿状态转变前会等待一个 eviction-pressure-transition-period, 默认为 5 分钟。
  • 每种饥饿资源都有对应的选取 Pod 的排序函数,对应关系见 buildSignalToRankFunc

参考

https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/

https://www.cnblogs.com/lianngkyle/p/16652129.html

https://renhongcai.gitbook.io/kubernetes/di-shi-liu-zhang-api-she-ji-yue-ding/1.2-api_convention_condition

https://cloud.tencent.com/developer/article/1690175

https://kubernetes.io/blog/2022/09/19/local-storage-capacity-isolation-ga/

https://sysdig.com/blog/kubernetes-pod-evicted/

https://www.jianshu.com/p/f2403e33c766

https://github.com/kubernetes/kubernetes/issues/64530

https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/