如果你不確定,請優先使用 zswap。除非有高度特定的理由,否則請勿使用 zram。

我最近收到一位讀者關於 Linux 上壓縮交換技術的提問:

我讀了您關於 Linux 記憶體管理(交換)的文章,終於聽到專家(而不是網路上的專家)的說法了 :) 像是「你有 32GB 記憶體 - 關掉它」。

如果能針對 zswap 或 zram 進行後續討論會很棒… :) 如果它們(在配備 32GB 或更多記憶體的桌上型電腦上)好用,哪個比較好,為什麼… 我理解它們的工作原理(從描述中),但在實際生活中很難衡量和比較。同樣地,許多「網路專家」只是說「zram - 因為你不會磨損你的 SSD」…

嗯,首先,既然我寫了這篇文章,顯然奉承會讓你無往不利 ;-)

網路上關於何時使用 zswap 相較於 zram,以及它們之間的權衡確實存在很多混淆和錯誤資訊。我花了近十年的時間研究核心記憶體管理和交換程式碼,我見證了這些技術的演進以及圍繞它們產生的一些常見誤解。

對大多數人來說,簡短的答案是:使用 zswap。除非你對它可能對你的工作負載帶來的風險有深入的了解,否則請勿使用 zram。但要理解為什麼(以及何時 zram 可能實際上是正確的選擇),需要深入了解這兩種技術在核心本身是如何運作的。

大多數人認為 zswap 和 zram 只是同一件事的兩種不同風味:壓縮交換。表面上看,這是正確的——兩者都會壓縮原本會寫入磁碟的頁面——但它們在核心如何處理記憶體壓力方面做出了根本不同的假設,為你的情況選擇錯誤的技術可能會讓情況比根本沒有交換更糟。

它們之間最顯著的差異在於它們在核心儲存層級中的位置,以及因此它們能夠向核心其他部分發出的訊號:

換句話說,zram 提供了一個硬容量限制,而 zswap 在更快的(即壓縮的 RAM)和較慢的交換(即你的磁碟)之間提供自動分層,並隨著記憶體壓力的增加而平滑降級。對大多數人來說,平滑降級是你想要的,但讓我們看看這些在實踐中是如何發揮作用的。

zram 創建了一個壓縮的區塊裝置,作為獨立的交換。設定 zram 交換的典型程序是:

你可能會注意到,這看起來和你設定分割區交換時做的幾乎一樣,這不是巧合:核心透過區塊層將其視為另一個儲存裝置(參見 zram_submit_bio())。

這使得 zram 非常適合嵌入式系統:它完全是獨立的,不依賴於磁碟儲存,而這些系統可能根本沒有磁碟。當你在嵌入式控制器或配備 SD 卡的 Raspberry Pi 上時,zram 為你提供了一定程度的記憶體卸載,而無需任何外部依賴。到目前為止都相當合理。

然而,當你有磁碟儲存(如 SSD)時,zram 的區塊裝置架構會產生一些顯著的限制。核心基本上對 zram 與典型慢速磁碟上的區塊裝置沒有區別,因此會套用其正常的以磁碟為導向的預設值。舉例來說,有一個核心可調參數 vm.page-cluster,它決定了在載入單個交換頁面時我們要預讀多少頁。預設值為 3——這意味著核心一次讀取 2^3 = 8 頁——以分攤磁碟工作,這在順序讀取時成本很低。這對硬碟來說更重要,但在現代 NAND 上,隨機讀取和順序讀取之間仍然存在有意義的效能差異。

當我們開始在 Quest 上使用 zram 時(因為它運行在 Android 上,而 Android 使用 zram),這種預讀行為是我們遇到的第一個問題之一。對於磁碟,預讀假設是成立的:磁碟上相鄰的頁面傾向於在時間上相鄰需要,所以分攤是好的。對於 zram,壓縮頁面沒有局部性,所以假設反轉了:每次你想要 1 頁時,你都會用 7 個你不需要的頁面污染交換快取,這會嚴重地對你不利。

重要的是,這既不是 Quest 特有的,也不是 vm.page-cluster 的問題——它更多是核心將 zram 視為任何其他區塊裝置的結果。vm.page-cluster 至少是可調的,但核心中還有其他一些假設甚至沒有作為 sysctl 公開。在許多情況下,核心會與你對抗,需要大量的努力和知識才能正確處理。

