为 Kubernetes 调优 Linux Swap:深入探讨

Kubernetes 的节点 Swap 特性预计将在即将发布的 Kubernetes v1.34 版本中进入**稳定**(stable)阶段,该特性允许使用 swap:这与为了性能可预测性而禁用 swap 的传统做法相比,是一个重大的转变。本文专门关注在 Linux 节点上调优 swap,该特性在这些节点上可用。通过允许 Linux 节点在物理 RAM 耗尽时使用二级存储作为额外的虚拟内存,节点 swap 支持旨在提高资源利用率并减少内存不足(OOM)终止事件。

然而,启用 swap 并非一个“开箱即用”的解决方案。在内存压力下,节点的性能和稳定性严重依赖于一组 Linux 内核参数。配置不当可能导致性能下降,并干扰 Kubelet 的驱逐逻辑。

在这篇博文中,我将深入探讨控制 swap 行为的关键 Linux 内核参数。我将探究这些参数如何影响 Kubernetes 工作负载性能、swap 利用率以及关键的驱逐机制。我将展示各种测试结果,展示不同配置的影响,并分享我为实现稳定和高性能的 Kubernetes 集群而获得的最佳设置。

Linux swap 简介

从宏观层面看,Linux 内核通过页(page)来管理内存,通常每页大小为 4KiB。当物理内存变得紧张时,内核的页面置换算法会决定将哪些页移动到 swap 空间。虽然确切的逻辑是一个复杂的优化过程,但这个决策过程受到某些关键因素的影响:

  1. 页面访问模式(页面最近被访问的情况)
  2. 页面脏状态(页面是否被修改过)
  3. 内存压力(系统对空闲内存的需求紧急程度)

匿名内存与文件支持的内存

重要的是要理解,并非所有内存页都是相同的。内核区分匿名内存和文件支持的内存。

匿名内存:这是指不由磁盘上的特定文件支持的内存,例如程序的堆和栈。从应用程序的角度来看,这是私有内存,当内核需要回收这些页时,必须将它们写入专用的 swap 设备。

文件支持的内存:这种内存由文件系统上的文件支持。这包括程序的可执行代码、共享库和文件系统缓存。当内核需要回收这些页时,如果它们未被修改(“干净”),可以直接丢弃。如果页面已被修改(“脏”),内核必须先将更改写回文件,然后才能丢弃。

虽然没有 swap 的系统仍然可以在内存压力下通过丢弃干净的文件支持页来回收内存,但它无法卸载匿名内存。启用 swap 提供了这种能力,允许内核将较少访问的内存页移动到磁盘,以节省内存,避免系统 OOM 终止。

用于 swap 调优的关键内核参数

为了有效地调优 swap 行为,Linux 提供了几个可以通过 sysctl 管理的内核参数。

  • vm.swappiness:这是最著名的参数。它的值范围是 0 到 200(在旧版内核中是 100),用于控制内核在交换匿名内存页和回收文件支持的内存页(页面缓存)之间的偏好。
    • 高值(例如:90+):内核会积极地将较少使用的匿名内存换出,为文件缓存腾出空间。
    • 低值(例如:< 10):内核会强烈倾向于丢弃文件缓存页,而不是交换匿名内存。
  • vm.min_free_kbytes:该参数告诉内核保留一个最小量的空闲内存作为缓冲区。当空闲内存量低于这个安全缓冲区时,内核会开始更积极地回收页面(交换,并最终处理 OOM 终止)。
    • 功能:它作为一个安全杠杆,确保内核有足够的内存来处理不能延迟的关键分配请求。
    • 对 swap 的影响:设置较高的 min_free_kbytes 值实际上提高了空闲内存的下限,导致内核在内存压力下更早地启动 swap。
  • vm.watermark_scale_factor:这个设置控制着不同水位线之间的差距:minlowhigh,这些水位线是基于 min_free_kbytes 计算的。
    • 水位线解释:
      • low:当空闲内存低于此标记时,kswapd 内核进程会唤醒,在后台回收页面。这是一个交换周期的开始。
      • min:当空闲内存达到这个最低水平时,激进的页面回收将阻塞进程分配。如果回收页面失败,将导致 OOM 终止。
      • high:一旦空闲内存达到这个水平,内存回收就会停止。
    • 影响:较高的 watermark_scale_factor 值会在 lowmin 水位线之间创建一个更大的缓冲区。这给了 kswapd 更多的时间来逐步回收内存,以免系统进入临界状态。

