kubelet驱逐机制的源码分析

1 概述:

1.1 源码环境

版本信息如下:
a、kubernetes集群:v1.15.0

1.2 维持节点稳定的两道防线

对于linux操作系统,CPU时间片、内存、磁盘容量和inode、PID等都是系统资源。

系统资源的一种分类:
a、可压缩资源(CPU)
b、不可压缩资源(内存、磁盘容量和inode、PID)

kubelet作为节点代理,自然而然需要一定机制保证服务器资源不会被耗尽。当可压缩资源不够时,Pod(容器或进程)会饥饿,但不会被操作系统和kubelet进行强制退出。当不可压缩资源(例如内存)不够用时,kubelet层面可提前回收一些服务器资源(删除用不上的镜像、清理退出的容器和失败的Pod、杀死正在运行的Pod),而linux操作系统的OOM KILLER作为最后一道防线,也有机会出面直接杀死用户态进程。

kubelet杀死正在运行的Pod,这个过程称为驱逐。kubelet删除用不上的镜像、清理退出的容器和失败的Pod,这个过程称为回收节点级别资源。

因此,有两道防线来维持节点的稳定性,第一道防线是用户进程kubelet,第二道防线是Linux OOM KILLER。


1.3 kubelet维持节点稳定性的机制

不可压缩资源(内存、磁盘容量和inode、PID)不够使用时,会严重影响节点的稳定性。为此,kubelet作为linux用户态进程和k8s集群的节点代理,应该有一定的机制来维持节点的稳定性,本质是通过删除镜像和停止容器进程来达到目的。


1.3.1 资源阈值,用户说了算

那kubelet需要定期获取节点资源的整体信息,这是必然的。
那服务器还剩余多少资源,才算不可压缩资源是"不够"的?这种"不够"的依据,是用户在kubelet的配置文件中指定的。
kubelet只要获取节点资源的容量、实际使用量(率)和用户配置的阈值,觉得"条件满足了"即可开始执行删除镜像和停止容器进程的操作。


1.3.2 软硬驱逐

“条件满足了”,等待零秒就开始执行删除镜像和停止容器进程的操作,这就是硬驱逐。
“条件满足了”,等待N(N > 0)秒就开始执行删除镜像和停止容器进程的操作,这就是软驱逐。数字N,可通过kubelet的参数–eviction-soft-grace-period来设定。另外,对于软驱逐,–eviction-max-pod-grace-period参数来影响驱逐管理器等待kubelet清理Pod的时间(真正等待的时间是eviction-max-pod-grace-period+ ( eviction-max-pod-grace-period/ 2 ) )。


1.3.3 阻塞Pod的创建

当节点的不可压缩资源处于"不够"时,kubelet也一定有机制阻塞Pod的创建,这和驱逐管理器的Admit(…)方法有关。

1.3.4 影响Linux OOM KILLER

Kubelet作为维持服务器稳定的第一道防线,如果搞不定,那么必然由第二道防线Linux OOM KILLER来兜底。第一道防线和第二道防线之间的关联就是oom_score_adj。
pod的Qos不同,kubelet为其配置oom_score_adj的分数是不一样的,而进程的oom_score_adj是影响Linux OOM KILLER杀死用户态进程的一个因素。

在这里插入图片描述

其他说明:
1)查看Pod中的容器的进程的oom_score_adj分数的命令模板:
kubectl exec demo-qos-pod cat /proc/1/oom_score_adj


1.4 Linux OOM KILLER概述

1)Linux对大部分申请内存的请求都回复"yes",以便能跑更多更大的程序。因为申请内存后,并不会马上使用内存。这种技术叫做Overcommit。

2)当linux发现内存不足时,会发生OOM killer(OOM=out-of-memory),此时它会选择杀死一些进程(用户态进程,不是内核线程),以便释放内存。

3)当oom-killer发生时,linux会选择杀死哪些进程?linux会计算每个进程的点数(0~1000)。点数越高,这个进程越有可能被杀死。
oom点数的公式:

进程OOM点数 = oom_score + oom_score_adj 

#oom_score和进程消耗的内存有关。
#oom_score_adj是可以配置的,取值范围:-1000最低,1000最高。

4)oom的日志在/var/log/messages文件中,属于内核日志,可通过grep kernel /var/log/messages命令来查看内核日志。
在这里插入图片描述


2 kubelet驱逐源码分析:

2.1 关键方法

																				|-> kl.cadvisor.Start()  // 启动cadvisor,cadvisor能获取节点信息和容器信息
																				|
		  //启动							//初始化额外模块,只会运行一次		        |