相比之下,zswap 完全不創建區塊裝置——它直接整合到核心的記憶體管理子系統中。這個區別比聽起來更重要:因為 zswap 編織到回收路徑本身中,核心實際上知道 zswap 池中的哪些頁面是熱的,哪些是冷的。zram,作為另一個區塊裝置,沒有這種可見性。

這種意識是自動分層的基礎,也是 zswap 在記憶體壓力下比 zram 降級得更好的主要原因。

關於如何啟用 zswap,請檢查是否已啟用:cat /sys/module/zswap/parameters/enabled。大多數主要發行版預設啟用它。如果沒有:

要使其在重新啟動後持久化,請將 zswap.enabled=1 添加到你的核心命令列。

在配置選擇器上,優先選擇 zsmalloc 而不是舊的 z3fold 或 zbud 配置器。zsmalloc 透過分組相似的物件來實現更高的壓縮率,而舊的配置器使用固定大小的物件,容易浪費空間。z3fold 和 zbud(以及 zpool 介面本身)已從上游核心中移除,所以在當前核心上你不需要設定這個。在仍然包含它們的發行版核心上,你可能需要明確選擇 zsmalloc:

那麼 zswap 的分層實際上是如何工作的呢?當核心需要交換出一個頁面時,它會呼叫 swap_writeout(),這會讓 zswap first dibs 攔截它:

如果 zswap_store() 返回 true,則頁面已儲存在壓縮的 RAM 中,並且永遠不會接觸磁碟。只有當 zswap 拒絕(或未啟用)時,核心才會回退到寫入後備交換裝置。

以下是 zswap_store() 在內部執行的操作:

你可能會注意到這個 queue_work(shrink_wq, &zswap_shrink_work) 的業務。它的作用是,由於我們剛剛發現 zswap 已滿,所以排隊一個工作者來自動將一些冷頁面逐出到磁碟。它最終會呼叫 shrink_worker(),後者負責處理這種回收。

這種與記憶體管理子系統其餘部分的緊密整合,以及其他 mm 程式碼知道這種儲存的性質,這就是 zswap 與 zram 顯著不同的地方。zswap 作為 SSD 交換的前端透明壓縮層,而不是像 zram 那樣的獨立儲存層。當池滿時,它會自動觸發收縮器將冷頁面逐出到磁碟。

但是,等等,Chris,我在我的 zram 交換裝置上設定了優先級。那麼這不就和 zswap 的這種「分層」架構一樣嗎?

不幸的是,這種邏輯是人們最常讓自己陷入 zram 陷阱的方式之一。故事大致是這樣的:

這在紙面上聽起來不錯,對吧?當然,這也正是它不斷讓人生氣的原因。那麼問題出在哪裡呢?

問題在於核心如何在多個裝置之間分配交換空間。核心有一個名為 swap_alloc_slow() 的函數,負責尋找正確的裝置和叢集進行寫入:

這段程式碼基本上說的是,優先級較高的裝置會先被使用。這聽起來不錯,對吧?當然,這也是使用者總是這樣設定它的原因。但這樣的用戶無意中創造了一個隨著正常運行時間越來越有可能出現的陷阱。

陷阱是這樣的:由於 zram 裝置上的交換具有最高優先級,核心會優先處理 zram 的所有分配。當 zram 滿時,它會切換到基於磁碟的交換來處理所有未來的分配。

這意味著,在沒有干預的情況下,你寶貴的 zram 會被隨機交換出去的頁面填滿。這些通常與你現在實際需要的頁面完全相反。

在典型的桌面會話中,這些頁面通常是冷啟動時的資料,在早期被逐出,以便為你剛打開的瀏覽器騰出空間。這些冷頁面會永久佔用快速的 zram。同時,隨著你的會話繼續,記憶體壓力持續存在,更新的、可能是「更熱」的頁面(例如你正在積極切換的最近瀏覽器分頁)被迫溢出到低優先級裝置:慢速的機械硬碟或 SSD。

這就是所謂的 LRU 反轉:你最快的儲存層被最冷的資料堵塞,無法將其逐出,並且會主動將你的工作集強制到最慢的儲存上。在這種情況下,zram 不僅無濟於事,反而會讓情況比根本沒有壓縮交換更糟。更糟糕的是,系統運行的時間越長,情況就越糟:熱頁面漂移到磁碟,冷頁面在 zram 中鈣化,zram 持有的資料和你實際需要的資料之間的差距不斷擴大。太棒了!

