Skip to main content

1. 前言

kubelet监控集群节点的内存、磁盘空间和文件系统的inode等资源。 当这些资源中的一个或者多个达到特定的消耗水平,kubelet可以主动驱逐节点上一个或者多个Pod,以回收资源防止节点最终崩溃。

需要注意的是:由Kubelet发起的驱逐并不会遵循PodDisruptionBudgetterminationGracePeriodSeconds的配置。对K8S来说,保证节点资源充足优先级更高,牺牲小部分Pod来保证剩余的大部分Pod

另外Kubelet在真正驱逐Pod之前会执行一次垃圾回收,尝试回收节点资源,如果回收后资源充足了,就可以避免驱逐Pod

kubelet中实现Pod驱逐逻辑的具体源码位于:https://github.com/kubernetes/kubernetes/blob/4096c9209cbf20c51d184e83ab6ffa3853bd2ee6/pkg/kubelet/eviction/eviction_manager.go

2. 驱逐信号和阈值:什么时候会驱逐 Pod?

kubelet使用各种参数来做出驱逐决定,具体包含以下3个部分:

  • 1)驱逐信号
  • 2)驱逐条件
  • 3)监控间隔

2.1 驱逐信号与节点condition

2.1.1 驱逐信号

Kubelet使用驱逐信号来代表特定资源在特定时间点的状态,根据不同资源,Kubelet 中定义了5种驱逐信号。

驱逐信号这个词感觉有点迷,实际看下来就是检测了这几种资源的余量用于判断是否需要驱逐Pod

Linux系统中,5种信号分别为:

驱逐信号描述
memory.availablememory.available := node.status.capacity[memory] - node.stats.memory.workingSet
nodefs.availablenodefs.available := node.stats.fs.available
nodefs.inodesFreenodefs.inodesFree := node.stats.fs.inodesFree
imagefs.availableimagefs.available := node.stats.runtime.imagefs.available
imagefs.inodesFreeimagefs.inodesFree := node.stats.runtime.imagefs.inodesFree
pid.availablepid.available := node.stats.rlimit.maxpid - node.stats.rlimit.curproc

可以看到,主要是对 内存磁盘PID 这三种类型资源进行检测。其中磁盘资源又分为两类:

  1. nodefs:节点的主要文件系统,用于本地磁盘卷、不受内存支持的emptyDir卷、日志存储等。 例如,nodefs 包含 /var/lib/kubelet/
  2. imagefs:可选文件系统,供容器运行时存储容器镜像和容器可写层。

2.1.2 节点condition与污点

除了用驱逐信号来判断是否需要驱逐Pod之外,Kubelet 还会把驱逐信号反应为节点的状态。

即:更新到node对象的status.condition字段里

对应关系如下:

Node ConditionEviction SignalDescription
MemoryPressurememory.available节点可用内存余量满足驱逐阈值
DiskPressurenodefs.available, nodefs.inodesFree, imagefs.available, or imagefs.inodesFree节点主文件系统或者镜像文件系统剩余磁盘空间或者 inodes 数量满足驱逐阈值
PIDPressurepid.available节点上可用进程标识符(processes identifiers) 低于驱逐阈值

总的来说就是节点上对应资源不足时kubelet就会被节点打上对应的标记。

同时 control plane(具体为 node controller) 会自动把节点上相关condition转换为taint标记,具体见:#taint-nodes-by-condition,这样可以避免把Pod调度到本来就资源不足的Pod上。

如果不给资源不足的节点打上标记,可能发生Pod刚调度过去就被驱逐的情况。

整体运作流程

  • 1)首先Kubelet检测到节点上剩余磁盘空间不足,更新Node Condition增加 DiskPressure 状态
  • 2)然后node controller自动将Condition转换为污点 node.kubernetes.io/disk-pressure
  • 3)最后Pod调度的时候,由于没有添加对应的容忍,因此会优先调度到其他节点,或者最终调度失败

有时候节点状态处于阈值附近上下波动,导致软驱逐条件也在truefalse之间反复切换,为了过滤掉这种误差,kubelet 提供了 eviction-pressure-transition-period 参数来限制,节点状态转换前必须要等待对应的时间,默认为5m

