10.5 资源紧缺时的Pod驱逐机制

在Kubernetes集群中,节点最重要的资源包括CPU、内存和磁盘,其中,内存和磁盘资源属于不可压缩的资源,如果这类资源不足,则无法继续申请新的资源。同时,节点中现存的进程,包括操作系统的进程、用户进程(含Pod进程),随时可能申请更多的内存或磁盘资源,所以在资源严重不足的情况下,操作系统会触发OOM Killer的终极审批。

为了避免出现这种严重后果,Kubernetes设计和实现了一套自动化的Pod驱逐机制,该机制会自动从资源紧张的节点上驱逐一定数量的Pod,以保证在该节点上有充足的资源。具体做法是通过kubelet实现Pod的驱逐过程,而kubelet也不是随机驱逐的,它有自己的一套驱逐机制,每个节点上的kubelet都会通过cAdvisor提供的资源使用指标来监控自身节点的资源使用量,并根据这些指标的变化做出相应的驱逐决定和操作。kubelet持续监控主机的资源使用情况,尽量防止计算资源被耗尽,一旦出现资源紧缺的迹象,就会主动终止一个或多个Pod的运行,以回收紧缺的资源。当一个Pod被终止时,其中的容器会被全部停止,Pod的状态会被设置为Failed。

10.5.1 驱逐时机

首先,在磁盘资源不足时会触发Pod的驱逐行为。Kubernetes包括两种文件系统:nodefs和imagefs。nodefs是kubelet用于存储卷系统、服务程序日志等的文件系统;imagefs是容器运行时使用的可选文件系统,用于存储容器镜像和容器可写层数据。cAdvisor提供了这两种文件系统的相关统计指标,分别如下。

◎ available:表示该文件系统中可用的磁盘空间。

◎ inodesFree:表示该文件系统中可用的inode数量(索引节点数量)。

默认情况下,kubelet检测到下面的任意条件满足时,就会触发Pod的驱逐行为。

◎ nodefs.available<10%。

◎ nodefs.inodesFree<5%。

◎ imagefs.available<15%。

◎ imagefs.available<15%。

如果nodefs达到驱逐阈值,kubelet就会删除所有已失效的Pod及其容器实例对应的磁盘文件。相应地,如果imagefs达到驱逐阈值,则kubelet会删除所有未使用的容器镜像。kubelet不关注其他文件系统,不支持所有其他类型的配置,例如保存在独立文件系统中的卷和日志。

然后,当节点的内存不足时也会触发Pod的驱逐行为。memory.available代表当前节点的可用内存,默认情况下,memory.available<100Mi时会触发Pod的驱逐行为。驱逐Pod的过程:①kubelet从cAdvisor中定期获取相关的资源使用量指标数据,通过配置的阈值筛选出满足驱逐条件的Pod;②kubelet对这些Pod进行排序,每次都选择一个Pod进行驱逐。

最后,从Kubernetes 1.9版本开始,kubelet在驱逐Pod的过程中不会参考Pod的QoS等级,只根据Pod的nodefs使用量进行排序,并选择使用量最多的Pod进行驱逐。所以即使是QoS等级为Guaranteed的Pod,在这个阶段也有可能被驱逐(例如nodefs使用量最大)。

10.5.2 驱逐阈值

kubelet可以定义驱逐阈值,一旦超出阈值,就会触发kubelet的资源回收行为。

阈值的定义方式如下:

其中:①当前仅支持一个operator(运算符)“<”(小于);②quantity需要符合Kubernetes的数量表达方式,也能以%结尾的百分比表示。

例如,如果一个节点有10GiB内存,我们希望在可用内存不足1GiB时进行驱逐Pod的操作,就可以这样定义驱逐阈值:memory.available<10%或者memory.available<1GiB。

对驱逐阈值又可以通过软阈值和硬阈值两种方式进行设置,如下所述。

1. 驱逐软阈值

驱逐软阈值由一个驱逐阈值和一个管理员设定的宽限期共同定义。当系统资源消耗达到软阈值时,在这一状况的持续时间达到宽限期之前,kubelet不会触发驱逐动作。如果没有定义宽限期,则kubelet会拒绝启动。

另外,可以定义终止Pod的宽限期。如果定义了这一宽限期,那么kubelet会使用pod.Spec.TerminationGracePeriodSeconds和最大宽限期这两个值之间较小的数值进行宽限,如果没有指定,则kubelet会立即“杀掉”Pod。

软阈值的定义包括以下几个参数。

◎ --eviction-soft:描述驱逐阈值(例如memory.available<1.5GiB),如果满足这一条件的持续时间超过宽限期,就会触发对Pod的驱逐动作。