除了放置問題,雙方都付出了實際的開銷。進入 zram 的每個頁面都需要 CPU 週期來壓縮。zram 中的每個頁面訪問都需要一個小的故障和解壓縮回主記憶體,然後才能使用。你正在為你沒有主動使用的資料支付壓縮和解壓縮的開銷,而你需要的資料卻在慢速磁碟 I/O 中停滯不前。

現在,如果我幾年前寫這篇文章,討論就會在這裡結束。:-) 但自從核心 4.14 以來,zram 就有了寫回支援,這是解決這個問題的一種嘗試。

透過配置寫回,zram 可以寫回閒置或壓縮效果不佳的頁面。這意味著現代 zram 設定理論上可以實現分層,但是:

這聽起來可能沒那麼難。請允許我嘗試說服你反對。

要實現類似 zswap 的行為,將不可壓縮或閒置頁面移動到磁碟,你必須創建自己的解決方案。一個問題是,你不能簡單地將 zram 寫回指向交換檔案或現有的交換分割區。寫回介面需要一個專用的、未格式化的區塊裝置。使用 zram-generator,看起來像這樣:

另一個問題是,你必須重新分割你的磁碟以創建一個專用的分割區用於 zram 後備,或管理迴路裝置(這會增加開銷)。你也無法輕鬆地與系統的休眠交換或其他資料共用此空間。

一個更大的障礙是實際進行刷新。即使連接了裝置,zram 也無法自動將頁面寫入其中,核心也沒有內建的啟發法來決定何時將資料從 zram 移動到後備裝置,因為 zram 與 mm 子系統的整合程度不足以有效實現這一點。

相反,你必須創建一個 systemd 計時器或 cron 作業來手動觸發此刷新,即使這樣也不容易。

例如,要刷新 zram 無法壓縮的頁面,只需執行以下操作:

…但要刷新閒置或冷資料,則更複雜。因為 zram 對其上有交換的事實不敏感,不像其他類型的資料,zram 本身不追蹤 LRU 年齡,無法簡單地逐出。你必須先告訴核心將頁面標記為閒置,然後告訴 zram 將它們寫出。

正如 Sam 在審查這篇文章時所說的,這是「記憶體管理的 IKEA」——組裝 dombås 衣櫃時還可以,但當它在生產環境中咬你一口時,你會感覺像個 dombås。

除了複雜性之外,與 zswap 的原生 LRU 分層相比,zram 在架構上也處於相當不利的地位。

例如,在 zram 中,這種年齡檢查是一次性事件。當你運行腳本或計時器時,它會拍攝當前狀態的快照。如果一個頁面在 5 分鐘後變冷,它會保留在 RAM 中,直到你再次運行腳本。它與回收過程或收縮器沒有聯繫,如果記憶體壓力突然升高,運行腳本可能就太晚了。

與 zswap 相比,zswap 在正常的記憶體回收生命週期中會評估 LRU 列表。一旦記憶體壓力升高,核心就會在回收過程中查看頁面的即時列表,並立即逐出最舊的頁面。具體來說,使用 zram,記憶體峰值發生在你運行腳本之間,意味著你的應用程式將在最糟糕的時刻寫入磁碟交換。相比之下,使用 zswap,核心會在壓力發生時自然響應。

還有一個粒度問題。使用 zram,你必須猜測一個魔術數字來基於時間(例如 24 小時)執行逐出。如果你猜得太高,你會浪費 RAM。如果你猜得太低,你會刷新你可能實際上想要的資料。畢竟,系統只會做你說的,並且沒有長時間的廣泛分析,很難知道該告訴它什麼才能有效。

相比之下,在 zswap 中,沒有魔術數字,它會根據壓力動態平衡 LRU。如果你有足夠的 RAM 且沒有壓力,它會無限期地保留資料。如果你 RAM 不足,它會積極地逐出最舊的資料,無論它是 24 小時前還是 24 分鐘前。它具有對系統其餘部分的洞察力並能夠與之協商,因此可以做出更好的決策。

最終,zram 寫回是一種權宜之計,而不是解決方案。並不是說它在學術意義上無法奏效——當然可以。問題在於所有棘手的邊緣案例都恰恰在最糟糕的時刻出現:在峰值期間,記憶體壓力最高,而你精心猜測的閾值最有可能錯誤。我強烈建議你不要這樣管理你的記憶體。