在典型的服务器工作负载中,你可能会有一个长期运行的进程,其部分内存会变得“冷”。较高的 swappiness 值可以通过将冷内存换出到 swap 空间来释放 RAM,供其他可以从保留文件缓存中受益的活动进程使用。

通过调优 min_free_kbyteswatermark_scale_factor 参数来提前交换窗口,将为 kswapd 提供更多空间来将内存卸载到磁盘,并在突发内存峰值期间防止 OOM 终止。

Swap 测试与结果

为了了解这些参数的实际影响,我设计了一系列压力测试。

测试设置

  • 环境:Google Cloud 上的 GKE
  • Kubernetes 版本: 1.33.2
  • 节点配置n2-standard-2(8GiB RAM,50GB swap 位于 pd-balanced 磁盘上,未加密),Ubuntu 22.04
  • 工作负载:一个自定义的 Go 应用程序,旨在以可配置的速率分配内存,产生文件缓存压力,并模拟不同的内存访问模式(随机 vs 顺序)。
  • 监控:一个 sidecar 容器,每秒捕获系统指标。
  • 保护:通过在各自的 cgroup 中设置 memory.swap.max=0,防止关键系统组件(kubelet、容器运行时、sshd)被交换。

测试方法

我在具有不同 swappiness 设置(0、60 和 90)的节点上运行了一个压力测试 Pod,并改变了 min_free_kbyteswatermark_scale_factor 参数,以观察在重度内存分配和 I/O 压力下的结果。

可视化 swap 的运行过程

下图来自一个 100MBps 的压力测试,展示了 swap 的运行过程。随着空闲内存(在“内存使用”图中)减少,swap 使用量(Swap Used (GiB))和换出活动(Swap Out (MiB/s))增加。关键的是,随着系统更多地依赖 swap,I/O 活动和相应的等待时间(在“CPU 使用”图中的 IO Wait %)也随之上升,表明 CPU 存在压力。

Graph showing CPU, Memory, Swap utilization and I/O activity on a Kubernetes node

发现

我使用默认内核参数(swappiness=60min_free_kbytes=68MBwatermark_scale_factor=10)进行的初步测试很快导致了 OOM 终止,甚至在高内存压力下出现意外的节点重启。通过选择合适的内核参数,可以实现节点稳定性和性能之间的良好平衡。

swappiness 的影响

swappiness 参数直接影响内核在回收匿名内存(交换)和丢弃页面缓存之间的选择。为了观察这一点,我运行了一个测试,其中一个 Pod 产生并保持文件缓存压力,然后第二个 Pod 以 100MB/s 的速度分配匿名内存,以观察内核在回收方面的偏好。

我的发现揭示了一个明显的权衡:

  • swappiness=90:内核主动将不活动的匿名内存换出,以保留文件缓存。这导致了高且持续的 swap 使用量和显著的 I/O 活动(“Blocks Out”),进而导致 CPU 上的 I/O 等待出现峰值。
  • swappiness=0:内核倾向于丢弃文件缓存页,从而延迟了 swap 的消耗。然而,关键是要理解这**并不会禁用交换**。当内存压力很高时,内核仍然会将匿名内存交换到磁盘。

选择取决于工作负载。对于对 I/O 延迟敏感的工作负载,较低的 swappiness 更可取。对于依赖于大型且频繁访问的文件缓存的工作负载,较高的 swappiness 可能更有利,前提是底层磁盘速度足够快以处理负载。

调优水位线以防止驱逐和 OOM 终止

我遇到的最关键的挑战是快速内存分配与 Kubelet 驱逐机制之间的相互作用。当我的测试 Pod(被有意配置为超额使用内存)以高速率(例如 300-500 MBps)分配内存时,系统很快就耗尽了空闲内存。

