為 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 模式。