如上所述,zswap 的緊密 mm 整合為你免費提供了這一切,這帶有免費午餐的可疑氣味。而且對大多數工作負載來說,這確實有點像免費午餐,但有一些值得注意的陷阱。

zswap 的分層機制透過兩種不同的收縮器機制工作,這兩種機制很容易混淆,因此值得預先了解兩者。

第一個——zswap_shrinker_count()(及其伴隨的 zswap_shrinker_scan())——作為動態收縮器的一部分存在。它由記憶體回收器(如 kswapd、直接回收器和主動回收器 Senpai)獨立觸發,而不是由池限制觸發。它的工作是根據記憶體存取模式、壓縮性和記憶體壓力動態調整 zswap 池的大小,目標是理想情況下你永遠不會達到靜態池限制。在 Meta 的生產環境中,達到靜態池限制很少見,因為這個動態收縮器在達到該程度之前就能保持一切正常。在記憶體受限的系統(如筆記型電腦)上,你可能會更多地看到它。

第二個收縮器 shrink_worker() 是基於限制的回退,僅在池限制實際達到時觸發。這就是效能懸崖所在,下面將對此進行更多介紹。

棘手的部分是決定要逐出多少頁面。逐出太少,池會繼續填滿;逐出太多,你會在剛從磁碟讀取後立即將頁面寫回。以下是 zswap_shrinker_count() 如何處理:

也就是說,zswap 不是依賴靜態閾值或定期輪詢,而是根據回收路徑的即時回饋,追蹤實際的磁碟交換率和壓縮率來逐出。一旦壓力增加,冷頁面就會流入 SSD。當記憶體確實稀缺時,壓縮池會保留你的活動工作集,而不是你幾小時前停止觸碰的資料,並且最重要的頁面故障會保留在快速壓縮的 RAM 中,而不是寫入磁碟。

但是,一如既往,有一個陷阱。當 zswap 達到其 max_pool_percent 限制時,zswap_check_limits() 會導致 zswap_store() 拒絕該頁面並返回 false。這會喚醒 shrink_worker() 將冷頁面逐出到磁碟,但該工作是異步進行的——當前頁面不會儲存在 zswap 中,並且必須立即去某個地方。

接下來會發生什麼取決於 cgroup 的寫回模式:

這意味著在寫回啟用且記憶體壓力很大的情況下,zswap 可能會在沒有警告的情況下開始將頁面直接發送到磁碟。這會產生一個效能懸崖,系統的交換效能會突然從你預期的 RAM 存取幅度下降到你預期的磁碟存取幅度。這並不比 zram 的行為差,但值得注意。

zswap 和 zram 在正常操作下的開銷相似。它們之間的主要區別在於它們在記憶體壓力下的故障模式,而理解這些故障模式是理解使用哪一個的關鍵。

zswap 可以偵測不可壓縮頁面並拒絕它們,將它們直接發送到磁碟。這可以節省 RAM(透過不儲存壓縮效果差的資料)和 CPU 週期(透過不重複嘗試壓縮不可壓縮資料)。你可以在 /sys/kernel/debug/zswap/ 中的 reject_compress_poor 計數器中看到 zswap 執行了多少次。

相比之下,zram 預設情況下會壓縮所有內容,無論壓縮率如何。zram 在其 huge_pages 統計數據中追蹤這些壓縮效果差的頁面,但即使是壓縮到 3.9KB 的 4KB 頁面也會樂意儲存,浪費記憶體和 CPU。

這意味著 zswap 對於包含大量不可壓縮資料的工作負載通常具有更好的最壞情況行為,儘管對於典型的混合工作負載來說,實際差異通常可以忽略不計。

zswap 拒絕不可壓縮頁面時的行為在最近的核心版本中也有所演變。預設情況下,正如你在 swap_writeout() 中看到的,被拒絕的頁面會落入 __swap_writepage() 並寫入磁碟。但是,對於任何交換 I/O 都不希望發生的工作負載,核心現在支援每個 cgroup 的寫回禁用模式(核心 6.8+)。當為 cgroup 禁用時,zswap 拒絕的任何頁面——無論是出於不可壓縮性、池限制還是其他原因——都會循環回到活動列表,而不是寫入磁碟。這可以防止一種 LRU 反轉形式,即熱的不可壓縮頁面比更冷的但可壓縮的頁面更早寫入磁碟。啟用方法如下:

但是,寫回禁用模式有一個缺點:如果記憶體壓力很大,並且一個 cgroup 產生大量不可壓縮資料,回收器可能會陷入病態循環——重複嘗試壓縮相同的不可壓縮頁面,失敗,將它們循環回到活動列表,然後重試。沒有磁碟後備,就無法取得進展,這可能在生產環境中導致嚴重問題。我們正在開發一種方法,該方法將不可壓縮頁面保留在 zswap 池中,而不是循環它們,並以每個 cgroup 的 LRU 形式組織,以便收縮器在它們變冷後將它們逐出到磁碟。

另一個人們說優先使用 zram 而不是 zswap 的原因是,他們認為這可以減少 SSD 磨損——也就是說,他們認為使用它會減少磁碟 I/O。

但這是愚蠢的。RAM 是有限的。如果你用某種類型的資料填滿你的 RAM,最終,當你的所有 RAM 都用完時,資料需要去某個地方。

在大多數情況下,伺服器和桌上型電腦上的記憶體主要由兩種類型的頁面組成。一種是匿名頁面,例如你的程式堆疊和堆資料。另一種是檔案頁面,即磁碟快取。如果你在沒有實體後備裝置的情況下使用 zram,你實際上會將所有匿名資料鎖定在 RAM 中。當記憶體壓力來臨時,核心別無選擇,只能積極地逐出檔案快取以騰出空間。

如果這些被逐出的檔案頁面是「髒的」(即它們包含修改過的資料),核心被迫將它們寫入 SSD 以釋放空間並取得進展。即使它們是「乾淨的」(即未修改),它們也會被丟棄,下次需要時迫使 SSD 再次讀取它們。透過拒絕將冷、未使用的匿名資料透過 zswap 或交換分割區交換到實體磁碟,你會扼殺頁面快取。這會迫使系統不斷地刷新和重新讀取活動檔案。

僅使用 zram 會移除磁碟交換 I/O,但它實際上只是將壓力轉移到頁面快取上。在記憶體壓力下,可能會丟棄更多檔案快取(導致重新讀取)或寫回(如果髒了)。使用基於磁碟的交換(或其前面的 zswap),系統通常可以改為逐出冷匿名頁面,這可能會減少快取抖動,從而減少 I/O。這意味著 zram 實際上可能會增加總磁碟 I/O,如果管理不當的話。

真正的目標是將活動工作集保留在 RAM 中——並且良好利用的磁碟交換可以透過讓冷匿名頁面有地方去,而不是迫使冷熱資料競爭同一個池來幫助你實現這一點。

我們有一些具體的數字可以證明這一點。在 Instagram 上,它運行在 Django 上並且主要受記憶體限制,我們進行了一項測試,將其從現有設定(完全禁用交換)轉移到具有磁碟交換和 zswap 分層的設定。Django 工作程序在其生命週期中會累積大量的冷堆狀態,例如具有重複記憶體的 fork 進程、不斷增長的請求快取、Python 物件開銷,你懂的。結果是雙重的:

正如你所想像的,作為該測試的結果,Instagram 多年來一直在使用 zswap。

現在,有些人可能會想,為什麼添加交換會減少磁碟 I/O?究竟是如何做到,添加更多的基於磁碟的記憶體卸載會減少它?

嗯,你可能會認為你永遠不會用完所有的 RAM。但在 Linux 上,我們實際上並沒有讓 RAM 空著。核心遵循未使用 RAM 就是浪費 RAM 的理念,因此會自動用頁面快取和其他有用的東西填充任何剩餘空間,例如檔案副本、函式庫和磁碟資料,以加快將來的存取。

作為這項工作的一部分,核心守護進程 kswapd 會在可用空間低於某些水位標記時主動喚醒以回收記憶體,並努力平衡記憶體使用。在理想情況下,我們希望始終有頁面可供立即分配而無需等待回收,因此它會將壓力作為正常操作狀態來管理,以確保始終有一個緩衝區用於立即分配。

這種回收必須去某個地方,而僅使用 zram 時,只有一個地方可以去:檔案快取。使用基於磁碟的交換(或其前面的 zswap),核心有一個選擇——它可以回收較冷的,無論是匿名頁面還是檔案快取,基於最近性和存取模式。發生的 I/O 是故意的,而不是絕望的。