Kubelet -> Run() -> updateRuntimeUp()  ->  initializeRuntimeDependentModules -->|-> kl.containerManager.Start()  //containerManager需要cAdvisor提供的文件系统信息
																				|
																				|
																				|	//evictionManager需要通过cadvisor得知容器运行时是否具备一个独立专用的imagefs
																				|
																				|-> kl.evictionManager.Start() -> 定时任务 ->| -> m.synchronize() -> | -> m.reclaimNodeLevelResources(...)  //清理节点级别的资源(镜像、已退出的容器)
																															|					    |
																															| 	                    | -> m.evictPod(...)  //清理pod对象
																															|
																															| -> m.waitForPodsCleanup(...)   //如果m.synchronize()驱逐了pod,执行本方法进行等待pod被清理.

2.2 支持的能触发驱逐Pod的信号

内存、磁盘容量、磁盘inode数量、pid数量,这些资源不够时,可触发kubelet清理镜像和容器,以达到回收节点资源的目的。

// Signal defines a signal that can trigger eviction of pods on a node.
type Signal string

/*
分类:
1)内存
2)磁盘容量
3)磁盘inode数量
4)pid数量
*/
const (
	// SignalMemoryAvailable is memory available (i.e. capacity - workingSet), in bytes.
	SignalMemoryAvailable Signal = "memory.available"
	
	// SignalNodeFsAvailable is amount of storage available on filesystem that kubelet uses for volumes, daemon logs, etc.
	// nodefs是kubelet使用的卷、进程日志所在的文件系统
	SignalNodeFsAvailable Signal = "nodefs.available"
	
	// SignalNodeFsInodesFree is amount of inodes available on filesystem that kubelet uses for volumes, daemon logs, etc.
	SignalNodeFsInodesFree Signal = "nodefs.inodesFree"
	
	// SignalImageFsAvailable is amount of storage available on filesystem that container runtime uses for storing images and container writable layers.
	// imagefs是容器运行时(例如docker)的镜像层、读写层所在的文件系统
	SignalImageFsAvailable Signal = "imagefs.available"
	
	// SignalImageFsInodesFree is amount of inodes available on filesystem that container runtime uses for storing images and container writable layers.
	SignalImageFsInodesFree Signal = "imagefs.inodesFree"
	
	// SignalAllocatableMemoryAvailable is amount of memory available for pod allocation (i.e. allocatable - workingSet (of pods), in bytes.
	SignalAllocatableMemoryAvailable Signal = "allocatableMemory.available"
	
	// SignalPIDAvailable is amount of PID available for pod allocation
	SignalPIDAvailable Signal = "pid.available"
)

2.3 整体代码

//kubelet的启动入口
func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
	/*
		其他代码
	*/
	
	go wait.Until(kl.updateRuntimeUp, 5*time.Second, wait.NeverStop)
	
}


// 当容器运行时第一次运行,初始化一些它依赖的模块
func (kl *Kubelet) updateRuntimeUp() {
	/*
		其他代码
	*/
	
	kl.oneTimeInitializer.Do(kl.initializeRuntimeDependentModules)

}

//启动cadvisor、containerManager和本文的核心研究对象evictionManager
func (kl *Kubelet) initializeRuntimeDependentModules() {

	// 启动cadvisor
	if err := kl.cadvisor.Start(); err != nil {		
	}
	
	/*
		其他代码
	*/	
	
	// containerManager必须在cAdvisor之后启动,因为它需要cAdvisor提供的文件系统信息
	if err := kl.containerManager.Start(node, kl.GetActivePods, kl.sourcesReady, kl.statusManager, kl.runtimeService); err != nil {		
	}
	
	//evictionManager是本文的核心
	// evictionManager必须在cAdvisor之后启动,因为它需要知道容器运行时是否具备一个独立专用的imagefs
	kl.evictionManager.Start(kl.StatsProvider, kl.GetActivePods, kl.podResourcesAreReclaimed, evictionMonitoringPeriod)
	
	/*
		其他代码
	*/	
}

驱逐管理器的结构体的主要属性