一般这种检测状态的为了提升稳定性,都会给一个静默期,比如K8S中的HPA扩缩容也会设置静默期,防止频繁触发扩缩容动作。

2.2 驱逐阈值:判断条件

拿到资源当前状态之后,就可以根据阈值判断是否需要触发驱逐动作了。

Kubelet使用[eviction-signal][operator][quantity]格式定义驱逐条件,其中:

  • eviction-signal 是要使用的驱逐信号
    • 比如剩余内存 memory.available
  • operator 是你想要的关系运算符
    • 比如 <(小于)。
  • quantity 是驱逐条件数量,例如 1Gi
    • quantity 的值必须与Kubernetes使用的数量表示相匹配。
    • 你可以使用文字值或百分比(%)。

例如,如果一个节点的总内存为10GiB并且你希望在可用内存低于1GiB时触发驱逐, 则可以将驱逐条件定义为 memory.available < 10%memory.available < 1G

根据紧急程度驱逐条件又分为软驱逐 eviction-soft硬驱逐 eviction-hard

2.2.1 软驱逐 eviction-soft

一般驱逐条件比较保守,此时还可以等待Pod优雅终止,需要配置以下参数

  • eviction-soft:软驱逐条件,例如 memory.available<1.5Gi
  • eviction-soft-grace-period:软驱逐宽限期,需要保持驱逐条件这么久之后才开始驱逐Pod(必须指定,否则kubelet启动时会直接报错)
  • eviction-max-pod-grace-period:软驱逐最大Pod宽限期,驱逐Pod的时候给Pod优雅终止的时间

Pod 生命周期部分 我们可以配置 spec.terminationGracePeriodSeconds来控制Pod优雅终止时间,就像这样:

apiVersion: v1
kind: Pod
metadata:
name: example
spec:
containers:
- name: my-container
image: my-image
terminationGracePeriodSeconds: 60 # 设置终止期限为 60 秒

然后驱逐这里又配置了一个 eviction-max-pod-grace-period,实际驱逐发生时,Kubelet会取二者中的较小值来作为最终优雅终止宽限期

2.2.2 硬驱逐 eviction-hard

相比之下则是比较紧急,配置条件都很极限,在往上节点可能会崩溃的那种。

比如 内存小于100Mi这种,因此硬驱逐没有容忍时间,需要配置硬驱逐条件 eviction-hard

只要满足驱逐条件 kubelet 就会立马将 pod kill 掉,而不是发送 SIGTERM 信号。

Kubelet提供了以下的默认硬驱逐条件:

  • memory.available < 100Mi
  • nodefs.available < 10%
  • imagefs.available < 15%
  • nodefs.inodesFree < 5%

例如下面这个配置:

  • eviction-soft: memory.available < 1 Gi
  • eviction-soft-grace-period: 60s
  • eviction-max-pod-grace-period: 30s
  • eviction-hard: memory.available < 100 Mi

当节点可用内存余量低于1Gi连续持续60s之后,kubelet就会开始软驱逐,给被选择的Pod发送SIGTERM,使其优化终止,如果终止时间超过30s则强制kill

如果节点内存可用余量低于100Mi,则kubelet进入硬驱逐,立马kill掉被选中的Pod

2.3 状态检测 & 驱逐频率

Kubelet默认每10s会检测一次节点状态,即驱逐判断条件为10s一次,当然也可以通过housekeeping-interval参数进行配置。

3. 回收节点级资源

为了提升稳定性,减少Pod驱逐次数,Kubelet在执行驱逐前会进行一次垃圾回收。如果本地垃圾回收后资源充足了就不再驱逐。

对于磁盘资源可以通过驱逐Pod以外的方式进行回收:

如果节点有一个专用的 imagefs文件系统供容器运行时使用,Kubelet会执行以下操作:

  • 如果 nodefs文件系统满足驱逐条件,Kubelet垃圾收集死亡Pod和容器。
  • 如果 imagefs文件系统满足驱逐条件,Kubelet将删除所有未使用的镜像。

如果节点只有一个满足驱逐条件的nodefs文件系统,kubelet按以下顺序释放磁盘空间:

  1. 对死亡的Pod和容器进行垃圾收集
  2. 删除未使用的镜像

对于CPU、内存资源则是只能驱逐Pod方式进行回收。