當然,Instagram 的工作負載對 zswap 特別有利,所以請對確切的數字持保留態度。但儘管如此,方向上這適用於幾乎所有用例:工作負載通常會隨著時間累積冷匿名頁面,並且這些頁面傾向於良好壓縮。

此外,zswap 還透過充當寫入減少篩檢程式來顯著減少 SSD 磨損。它會在 RAM 中吸收高頻率的頁面輸出/頁面輸入瞬態,在資料接觸磁碟之前進行壓縮。只有真正冷卻且未被快取淘汰的資料才會寫入磁碟。在上面提到的 Instagram 案例中,磁碟寫入減少 25% 的原因是記憶體快取吸收了本來會到達 SSD 的熱寫入抖動。

現代 SSD 通常也能處理數百 TB 的寫入。消費級 SSD 通常提供 150-600 TB TBW。在 2026 年,除非你使用非常便宜的 eMMC 儲存,否則整個對話基本上已經沒有意義了,即使如此,zram 可能也不是你的最佳選擇。

也就是說,如果交換 I/O 對於特定工作負載確實是硬性要求,zswap 的每個 cgroup 寫回禁用模式(請參閱上面的不可壓縮資料部分)可以讓你完全阻止特定 cgroup 的磁碟交換 I/O,而無需放棄 zswap 與記憶體管理子系統其餘部分的整合。你甚至可以進行一些混合搭配:延遲敏感服務可以使用 zswap 並禁用寫回,而其他服務則使用完整的 zswap-then-disk 分層。這比一刀切的 zram 方法靈活得多。

zswap 和 zram 在正常操作下的開銷相似。它們急劇分歧的地方在於它們在記憶體壓力下的故障模式,而理解這些故障模式是理解使用哪一個的關鍵。

zswap 的壓力是持續且主動處理的。隨著池的填滿,動態收縮器(zswap_shrinker_count)會喚醒並提前將冷頁面逐出到磁碟,追蹤磁碟交換率和壓縮率以避免抖動。實際上,這意味著池限制很少被命中。在 Meta 的生產伺服器上,它幾乎從未觸發——動態收縮器在達到該程度之前就能保持一切正常。當達到限制時,會出現一個效能懸崖,頁面開始繞過快取直接寫入磁碟。這並不好,但它是漸進式降級:系統減速而不是懸崖式崩潰。

zram 沒有類似的過程。沒有東西在監控裝置填滿並採取行動。當它達到容量時,它就停止接受頁面。如果存在低優先級的磁碟交換裝置,核心會溢出到該裝置,並出現上述所有 LRU 反轉問題。如果沒有其他裝置,系統將掛起,因為核心拼命嘗試回收任何東西,或者 OOM 殺手被觸發。在這兩種情況下,系統都不會平滑降級。

情況實際上可能更糟——在某些情況下,OOM 殺手可能根本不會被觸發。在 2026 年 3 月,Cloudflare 的 Matt Fleming 報告了生產機器上 20 到 30 分鐘的斷電,OOM 殺手從未觸發過。原因是 zram 的區塊裝置架構的直接後果。should_reclaim_retry() 透過檢查可用交換槽來估計可回收記憶體。對於基於磁碟的交換,一個可用槽意味著一個頁面可以放置在那裡而不會消耗更多 RAM。對於 zram,該裝置是薄配置的:它報告其完整的配置大小作為可用容量,即使支援這些槽的實體 RAM 已耗盡。一個使用量為 10% 的 377 GiB 裝置報告約 340 GiB 的可用槽——但寫入它們需要系統已不再擁有的 RAM。should_reclaim_retry() 繼續返回 true,核心在直接回收中無限期地循環。即使 OOM 殺手最終被觸發,它也不是許多人期望的乾淨逃生閥。

你可能會想:如果系統正在大量交換到磁碟,響應性已經被破壞了。我寧願系統 OOM 殺死一個進程,也不願讓用戶慢慢被折磨死。但這裡有一個危險的細微差別經常被忽略——核心 OOM 殺手根本不是瞬時的。

正如我在 SREcon 演講中所述,依賴核心內建的 OOM 殺手來挽救響應性通常是一場輸掉的戰鬥。核心實際上並沒有以任何直接意義上知道何時記憶體不足:「記憶體不足」不僅意味著記憶體已滿,還意味著沒有東西可以回收——確定這一點的唯一方法是嘗試完整的回收週期並讓它失敗。