// managerImpl implements Manager
type managerImpl struct {
	
	//kubelet的配置文件中和驱逐相关的字段会赋值到本属性
	config Config
	
	//杀死Pod的方法
	//--eviction-max-pod-grace-period参数和此方法相关
	killPodFunc KillPodFunc
	
	
	// the interface that knows how to do image gc
	imageGC ImageGC
	// the interface that knows how to do container gc
	containerGC ContainerGC
	// protects access to internal state
	
	// 达到阈值的不可压缩资源,就会出现在本属性
	thresholdsMet []evictionapi.Threshold
	
	// 对Pod进行排序的方法保存在此属性
	signalToRankFunc map[evictionapi.Signal]rankFunc
	// signalToNodeReclaimFuncs maps a resource to an ordered list of functions that know how to reclaim that resource.
	
	thresholdNotifiers []ThresholdNotifier

}	

2.4 驱逐管理器的启动方法

func (m *managerImpl) Start(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc, podCleanedUpFunc PodCleanedUpFunc, monitoringInterval time.Duration) {
	/*
	其他代码
	*/
	
	//通过for循环和睡眠来定时执行m.synchronize()方法
	go func() {
		for {
		    //m.synchronize()方法内部有清理 节点资源 和 Pod 的逻辑
			if evictedPods := m.synchronize(diskInfoProvider, podFunc); evictedPods != nil {
				m.waitForPodsCleanup(podCleanedUpFunc, evictedPods)
			} else {
			    //睡眠
				time.Sleep(monitoringInterval)
			}
		}
	}()
}

2.4 驱逐管理器回收资源的主逻辑


func (m *managerImpl) synchronize(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc) []*v1.Pod {
		
	/*
		填充m.signalToRankFunc的值,也就是pod排序的排序函数,整个过程只执行一次;
	*/
	
	// 获取节点上运行中的pod
	activePods := podFunc()
	// 从cadvisor获取节点的详细信息,并进行统计
	summary, err := m.summaryProvider.Get(true)
	//再从统计数据中获得节点资源的使用情况observations
	observations, statsFunc := makeSignalObservations(summary)
	
	// 将资源实际使用量和资源容量进行比较,最终得到阈值结构体对象的列表
	thresholds = thresholdsMet(thresholds, observations, false)	
	thresholdsFirstObservedAt := thresholdsFirstObservedAt(thresholds, m.thresholdsFirstObservedAt, now)
	//此时的thresholds是即将触发回收资源的thresholds,因为这些thresholds已经过了平滑时间
	thresholds = thresholdsMetGracePeriod(thresholdsFirstObservedAt, now)

		
	/*
		获取nodeConditions;
		nodeConditions是一个字符串切片;
		打印一些和nodeConditions相关的日志信息;	
	*/
	
	/*
		更新驱逐管理器(即本方法的接收者)的一些成员变量的值
		
		m.thresholdsMet = thresholds
		
		//m.nodeConditions过了--eviction-pressure-transition-period指定的时间,也会更新到kubelet本地的node对象中,而node对象最终也会被kubelet同步到kube-apiserver中
		//m.nodeConditions也和m.Admit(...)方法相关,m.Admit(...)方法决定是否允许在当前节点上创建或运行目标pod
		m.nodeConditions = nodeConditions
		
	*/
	
	// 本地临时存储导致的驱逐(一定是驱逐pod),如果发生,后续的回收资源(清理节点资源和运行中的pod)的操作不会执行,直接返回
	if utilfeature.DefaultFeatureGate.Enabled(features.LocalStorageCapacityIsolation) {
		if evictedPods := m.localStorageEviction(summary, activePods); len(evictedPods) > 0 {
			return evictedPods
		}
	}
	
	// 节点资源使用量没有到达用户配置的阈值,不需要回收节点资源,因此直接返回
	if len(thresholds) == 0 {
		klog.V(3).Infof("eviction manager: no resources are starved")
		return nil
	}
	
	// 回收节点级的资源,如果回收的资源足够的话,直接返回,不需要驱逐正在运行中的pod
	if m.reclaimNodeLevelResources(thresholdToReclaim.Signal, resourceToReclaim) {
		klog.Infof("eviction manager: able to reduce %v pressure without evicting pods.", resourceToReclaim)
		return nil
	}

	// 根据资源类型,从一个map中拿到一个排序方法,使用该排序方法对运行中的pod进行排序
	rank, ok := m.signalToRankFunc[thresholdToReclaim.Signal]	
	rank(activePods, statsFunc)
	// 此时activePods已经按照一定规则进行排序
	klog.Infof("eviction manager: pods ranked for eviction: %s", format.Pods(activePods))
	
	// 开始对运行中的pod实施驱逐操作
	for i := range activePods {	
		//如果驱逐了一个pod,则立马返回,因为一个周期最多驱逐一个pod
		if m.evictPod(pod, gracePeriodOverride, message, annotations) {
			return []*v1.Pod{pod}
		}
	}
	
	// 到达此处,说明本周期试图驱逐pod,但连一个pod都没驱逐成功
	klog.Infof("eviction manager: unable to evict any pods from the node")
	return nil
}