◎ --eviction-soft-grace-period:驱逐宽限期(例如memory.available=1m30s),用于定义达到软阈值之后持续时间超过多久才进行驱逐。

◎ --eviction-max-pod-grace-period:在达到软阈值后,终止Pod的最大宽限时间(单位为s)。

2. 驱逐硬阈值

硬阈值没有宽限期,如果达到了硬阈值,则kubelet会立即“杀掉”Pod并进行资源回收。

硬阈值的定义包括参数--eviction-hard:驱逐硬阈值,一旦达到阈值,就会触发对Pod的驱逐操作。

kubelet的默认硬阈值定义如下:

kubelet的--housekeeping-interval参数用于定义了一个时间间隔,kubelet每隔一个这样的时间间隔就会对驱逐阈值进行评估。

10.5.3 节点状态

kubelet会将一个或多个驱逐信号与节点状态对应起来。无论是触发了硬阈值还是触发了软阈值,kubelet都会认为当前节点的压力太大,如表10.6所示为节点状态与驱逐信号的对应关系。

表10.6 节点状态与驱逐信号的对应关系

kubelet会持续向Master报告节点状态的更新过程,这一频率由参数--node-status-update-frequency指定,默认为10s。

10.5.4 节点状态的振荡

如果一个节点状态在软阈值的上下振荡,但没有超过宽限期,则会导致该节点的相应状态在True和False之间不断变换,可能对调度的决策过程产生负面影响。

要防止这种状态出现,可以使用参数--eviction-pressure-transition-period(在脱离压力状态前需要等待的时间,默认值为5m0s)为kubelet设置脱离压力状态之前需要等待的时间。

这样一来,kubelet在把压力状态设置为False之前,会确认在检测周期之内该节点没有达到驱逐阈值。

10.5.5 回收Node级别的资源

如果达到了驱逐阈值,并且也过了宽限期,kubelet就会回收超出限量的资源,直到驱逐信号量回到阈值以内。

kubelet在驱逐用户Pod之前,会尝试回收Node级别的资源。在观测到磁盘压力时,基于服务器是否为容器运行时定义了独立的imagefs,会有不同的资源回收过程。

1. 有Imagefs时

(1)如果nodefs文件系统达到了驱逐阈值,则kubelet会删掉已停掉的Pod和容器来清理空间。

(2)如果imagefs文件系统达到了驱逐阈值,则kubelet会删掉所有无用的镜像来清理空间。

2. 没有Imagefs时

如果nodefs文件系统达到了驱逐阈值,则kubelet会这样清理空间:首先删除已停掉的Pod、容器;然后删除所有无用的镜像。

10.5.6 驱逐用户的Pod

kubelet如果无法在节点上回收足够的资源,就会开始驱逐用户的Pod。

kubelet会按照下面的标准对Pod的驱逐行为进行判断。

◎ Pod要求的服务质量。

◎ Pod对紧缺资源的消耗量(相对于资源请求Request)。

接下来,kubelet会按照下面的顺序驱逐Pod。

(1)BestEffort:紧缺资源消耗最多的Pod最先被驱逐。

(2)Burstable:根据相对请求来判断,紧缺资源消耗最多的Pod最先被驱逐,如果没有Pod超出它们的请求,则策略会瞄准紧缺资源消耗量最大的Pod。

(3)Guaranteed:根据相对请求来判断,紧缺资源消耗最多的Pod最先被驱逐,如果没有Pod超出它们的请求,则策略会瞄准紧缺资源消耗量最大的Pod。

Guaranteed Pod永远不会因为其他Pod的资源消费被驱逐。如果系统进程(例如kubelet、docker、journald等)消耗了超出system-reserved或者kube-reserved的资源,而在这一节点上只运行了Guaranteed Pod,那么为了保证节点的稳定性并降低异常消耗对其他Guaranteed Pod的影响,必须选择一个Guaranteed Pod进行驱逐。

本地磁盘是一种BestEffort资源。如有必要,kubelet会在DiskPressure的情况下,对Pod进行驱逐以回收磁盘资源。kubelet会按照QoS进行评估。如果kubelet判定缺乏inode资源,就会通过驱逐最低QoS的Pod方式来回收inodes。如果kubelet判定缺乏磁盘空间,就会在相同QoS的Pod中选择消耗最多磁盘空间的Pod进行驱逐。下面针对有Imagefs和没有Imagefs的两种情况,说明kubelet在驱逐Pod时选择Pod的排序算法,然后按顺序对Pod进行驱逐。

1. 有Imagefs的情况