在 OOM 殺手被調用之前,核心會進入一個積極回收的週期:

我們經常看到這個過程對簡單的生產工作負載可能需要幾秒鐘甚至幾分鐘。在此期間,你的應用程式會被暫停,系統會顯示為掛起。到 OOM 殺手實際觸發時,用戶可能已經經歷了顯著的無響應,並且系統可能已鎖定到用戶幾乎無能為力的程度。

核心 OOM 殺手也非常不精確。它使用一個啟發法「分數」來決定殺死誰——如果「分數」聽起來像一個模糊的詞,那是因為它是。這是核心承認它也不知道正確的受害者是誰,並希望你透過 oom_score_adj 來填補這個空白。實際結果是,它經常只是殺死最大的進程,而不是真正洩漏記憶體的進程。考慮一個系統,其中 Chrome 佔用 80% 的 RAM,而一個背景守護進程開始洩漏:OOM 殺手目標是 Chrome,殺死它會穩定系統,而守護進程永遠不會被識別。下次它洩漏時,Chrome 會再次 dies。而守護進程則繼續洩漏。

為什麼像 Fedora 這樣的發行版在配備快速 SSD 的桌上型電腦上預設使用僅 zram 設定?為什麼不直接使用 zswap?

答案是 zswap 實際上從未被考慮過。Fedora 的目標已經有一段時間是完全消除磁碟交換,由於 zswap 在架構上是磁碟交換的前端快取,因此它根本不是候選者。

他們消除磁碟交換的原因並不完全是關於記憶體管理,而且很大程度上也是由其他系統屬性驅動的。例如,當交換將頁面逐出到磁碟時,私鑰、密碼、會話令牌和瀏覽器狀態會出現在持久化分割區上。zram 完全繞過了這一點:它位於 RAM 中,重新啟動會將其清除,因此沒有東西洩漏到磁碟的風險。交換加密也可以在這裡提供幫助,但它增加了配置複雜性,並且仍然需要信任金鑰管理故事,最終 Fedora 的目標是消除表面區域,而不是在其之上疊加緩解措施。

Fedora 將 zram 與 systemd-oomd 配對,後者監控 PSI 以根據策略提前主動終止進程。他們還透過僅有一個交換裝置(在 zram 上)來繞過 LRU 反轉,因此沒有磁碟交換裝置可以反轉。這使得一些 zram 設定在給定約束下是有意義的,並使用了 systemd-oomd 的緩解措施。使用他們的設定,在記憶體壓力很大的桌面用戶可能已經看到響應性降低,而用戶空間 OOM 守護進程提前乾淨地終止有問題的進程通常比等待系統透過磁碟交換進行數分鐘的抖動要好。

但是,這很重要,這僅適用於運行和配置了用戶空間 OOM 守護進程,並且根本沒有磁碟交換裝置。沒有 systemd-oomd,你會得到硬限制而沒有乾淨的終止,系統將掛起,情況甚至更糟。

因此,可以肯定地說,這幾乎肯定不是純粹為了記憶體管理而優化的設定,而 zswap 更緊密的 mm 整合和 LRU 分層提供了 zram 無法比擬的真正優勢。但是記憶體效率並不是 Fedora 唯一優化的目標,並且他們的幾個約束與記憶體管理無關。在這些約束下,這個決定是連貫的:優化性總是相對於你試圖實現的目標(這也適用於你,親愛的讀者——你比我更了解你在做什麼)。

也就是說,如果未來幾年 zswap 獲得即將推出的無磁碟模式,我會驚訝地發現那裡沒有一些運動,特別是考慮到核心開發人員越來越遠離支援 zram(稍後會詳細介紹)。

在簡單的情況下,使用 zram,你必須預先猜測裝置大小。如果你猜得太小,你會浪費潛力。如果你設定得太大,你會冒 OOM 殺死的風險,或者導致不必要的微小故障。

那麼 Fedora 是如何設定大小的?嗯,他們將其設定為你 RAM 的 100%。搞定。

…好吧,也許這需要一點額外的解釋。:-)

Fedora 採取了積極的方法。他們將 zram 裝置的大小設定為你實體 RAM 的 100%,上限為 8GB。你可能會想,這有什麼意義——如何擁有一個大小可能等於你 RAM 大小的交換裝置?而且讀取 zram 中的頁面肯定需要將其解壓縮到主 RAM 中,對吧?如果 zram 滿了,它甚至會將解壓縮後的頁面放在哪裡?這是什麼瘋狂?!