2.4 驱逐管理器的signalToRankFunc属性

它保存着对Pod进行排序的一组方法。

func (m *managerImpl) synchronize(diskInfoProvider DiskInfoProvider, podFunc ActivePodsFunc) []*v1.Pod {
	/*
	其他代码
	*/
	
	if m.dedicatedImageFs == nil {
		hasImageFs, ok := diskInfoProvider.HasDedicatedImageFs()
		if ok != nil {
			return nil
		}
		//dedicatedImageFs被赋值后,不会再进入当前整个if语句
		m.dedicatedImageFs = &hasImageFs
		//把pod的排序方法作为map传给驱逐管理器的signalToRankFunc属性
		m.signalToRankFunc = buildSignalToRankFunc(hasImageFs)   
	}
	
	/*
	其他代码
	*/
}
func buildSignalToRankFunc(withImageFs bool) map[evictionapi.Signal]rankFunc {

	//不管入参是true还是false,内存、pid这两种资源的排序方法是不变的
	signalToRankFunc := map[evictionapi.Signal]rankFunc{
		evictionapi.SignalMemoryAvailable:            rankMemoryPressure,
		evictionapi.SignalAllocatableMemoryAvailable: rankMemoryPressure,
		evictionapi.SignalPIDAvailable:               rankPIDPressure,
	}
	
	//虽然都是rankDiskPressureFunc,但方法的入参不同。
	if withImageFs {
		
		// 使用独立的imagefs,那么nodefs的排序方法只包含日志和本地卷
		signalToRankFunc[evictionapi.SignalNodeFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsLogs, fsStatsLocalVolumeSource}, v1.ResourceEphemeralStorage)
		signalToRankFunc[evictionapi.SignalNodeFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsLogs, fsStatsLocalVolumeSource}, resourceInodes)
			
		// 使用独立的imagefs,那么imagefs的排序方法只包含rootfs
		signalToRankFunc[evictionapi.SignalImageFsAvailable] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot}, v1.ResourceEphemeralStorage)
		signalToRankFunc[evictionapi.SignalImageFsInodesFree] = rankDiskPressureFunc([]fsStatsType{fsStatsRoot}, resourceInodes)
	} else {	
		// 不使用独立的imagefs,nodefs的排序方法是包含所有文件系统,即fsStatsRoot, fsStatsLogs, fsStatsLocalVolumeSource
		// 此时imagefs和nodefs在使用同一个块设备,因此它们的排序方法都是一样的。
		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
}

2.4 对Pod排序的方法

驱逐管理器驱逐Pod的时候,会对正在运行的Pod进行排序,排在队头的Pod就会优先被清理。

Qos为Guaranteed的Pod在队列中是最靠后的,因为在正常运行的时候,使用量和request的差值一定是负数,当然在队列中排在靠后的位置。这种类型的Pod的资源使用量达到request的时候,也就是达到limit的时候,linux检测到并对比limit设定的值(放在cgroup文件中),会给Pod中的进程发送杀死信号,此时就不是正在运行中的Pod。

而Qos为Best-Effort的Pod是最靠前的,因为request是没有设置的,那么使用量和request的差值一定是正数,因此一定是队列的最前面部分。


2.4.1 内存紧张时,对Pod的排序方法

/*
当内存使用量达到阈值时,对入参pods进行排序。
首先看使用量是否超过pod的request字段
接着看pod的QOS类型
最后看使用量和request之间的差值
*/

func rankMemoryPressure(pods []*v1.Pod, stats statsFunc) {

	orderedBy(
		exceedMemoryRequests(stats),  //使用量是否超过pod的request字段
		priority,  //pod的QOS类型
		memory(stats)  //使用量和request之间的差值
	).Sort(pods) 
	
}

2.4.2 Pid紧张时,对Pod的排序方法

/*
当pid用量达到阈值时,根据pod的QOS类型,对入参pods进行排序。
*/
func rankPIDPressure(pods []*v1.Pod, stats statsFunc) {
	orderedBy(priority).Sort(pods)
}

2.4.3 磁盘紧张时,对Pod的排序方法