如果nodefs触发了驱逐,则kubelet会根据nodefs的使用情况(以Pod中所有容器的本地卷和日志所占的空间进行计算)对Pod进行排序。

如果imagefs触发了驱逐,则kubelet会根据Pod中所有容器消耗的可写入层的使用空间进行排序。

2. 没有Imagefs的情况

如果nodefs触发了驱逐,则kubelet会对各个Pod中所有容器的总体磁盘消耗(以本地卷+日志+所有容器的写入层所占的空间进行计算)进行排序。

10.5.7 资源最少回收量

在某些场景下,驱逐Pod可能只回收了很少的资源,这就导致了kubelet反复触发驱逐阈值。另外,回收磁盘这样的资源是需要消耗时间的。

要缓和这种状况,kubelet可以对每种资源都定义minimum-reclaim。kubelet一旦监测到了资源压力,就会试着回收不少于minimum-reclaim的资源数量,使得资源消耗量回到期望的范围。

例如,可以配置--eviction-minimum-reclaim如下:

这样配置的效果如下。

◎ 当memory.available超过阈值并触发了驱逐操作时,kubelet会启动资源回收,并保证memory.available至少有500MiB。

◎ 当nodefs.available超过阈值并触发了驱逐操作时,kubelet会恢复nodefs.available到至少1.5GiB。

◎ 当imagefs.available超过阈值并触发了驱逐操作时,kubelet会保证imagefs.available恢复到至少102GiB。

在默认情况下,所有资源的eviction-minimum-reclaim都为0。

10.5.8 节点资源紧缺情况下的系统行为

1. 调度器的行为

在节点资源紧缺的情况下,节点会向Master报告这一状况。在Master上运行的调度器(Scheduler)以此为信号,不再继续向该节点调度新的Pod。如表10.7所示为节点状况与调度行为的对应关系。

表10.7 节点状况与调度行为的对应关系

2.Node的OOM行为

如果节点在kubelet能够回收内存之前遭遇了系统的OOM(内存不足),节点则依赖oom_killer的设置进行响应(OOM评分系统详见10.4节的说明)。

kubelet根据Pod的QoS为每个容器都设置了一个oom_score_adj值,如表10.8所示。

表10.8 kubelet根据Pod的QoS为每个容器都设置了一个oom_score_adj值

如果kubelet无法在系统OOM之前回收足够的内存,则oom_killer会根据内存使用比率来计算oom_score,将得出的结果和oom_score_adj相加,得分最高的Pod首先被驱逐。

这个策略的思路是,QoS最低且相对于调度的Request来说消耗最多内存的Pod会首先被驱逐,来保障内存的回收。

与Pod驱逐不同,如果一个Pod的容器被OOM“杀掉”,则可能被kubelet根据RestartPolicy重启。

3. 对DaemonSet类型的Pod驱逐的考虑

通过DaemonSet创建的Pod具有在节点上自动重启的特性,因此我们不希望kubelet驱逐这种Pod。然而kubelet目前并没有能力分辨DaemonSet的Pod,所以无法单独为其制定驱逐策略,所以强烈建议不要在DaemonSet中创建BestEffort类型的Pod,避免产生驱逐方面的问题。

10.5.9 可调度的资源和驱逐策略实践

假设一个集群的资源管理需求如下。

◎ 节点内存容量:10GiB。

◎ 保留10%的内存给系统守护进程(操作系统、kubelet等)。

◎ 在内存使用率达到95%时驱逐Pod,以此降低系统压力并防止系统OOM。

为了满足这些需求,kubelet应该设置如下参数:

在这个配置方式中隐式包含这样一个设置:系统预留内存也包括资源驱逐阈值。

如果内存占用超出这一设置,则要么是Pod占用了超过其Request的内存,要么是系统使用了超过500MiB的内存。在这种设置下,节点一旦开始接近内存压力,调度器就不会向该节点部署Pod,并且假定这些Pod使用的资源数量少于其请求的资源数量。

10.5.10 现阶段的问题

1.kubelet无法及时观测到内存压力

kubelet目前通过cAdvisor定时获取内存使用状况的统计情况。如果内存使用在这个时间段内发生了快速增长,且kubelet无法观察到MemoryPressure,则可能会触发OOMKiller。Kubernetes正在尝试将这一过程集成到memcg通知API中来减少这一延迟,而不是让内核首先发现这一情况。

对用户来说,一个较为可靠的处理方式就是设置驱逐阈值大约为75%,这样就降低了发生OOM的概率,提高了驱逐标准,有助于集群状态的平衡。

2.kubelet可能会错误地驱逐更多的Pod

这也是状态搜集存在时间差导致的。未来可能会通过按需获取根容器的统计信息来减少计算偏差。