使用默认水位线时,用于回收的缓冲区太小。在 kswapd 能够通过交换释放足够内存之前,节点就会达到临界状态,导致两种可能的结果:

  1. Kubelet 驱逐:如果 Kubelet 的驱逐管理器检测到 memory.available 低于其阈值,它会驱逐该 Pod。
  2. OOM killer:在一些高速率场景中,OOM Killer 会在驱逐完成前激活,有时会杀死并非压力来源的更高优先级的 Pod。

为了缓解这个问题,我调整了水位线:

  1. min_free_kbytes 增加到 512MiB:这迫使内核更早地开始回收内存,提供了一个更大的安全缓冲区。
  2. watermark_scale_factor 增加到 2000:这扩大了 lowhigh 水位线之间的差距(在我的测试节点的 /proc/zoneinfo 中从约 337MB 增加到约 591MB),有效地增加了交换窗口。

这种组合给了 kswapd 一个更大的操作区域和更多的时间来在内存峰值期间将页面交换到磁盘,成功地在我的测试运行中防止了过早的驱逐和 OOM 终止。

下表比较了来自 /proc/zoneinfo 的水位线级别(非 NUMA 节点)

min_free_kbytes=67584KiBwatermark_scale_factor=10min_free_kbytes=524288KiBwatermark_scale_factor=2000
Node 0, zone Normal
  pages free 583273
  boost 0
  min 10504
  low 13130
  high 15756
  spanned 1310720
  present 1310720
  managed 1265603
Node 0, zone Normal
  pages free 470539
  min 82109
  low 337017
  high 591925
  spanned 1310720
  present 1310720
  managed 1274542

下图揭示了内核缓冲区大小和缩放因子在决定系统如何响应内存负载方面起着至关重要的作用。通过正确组合这些参数,系统可以有效地使用 swap 空间来避免驱逐并保持稳定性。

A side-by-side comparison of different min_free_kbytes settings, showing differences in Swap, Memory Usage and Eviction impact

风险与建议

在 Kubernetes 中启用 swap 是一个强大的工具,但它也带来了必须通过仔细调优来管理的风险。

  • 性能下降的风险:交换比访问 RAM 慢几个数量级。如果应用程序的活动工作集被换出,其性能将因高 I/O 等待时间(颠簸)而急剧下降。swap 最好配置在 SSD 支持的存储上以提高性能。

  • 掩盖内存泄漏的风险:Swap 可能会隐藏应用程序中的内存泄漏,否则这些泄漏可能很快导致 OOM 终止。有了 swap,一个有泄漏的应用程序可能会随着时间的推移慢慢降低节点性能,使根本原因更难诊断。

  • 禁用驱逐的风险:Kubelet 会主动监控节点的内存压力并终止 Pod 以回收资源。不当的调优可能导致在 kubelet 有机会优雅地驱逐 Pod 之前发生 OOM 终止。正确配置 min_free_kbytes 对于确保 kubelet 的驱逐机制保持有效至关重要。

Kubernetes 上下文

内核水位线和 kubelet 驱逐阈值共同在节点上创建了一系列内存压力区域。需要调整驱逐阈值参数,以配置 Kubernetes 管理的驱逐在 OOM 终止之前发生。

Preferred thresholds for effective swap utilization

如图所示,一个理想的配置是创建一个足够大的“交换区”(在 highmin 水位线之间),以便内核可以通过交换来处理内存压力,而不是让可用内存下降到驱逐/直接回收区。

基于这些发现,我推荐以下配置作为启用 swap 的 Linux 节点的起点。您应该使用自己的工作负载对此进行基准测试。

  • vm.swappiness=60:Linux 默认值是通用工作负载的一个良好起点。然而,理想值取决于工作负载,对 swap 敏感的应用程序可能需要更仔细的调优。
  • vm.min_free_kbytes=500000(500MB):将其设置为一个合理的高值(例如,节点总内存的 2-3%),为节点提供一个合理的安全缓冲区。
  • vm.watermark_scale_factor=2000:为 kswapd 创建一个更大的工作窗口,以防止在突发内存分配峰值期间发生 OOM 终止。

我鼓励您在 Kubernetes 集群中首次设置 swap 时,在测试环境中使用自己的工作负载运行基准测试。Swap 性能可能对不同的环境差异敏感,例如 CPU 负载、磁盘类型(SSD vs HDD)和 I/O 模式。