/*
当磁盘使用量达到阈值时,对入参pods进行排序。
首先看使用量是否超过pod的request字段
接着看pod的QOS类型
最后看使用量和request之间的差值
*/
func rankDiskPressureFunc(fsStatsToMeasure []fsStatsType, diskResource v1.ResourceName) rankFunc {
	return func(pods []*v1.Pod, stats statsFunc) {
	
		orderedBy(
			exceedDiskRequests(stats, fsStatsToMeasure, diskResource),	//使用量是否超过pod的request字段
			priority,  //pod的QOS类型
			disk(stats, fsStatsToMeasure, diskResource)   //使用量和request之间的差值
		).Sort(pods) 
		
	}
}

2.4 其他方法

2.4.1 func (m *managerImpl) Admit(…)

驱逐管理器的Admit(…)方法会影响Pod是否被允许创建。

/*
1)入参attrs对象中包含一个pod对象
2)m.nodeConditions字段通过本方法和kubelet的其他流程有关联:
  2.1)HandlePodAdditions(...)方法中会间接调用本方法,从而达到不创建目标pod的效果。
  2.2)syncPod(...)方法中会间接调用本方法,从而到达目标pod(如果存在的话)会被删除的效果。
*/

func (m *managerImpl) Admit(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
	m.RLock()
	defer m.RUnlock()
	
	//节点无资源压力,则允许创建任意pod
	if len(m.nodeConditions) == 0 {
		return lifecycle.PodAdmitResult{Admit: true}
	}
	
	//Critical pods也能被允许创建
	if kubelettypes.IsCriticalPod(attrs.Pod) {
		return lifecycle.PodAdmitResult{Admit: true}
	}
	
	if hasNodeCondition(m.nodeConditions, v1.NodeMemoryPressure) {
		notBestEffort := v1.PodQOSBestEffort != v1qos.GetPodQOS(attrs.Pod)
		
		// 节点处于内存压力状态,best-effort类型的pod不能被允许创建,其余类型的pod可以
		if notBestEffort {
			return lifecycle.PodAdmitResult{Admit: true}
		}

		// 节点处于内存压力状态并且开启TaintNodesByCondition特性,pod能容忍内存压力污点的话,它也能被允许创建
		if utilfeature.DefaultFeatureGate.Enabled(features.TaintNodesByCondition) &&
			v1helper.TolerationsTolerateTaint(attrs.Pod.Spec.Tolerations, &v1.Taint{
				Key:    schedulerapi.TaintNodeMemoryPressure,
				Effect: v1.TaintEffectNoSchedule,
			}) {
			return lifecycle.PodAdmitResult{Admit: true}
		}
	}

	// 来到此处,说明节点是处于磁盘压力状态,或者处于内存压力状态但入参的pod是bestEffort类型,此时不允许创建pod	
	klog.Warningf("Failed to admit pod %s - node has conditions: %v", format.Pod(attrs.Pod), m.nodeConditions)
	return lifecycle.PodAdmitResult{
		Admit:   false,
		Reason:  Reason,
		Message: fmt.Sprintf(nodeConditionMessageFmt, m.nodeConditions),
	}
}

3 Pod被驱逐的经历

3.1 ds和sts控制器的Pod被驱逐后,控制器是不会新建Pod的。

被驱逐的Pod仍然占据着一条记录,sts控制器和ds控制器的主循环逻辑并不会新建Pod。
因此最好在master节点上执行一个crontab任务(kubectl delete pod --all-namespaces --field-selector=‘status.phase==Failed’),定时删除运行失败的Pod(包含了被驱逐的Pod)。

截图中的两个ds pod被驱逐后,由于控制器没有新建Pod,导致该节点的容器通信出现问题,因此被驱逐的ds和sts服务是不会自愈的。
截图中的coredns pod由于是deployment控制器管理的pod,旧pod被驱逐后,控制器会新建Pod,于是集群内部的dns服务是会自愈的。
在这里插入图片描述


4 总结

认识了节点资源可分为可压缩资源和不可压缩资源,当不可压缩资源处于匮乏状态时,这种匮乏会导致节点的不稳定性。
用户进程kubelet作为节点代理,是维护节点稳定性的第一道防线,它会定期检测节点资源状态、Pod实际使用资源的状态、以及用户配置的资源阈值,最终执行清理镜像和容器的操作来维持节点的稳定性,毕竟节点的稳定性是大于容器的稳定性。
第二道防线是Linux OOM KILLER,这也是兜底的防线。
第一道防线和第二道防线之间会有一种协作,就是oom_score_adj。kubelet在创建容器的时候,可设置oom_score_adj,从而影响Linux OOM KILLER杀死进程。

猜你喜欢

转载自blog.csdn.net/nangonghen/article/details/109696462