4. 驱逐对象:哪些 Pod 会被驱逐

如果进行垃圾回收后,节点资源也满足驱逐条件,那么为了保证当前节点不被压垮,kubelet只能驱逐Pod了。

根据触发驱逐的资源不同,驱逐目标筛选逻辑也有不同。

4.1 内存资源导致的驱逐

对于内存资源kubelet使用以下参数来确定Pod驱逐顺序:https://github.com/kubernetes/kubernetes/blob/4096c9209cbf20c51d184e83ab6ffa3853bd2ee6/pkg/kubelet/eviction/eviction_manager.go#L254

  • 1)Pod 使用的资源是否超过请求值:即当前占用资源是否高于yaml中指定的requests
  • 2)Pod 的优先级:这里不是QoS优先级,而是 priority-class 这个优先级
  • 3)Pod 使用资源相对于请求资源的百分比:百分比越高则越容易别驱逐,比如请求100Mi,使用了60Mi则占用了60%,如果没有limits导致Pod使用了200Mi,那就是200%

因此,虽然没有严格按照QoS来排序,但是整个驱逐顺序和PodQoS是有很大关系的:

4.1.1 QoS 级别介绍

Kubernetes中,Pod根据其资源配置被分为三个QoSQuality of Service)级别:

1. Guaranteed(保证级别)

  • 所有容器都必须设置CPU和内存的requestslimits
  • 每个容器的requests必须等于limits
  • 这类Pod拥有最高的服务质量保证,在资源紧张时最不容易被驱逐
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "1Gi" # 必须等于requests
cpu: "500m" # 必须等于requests

2. Burstable(突发级别)

  • 至少有一个容器设置了CPU或内存的requests
  • 不满足Guaranteed级别的条件(即requests不等于limits,或者部分容器未设置limits
  • 这类Pod可以在资源充足时使用超过requests的资源
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi" # 大于requests,允许突发使用
cpu: "1000m" # 大于requests,允许突发使用

3. BestEffort(尽力而为级别)

  • 所有容器都没有设置CPU和内存的requestslimits
  • 这类Pod在资源紧张时最容易被驱逐
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
resources: {} # 没有设置任何资源限制

4.1.2 驱逐优先级关系

  • 首先考虑资源使用量超过其请求的QoS级别为BestEffortBurstablePod。 这些Pod会根据它们的优先级以及它们的资源使用级别超过其请求的程度被驱逐。
  • 资源使用量少于请求量的Guaranteed级别的PodBurstable Pod根据其优先级被最后驱逐。

4.2 inode & pid 导致的驱逐

kubeletinode进程标识符(pid) 不足而驱逐Pod时, 它使用Pod的相对优先级来确定驱逐顺序,因为inodePID没有对应的请求字段。

相对优先级排序方式如下:

  1. 节点有 imagefs
  • 如果 nodefs 触发驱逐,kubelet会根据 nodefs 使用情况(本地卷 + 所有容器的日志)对Pod进行排序。
  • 如果 imagefs 触发驱逐,kubelet会根据所有容器的可写层使用情况对Pod进行排序。
  1. 节点没有 imagefs
  • 如果 nodefs 触发驱逐,kubelet会根据磁盘总用量(本地卷 + 日志和所有容器的可写层)对Pod进行排序。
  1. 即:inode、pid导致的驱逐,Kubelet会优先驱逐磁盘消耗大的Pod,而不是根据Pod QoS来。

4.3 小结

Kubelet并没有使用QoS来作为驱逐顺序,但是对于内存资源回收的场景,驱逐顺序和QoS是相差不大的。不过对于 磁盘和 PID 资源的回收则完全不一样的,会优先考虑驱逐磁盘占用多的Pod,即使Pod QoS等级为 Guaranteed

毕竟不是所有资源都有requestslimits,只能先驱逐占用量大的。

4.4 最少资源回收量

为了保证尽量少的Podkubelet每次只会驱逐一个Pod,驱逐后就会判断一次资源是否充足。

这样可能导致该节点资源一直处于驱逐阈值,反复达到驱逐条件从而触发多次驱逐,kubelet也提供了参数 --eviction-minimum-reclaim 来指定每次驱逐最低回收资源,达到该值后才停止驱逐。从而减少触发驱逐的次数。

5. 参考资料