节点压力驱逐是指 kubelet 主动终止 Pod 以回收节点上 资源 的过程。
kubelet 会监控集群节点上的内存、磁盘空间和文件系统 inode 等资源。当一个或多个资源达到特定的消耗水平时,kubelet 会主动终止节点上的一个或多个 Pod,以回收资源并防止资源耗尽。
在节点压力驱逐过程中,kubelet 会将所选 Pod 的 阶段(Phase) 设置为 Failed,并终止该 Pod。
节点压力驱逐与 API 发起的驱逐 不同。
kubelet 不会遵循你配置的 PodDisruptionBudget 或 Pod 的 terminationGracePeriodSeconds。如果你使用 软驱逐阈值,kubelet 会遵循你配置的 eviction-max-pod-grace-period。如果你使用 硬驱逐阈值,kubelet 会使用 0s 的宽限期(立即关闭)来终止 Pod。
在终止最终用户 Pod 之前,kubelet 会尝试 回收节点级资源。例如,当磁盘资源不足时,它会删除未使用的容器镜像。
如果这些 Pod 由一个会替换已失败 Pod 的 工作负载 管理对象(例如 StatefulSet 或 Deployment)管理,控制平面(kube-controller-manager)将创建新的 Pod 来替代被驱逐的 Pod。
如果你在资源压力较大的节点上运行 静态 Pod,kubelet 可能会驱逐该静态 Pod。随后,kubelet 会尝试创建一个替代 Pod,因为静态 Pod 始终代表在该节点上运行 Pod 的意图。
kubelet 在创建替代 Pod 时会考虑静态 Pod 的 优先级。如果静态 Pod 清单指定了低优先级,并且集群控制平面内定义了更高优先级的 Pod,且节点处于资源压力下,kubelet 可能无法为该静态 Pod 腾出空间。即使节点处于资源压力下,kubelet 也会持续尝试运行所有静态 Pod。
kubelet 使用各种参数来做出驱逐决策,如下所示
驱逐信号是特定资源在特定时间点的当前状态。kubelet 通过将这些信号与驱逐阈值(节点上应保持的最小资源量)进行比较,从而做出驱逐决策。
kubelet 使用以下驱逐信号
| 驱逐信号 | 描述 | 仅限 Linux |
|---|---|---|
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 | • |
containerfs.available | containerfs.available := node.stats.runtime.containerfs.available | |
containerfs.inodesFree | containerfs.inodesFree := node.stats.runtime.containerfs.inodesFree | • |
pid.available | pid.available := node.stats.rlimit.maxpid - node.stats.rlimit.curproc | • |
在此表中,描述列展示了 kubelet 如何获取该信号的值。每个信号都支持百分比或字面量。kubelet 会根据与该信号关联的总容量计算百分比值。
在 Linux 节点上,memory.available 的值派生自 cgroupfs,而不是像 free -m 这样的工具。这一点很重要,因为 free -m 在容器内无法工作。如果用户使用了 节点可分配(node allocatable) 特性,资源不足的决策会在 cgroup 层级的最终用户 Pod 部分以及根节点本地做出。这个 脚本 或 cgroupv2 脚本 复现了 kubelet 计算 memory.available 时所执行的一系列步骤。kubelet 在计算中排除了 inactive_file(非活跃 LRU 列表上的文件支持内存字节数),因为它假设该内存可以在压力下被回收。
在 Windows 节点上,memory.available 的值通过从节点的全局 CommitLimit 中减去全局 CommitTotal 得出(通过 GetPerformanceInfo() 系统调用查询)。请注意,如果节点的页面文件大小发生变化,CommitLimit 也会随之改变!
kubelet 识别三个特定的文件系统标识符,可用于驱逐信号(<identifier>.inodesFree 或 <identifier>.available)
nodefs:节点的主文件系统,用于本地磁盘卷、非内存支持的 emptyDir 卷、日志存储、临时存储等。例如,nodefs 包含 /var/lib/kubelet。
imagefs:一个可选的文件系统,容器运行时可以使用它来存储容器镜像(只读层)和容器可写层。
containerfs:一个可选的文件系统,容器运行时可以使用它来存储可写层。与主文件系统(见 nodefs)类似,它用于存储本地磁盘卷、非内存支持的 emptyDir 卷、日志存储和临时存储,但不存储容器镜像。当使用 containerfs 时,imagefs 文件系统可以被拆分,仅用于存储镜像(只读层)。
Kubernetes v1.31 [beta](默认启用)拆分镜像文件系统(split image filesystem)特性启用了对 containerfs 文件系统的支持,并增加了几个新的驱逐信号、阈值和指标。要使用 containerfs,Kubernetes v1.36 版本要求启用 KubeletSeparateDiskGC 特性门控。目前,只有 CRI-O (v1.29 或更高版本) 提供了 containerfs 文件系统支持。
因此,kubelet 通常允许三种容器文件系统配置选项
所有内容都在单个 nodefs 上,也称为“rootfs”或简称为“root”,没有专用的镜像文件系统。
容器存储(见 nodefs)位于专用磁盘上,imagefs(可写和只读层)与根文件系统分离。这通常被称为“拆分磁盘(split disk)”或“独立磁盘(separate disk)”文件系统。
容器文件系统 containerfs(与 nodefs 相同,外加可写层)位于根目录下,而容器镜像(只读层)存储在独立的 imagefs 上。这通常被称为“拆分镜像(split image)”文件系统。
kubelet 将尝试从底层的容器运行时直接自动发现这些文件系统及其当前配置,并忽略其他本地节点文件系统。
kubelet 不支持其他容器文件系统或存储配置,目前也不支持为镜像和容器使用多个文件系统。
为了支持驱逐功能,一些 kubelet 垃圾回收特性已被废弃
| 现有标志 | 原因 |
|---|---|
--maximum-dead-containers | 一旦旧日志存储在容器上下文之外,此项即被废弃 |
--maximum-dead-containers-per-container | 一旦旧日志存储在容器上下文之外,此项即被废弃 |
--minimum-container-ttl-duration | 一旦旧日志存储在容器上下文之外,此项即被废弃 |
你可以指定自定义的驱逐阈值,供 kubelet 在做出驱逐决策时使用。你可以配置 软(soft) 和 硬(hard) 驱逐阈值。
驱逐阈值的格式为 [驱逐信号][操作符][数量],其中
驱逐信号 是要使用的 驱逐信号。操作符 是你想要使用的 关系操作符,例如 <(小于)。数量 是驱逐阈值的大小,例如 1Gi。数量 的值必须符合 Kubernetes 使用的资源量表示方式。你可以使用字面量值或百分比 (%)。例如,如果一个节点总共有 10GiB 内存,你想在可用内存低于 1GiB 时触发驱逐,你可以将驱逐阈值定义为 memory.available<10% 或 memory.available<1Gi(不能两者同时使用)。
软驱逐阈值将驱逐阈值与管理员指定的宽限期相结合。在超过宽限期之前,kubelet 不会驱逐 Pod。如果你未指定宽限期,kubelet 在启动时会报错。
你可以同时指定软驱逐阈值宽限期和 kubelet 在驱逐期间使用的最大允许 Pod 终止宽限期。如果你指定了最大允许宽限期,而达到了软驱逐阈值,kubelet 会使用两者中的较小值。如果你未指定最大允许宽限期,kubelet 会立即终止被驱逐的 Pod,而不进行优雅终止。
你可以使用以下标志来配置软驱逐阈值
eviction-soft:一组驱逐阈值,如 memory.available<1.5Gi,如果持续超过指定的宽限期,可能会触发 Pod 驱逐。eviction-soft-grace-period:一组驱逐宽限期,如 memory.available=1m30s,定义了软驱逐阈值必须持续多久才能触发 Pod 驱逐。eviction-max-pod-grace-period:响应软驱逐阈值时终止 Pod 所允许的最大宽限期(以秒为单位)。硬驱逐阈值没有宽限期。当达到硬驱逐阈值时,kubelet 会立即终止 Pod,而不进行优雅终止,以回收资源。
你可以使用 eviction-hard 标志来配置一组硬驱逐阈值,例如 memory.available<1Gi。
kubelet 具有以下默认硬驱逐阈值
memory.available<100Mi (Linux 节点)memory.available<500Mi (Windows 节点)nodefs.available<10%imagefs.available<15%nodefs.inodesFree<5% (Linux 节点)imagefs.inodesFree<5% (Linux 节点)这些硬驱逐阈值的默认值仅在未更改任何参数时设置。如果你更改了任何参数的值,则其他参数的值将不会作为默认值继承,并会被设置为零。为了提供自定义值,你应该分别提供所有阈值。你也可以在 kubelet 配置文件中将 MergeDefaultEvictionSettings 设置为 true。如果设置为 true 且更改了任何参数,其他参数将继承其默认值,而不是 0。
containerfs.available 和 containerfs.inodesFree (Linux 节点) 的默认驱逐阈值设置如下
如果所有内容使用单个文件系统,则 containerfs 阈值与 nodefs 设置相同。
如果为镜像和容器配置了独立的文件系统,则 containerfs 阈值与 imagefs 设置相同。
目前不支持为 containerfs 相关的阈值设置自定义覆盖值,如果尝试这样做,将发出警告;提供的自定义值将被忽略。
kubelet 基于其配置的 housekeeping-interval(默认为 10s)评估驱逐阈值。
kubelet 会报告 节点状况(node conditions),以反映节点因达到硬或软驱逐阈值而处于压力下,这与配置的宽限期无关。
kubelet 将驱逐信号映射到节点状况的对应关系如下
| 节点状况 | 驱逐信号 | 描述 |
|---|---|---|
MemoryPressure | memory.available | 节点上的可用内存已达到驱逐阈值 |
DiskPressure | nodefs.available, nodefs.inodesFree, imagefs.available, imagefs.inodesFree, containerfs.available, 或 containerfs.inodesFree | 节点根文件系统、镜像文件系统或容器文件系统上的可用磁盘空间和 inode 已达到驱逐阈值 |
PIDPressure | pid.available | (Linux) 节点上的可用进程标识符已降至驱逐阈值以下 |
控制平面还会将这些节点状况 映射 到污点。
kubelet 根据配置的 --node-status-update-frequency(默认为 10s)更新节点状况。
在某些情况下,节点会在软驱逐阈值上下波动,而未保持在定义的宽限期内。这导致报告的节点状况在 true 和 false 之间持续切换,导致错误的驱逐决策。
为了防止抖动,你可以使用 eviction-pressure-transition-period 标志,它控制 kubelet 在将节点状况转换为不同状态之前必须等待的时间。转换周期的默认值为 5m。
kubelet 会在驱逐最终用户 Pod 之前尝试回收节点级资源。
当报告 DiskPressure 节点状况时,kubelet 会根据节点上的文件系统回收节点级资源。
imagefs 或 containerfs 时如果节点仅有触发驱逐阈值的 nodefs 文件系统,kubelet 将按以下顺序释放磁盘空间
imagefs 时如果节点有供容器运行时使用的专用 imagefs 文件系统,kubelet 执行以下操作
如果 nodefs 文件系统达到驱逐阈值,kubelet 会对死掉的 Pod 和容器进行垃圾回收。
如果 imagefs 文件系统达到驱逐阈值,kubelet 会删除所有未使用的镜像。
imagefs 和 containerfs 时如果节点配置了供容器运行时使用的 imagefs 文件系统,并同时拥有专用的 containerfs,kubelet 将按以下方式尝试回收资源
如果 containerfs 文件系统达到驱逐阈值,kubelet 会对死掉的 Pod 和容器进行垃圾回收。
如果 imagefs 文件系统达到驱逐阈值,kubelet 会删除所有未使用的镜像。
如果 kubelet 回收节点级资源的尝试未能使驱逐信号低于阈值,kubelet 将开始驱逐最终用户 Pod。
kubelet 使用以下参数来确定 Pod 驱逐顺序
因此,kubelet 按以下顺序排列并驱逐 Pod
使用量超过请求量的 BestEffort 或 Burstable Pod。这些 Pod 首先按优先级驱逐,然后按其使用水平超过请求量的程度驱逐。
Guaranteed Pod 以及使用量小于请求量的 Burstable Pod 最后被驱逐,同样依据优先级。
DiskPressure 状态则不适用。Guaranteed Pod 仅在所有容器都指定了请求量和限制且相等时才得到保证。这些 Pod 不会因为另一个 Pod 的资源消耗而被驱逐。如果系统守护进程(如 kubelet 和 journald)消耗的资源超过了通过 system-reserved 或 kube-reserved 预留的资源,且节点上仅剩下资源使用量少于请求量的 Guaranteed 或 Burstable Pod,kubelet 必须选择驱逐其中一个 Pod 以保持节点稳定性,并限制资源耗尽对其他 Pod 的影响。在这种情况下,它会优先选择驱逐优先级最低的 Pod。
如果你正在运行一个 静态 Pod,并希望避免其在资源压力下被驱逐,请直接设置该 Pod 的 priority 字段。静态 Pod 不支持 priorityClassName 字段。
当 kubelet 因 inode 或进程 ID 耗尽而驱逐 Pod 时,它会使用 Pod 的相对优先级来确定驱逐顺序,因为 inode 和 PID 没有请求量(requests)。
kubelet 根据节点是否拥有专用的 imagefs 或 containerfs 文件系统,以不同的方式对 Pod 进行排序
imagefs 或 containerfs(nodefs 和 imagefs 使用相同文件系统)时nodefs 触发驱逐,kubelet 根据 Pod 的总磁盘使用量(本地卷 + 日志 + 所有容器的可写层)对 Pod 进行排序。imagefs(nodefs 和 imagefs 文件系统分离)时如果 nodefs 触发驱逐,kubelet 根据 nodefs 使用量(本地卷 + 所有容器的日志)对 Pod 进行排序。
如果 imagefs 触发驱逐,kubelet 根据所有容器的可写层使用量对 Pod 进行排序。
imagesfs 和 containerfs(imagefs 和 containerfs 已被拆分)时如果 containerfs 触发驱逐,kubelet 根据 containerfs 使用量(本地卷 + 日志 + 所有容器的可写层)对 Pod 进行排序。
如果 imagefs 触发驱逐,kubelet 根据 镜像存储 等级对 Pod 进行排序,该等级代表给定镜像的磁盘使用量。
containerfs.available 指标设置自定义值。此特定指标的配置将自动设置,以反映为 nodefs 或 imagefs 设置的值(具体取决于配置)。在某些情况下,Pod 驱逐仅能回收少量稀缺资源。这可能导致 kubelet 反复达到配置的驱逐阈值并触发多次驱逐。
你可以使用 --eviction-minimum-reclaim 标志或 kubelet 配置文件 来为每种资源配置最小回收量。当 kubelet 注意到资源稀缺时,它会持续回收该资源,直到达到你指定的量。
例如,以下配置设置了最小回收量
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
evictionHard:
memory.available: "500Mi"
nodefs.available: "1Gi"
imagefs.available: "100Gi"
evictionMinimumReclaim:
memory.available: "0Mi"
nodefs.available: "500Mi"
imagefs.available: "2Gi"
在此示例中,如果 nodefs.available 信号达到驱逐阈值,kubelet 会回收资源,直到该信号达到 1GiB 的阈值,然后继续回收 500MiB 的最小量,直到可用 nodefs 存储值达到 1.5GiB。
类似地,kubelet 尝试回收 imagefs 资源,直到 imagefs.available 值达到 102Gi,代表 102 GiB 可用的容器镜像存储空间。如果 kubelet 能够回收的存储量少于 2GiB,则 kubelet 不会回收任何内容。
所有资源的默认 eviction-minimum-reclaim 均为 0。
如果节点在 kubelet 能够回收内存之前发生 内存不足 (OOM) 事件,节点将依赖 oom_killer 来响应。
kubelet 会根据 Pod 的 QoS 为每个容器设置 oom_score_adj 值。
| 服务质量 (QoS) | oom_score_adj |
|---|---|
Guaranteed | -997 |
BestEffort | 1000 |
Burstable | min(max(2, 1000 - (1000 × memoryRequestBytes) / machineMemoryCapacityBytes), 999) |
如果 kubelet 无法在节点发生 OOM 之前回收内存,oom_killer 会根据节点上使用的内存百分比计算 oom_score,然后将 oom_score_adj 加到该值上,得到每个容器的有效 oom_score。接着它会杀死得分最高的容器。
这意味着,相比其调度请求,消耗大量内存的低 QoS Pod 中的容器会先被杀死。
与 Pod 驱逐不同,如果容器被 OOM 杀死,kubelet 可以根据其 restartPolicy 重启它。
以下章节描述了驱逐配置的最佳实践。
当你为 kubelet 配置驱逐策略时,应确保如果 Pod 会立即引发内存压力,调度程序不会调度它们。
考虑以下场景
kubelet 等)预留 10% 的内存容量为了使此方案工作,kubelet 启动如下
--eviction-hard=memory.available<500Mi
--system-reserved=memory=1.5Gi
在此配置中,--system-reserved 标志为系统预留了 1.5GiB 内存,即 总内存的 10% + 驱逐阈值量。
如果 Pod 使用量超过其请求量,或者系统使用的内存超过 1GiB,导致 memory.available 信号降至 500MiB 以下并触发阈值,则节点可以达到驱逐阈值。
Pod 优先级是做出驱逐决策的主要因素。如果你不希望 kubelet 驱逐属于 DaemonSet 的 Pod,可以通过在 Pod 规范中指定合适的 priorityClassName,赋予这些 Pod 足够高的优先级。你也可以使用较低优先级或默认优先级,仅在有足够资源时才允许该 DaemonSet 中的 Pod 运行。
以下章节描述了与资源不足处理相关的已知问题。
默认情况下,kubelet 会定期轮询 cAdvisor 以收集内存使用统计信息。如果内存使用量在该窗口内快速增加,kubelet 可能无法足够快地观察到 MemoryPressure,OOM killer 仍会被调用。
你可以使用 --kernel-memcg-notification 标志在 kubelet 上启用 memcg 通知 API,以便在阈值跨越时立即收到通知。
如果你不是为了追求极致利用率,而是为了实现合理的超卖比,解决此问题的一个可行方法是使用 --kube-reserved 和 --system-reserved 标志为系统分配内存。
在 Linux 上,内核会将非活跃 LRU 列表上的文件支持内存字节数作为 active_file 统计信息进行跟踪。kubelet 将 active_file 内存区域视为不可回收。对于密集使用块支持的本地存储(包括临时本地存储)的工作负载,文件和块数据的内核级缓存意味着许多最近访问的缓存页面很可能会被计为 active_file。如果足够多的此类内核块缓冲区位于活跃 LRU 列表上,kubelet 可能会将其视为高资源占用,并将节点标记为正处于内存压力下 - 从而触发 Pod 驱逐。
更多详细信息,请参阅 https://github.com/kubernetes/kubernetes/issues/43916
你可以通过为可能执行密集 I/O 活动的容器设置相同的内存限制和内存请求来规避此行为。你需要估算或测量该容器的最佳内存限制值。