Fedora 透過一些有根據的賭博來解決這個問題。首先,zram 是薄配置的。在實際發生頁面故障之前,沒有記憶體使用。因此,一個大小為 100% 的 zram 裝置,如果裡面什麼都沒有,除了用於記帳的空間外,不佔用任何空間。

此外,他們賭你的資料壓縮效果很好——例如 3:1。那麼,一個 100% 大小的 zram 裝置實際上只佔用 RAM 的三分之一,留下 66% 的空間供作業系統和解壓縮緩衝區使用。

然後他們使用 systemd-oomd 來監控記憶體壓力。如果它看到 zram 實際填滿了 RAM,它會在達到死鎖牆(沒有足夠空間解壓縮)之前根據策略終止某些東西。

如果你不確定,我強烈建議你使用 zswap 和基於磁碟的交換。它與記憶體管理子系統的其餘部分整合得更緊密,比 zram 擁有更好的回收和逐出啟發法,能更好地處理不可壓縮資料,並且在壓力下能平滑降級。它還能透明地處理休眠。即使你有大量的 RAM,基於磁碟的交換在許多情況下仍然具有顯著的優勢。

zram 有意義的情況更為微妙。在嵌入式系統中,zram 非常簡單且獨立。當根本沒有磁碟時,它是顯然的選擇,並且在這些環境中,硬限制的可預測性通常是一個特徵而不是一個錯誤。另一種情況是當你故意透過設計完全無磁碟時,就像 Fedora 的情況一樣。

Android 是無磁碟方法的突出例子:數十億台裝置運行 zram 而沒有磁碟交換,並配備了用戶空間終止守護進程(lmkd)。這種組合完全繞過了 LRU 反轉,因為沒有磁碟交換層可以反轉。但是 Android 的 zram 有效,因為它已經針對手機硬體和手機工作負載進行了廣泛的調優——正如上面所述,即使是像預讀預設值這樣基本的事情也會對你不利,而這些只是可見的旋鈕。這些假設無法轉移,Android 的調優也一樣。

伺服器是 zram 變得特別難以推銷的另一個地方。除了 zram(不)降級的方式之外,zram 的記憶體使用基本上對核心是不透明的,並且不會計入任何 cgroup。核心無法看到 zram 在給定 cgroup 的名義上消耗了多少記憶體,這可能會破壞資源隔離和服務之間的壓力訊號。僅此一點就成為許多運行容器化或隔離工作負載的組織採用 zram 的硬性障礙。

即使是嵌入式和無磁碟的案例也在縮小。我們許多從事這個領域的人對未來的發展方向有類似的看法。Christoph,他是區塊層的核心貢獻者之一,直言不諱:

不行。停止在區塊層添加 hack,只是因為你濫用區塊驅動程式進行壓縮交換。請大家將精力集中在可插拔的 zswap 後端和無後備儲存的 zswap 上,而不是讓 zram 的混亂變得更糟。

Johannes,記憶體管理維護者之一,表示同意:

壓縮是一種記憶體消耗。一種很大的消耗。並且與交換一起,它位於回收路徑中。所以現在你必須解決區塊層之間的複雜 MM 問題。 […] 我們應該嘗試讓 zswap 成為單一的壓縮交換實現。這將大大簡化正在處理 MM 和交換子系統的核心開發人員的工作。這對用戶來說也會更好。

Christoph 提到的「可插拔的 zswap 後端和無後備儲存的 zswap」指的是允許 zswap 在沒有任何磁碟交換裝置的情況下運行的積極工作——這將關閉 zram 在無磁碟設定中最後的用例。Nhat Pham 目前正在以虛擬交換空間的名義領導這項工作。發展方向相當明確。

實際上,在我們大規模部署 zswap 的服務中,它持續減少了 OOM,降低了磁碟寫入壓力,並且無需任何手動干預。zram 是記憶體管理子系統中完全手動的部分——你承擔正確管理它的責任,否則就要承擔後果——而 zswap 由核心本身管理,具有所有即時回饋、回收整合和自動分層。對於絕大多數 Linux 系統來說,你確實希望核心透過 zswap 來完成這項工作。

非常感謝 Nhat、Javier、Sam、Johannes 和 Andreas 對這篇文章的回饋。

有問題或評論嗎?我的電子郵件是 chris@[此網域]。