在 Linux 桌面上擠出更多記憶體的各種方法、對 SWAP 常見的誤解

平時有需求平行跑一些很吃記憶體的圖形應用程式,再加上現在流行 Electron app,在 Linux 桌面上我便經常遇到界面卡頓。因此花時間研究擠出更多記憶體的方法。

SWAP

講到記憶體不夠當然會馬上想到 SWAP ,但對現代的電腦和應用程式,專業觀點似乎大多認為要避免使用 SWAP 。因為 SWAP 太慢,當主記憶體吃滿,系統完全卡住的時候,才開始把資料搬到 SWAP 通常也已經來不及挽救沒有回應的系統了(例如這篇這篇)。我的經驗是在這個狀況下大部分還是只能強制重開機,只有少數狀況下系統可以在幾分鐘之內回到有反應可操作的狀態。

不過這也牽涉到兩個參數:vm.swapiness 和 oom killer 的設定 。大部分 Linux 發行版預設的 swapiness 是 60,對於桌面應用來說似乎不夠。然後預設的 OOM killer 動作時間通常都太晚,砍掉程序的選擇感覺也很奇怪(實際上砍掉的程序和使用者覺得應該要被砍掉的程序不一樣)。這部分對於桌面為主的應用應該有和伺服器應用不同的調校。

另一方面, SWAP 是非常古老的設計,當時大多數應用程式的使用方式是在 shell 的前景背景切換,也沒有像現在有這麼多應用程式是基於 GC 類型的 runtime (Javascript, Java, etc)。SWAP 的設計並不適合 GC 類型的 runtime ,因為跑在 process 裡面的 virtual machine 並不知道底層的記憶體到底是主記憶體還是 SWAP(因為這個理論上應該是作業系統要去管理的),理想上我們希望在作業系統即將把某個區塊 swap out 的時候觸發 GC ,但實際上 process 不會知道這件事情。

ZRAM

ZRAM 的原理是在主記憶體中割一塊空間出來,把它變成一個壓縮的 block device ,然後在上面建 SWAP 。主要適用於沒有建置 SWAP 空間的系統。

ZSWAP

ZSWAP 是基於現有 SWAP 的改良,因此運作的前提是必須有建置 SWAP 空間。ZSWAP 的原理是去 hook kernel 裡面把 memory page swap out 的流程(這個 hook 叫做 FRONTSWAP),攔截要 swap out 的 page,然後嘗試壓縮這個 page ,如果可以壓縮,再使用 ZSWAP 特有的 page allocater ,zbud 或是 zsmalloc ,去 allocate 主記憶體上面的 physical page ,再把壓縮後的 page 放進去。以 zbud 為例,壓縮比例是固定的,2 個 virtual page 會塞進一個 physical page。如果這個 page 壓縮效果不佳,就會循正常的流程 swap out to disk。

zbud 、zsmalloc 和 kernel 裡面其他的 page allocater 的基本運作原理是一樣的,因此不像 ZRAM 是 allocate 一個固定大小的空間來放壓縮後的 page ,而是動態地配置和釋放記憶體。 ZSWAP 所使用的最大主記憶體比例是可以調整的,預設是 20% 。當這個壓縮的記憶體空間快要滿的時候,或是主記憶體耗竭的時候, ZSWAP 就會開始根據 LRU (Least Recently Used) 規則(尋找最久沒有使用的分頁)把已壓縮的 page 解壓縮後 swap out to disk 。

Mac OS X

Mac OS X 應該是主流作業系統裡面最早預設啟用記憶體壓縮的,從 10.9 版就預設啟用。在我的另一臺 Mac 筆電上即使在低可用記憶體的狀況下使用者界面仍然很流暢,我很好奇他們是怎麼做的,就做了研究,它的運作方式如下(這張投影片講得簡單明瞭):

根據研究論文:In lieu of swap: Analyzing compressed RAM in Mac OS X and Linux – ScienceDirect
投影片: pres-in_lieu_of_swap_-_analyzing_compressed_ram_in_mac_os_x_and_linux.pdf

所以看來, Mac OS X 的記憶體壓縮運作方式比較接近 ZSWAP。

KSM – Kernel Same Page Merging

要擠出更多記憶體空間,除了壓縮,另一個方法就是把資料一樣的分頁 (page) 合併只存一份,這就是 KSM 主要的想法。

目前在 mainline kernel 裡面的 KSM 設計似乎主要是給 KVM 虛擬機器使用,因為同時開很多臺一樣作業系統的 VM 顯然會有很多記憶體內容長得一樣,所以設計 KSM。

然而目前 mainline 的 KSM 功能只會去合併由應用程式特別標示「可以合併」的分頁 ,應用程式需要透過 madvise()系統呼叫將 page 標示成 MADV_MERGABLE ,KSM 才會去掃描它的內容。快速查了一下,目前除了 KVM 之外似乎沒有應用程式有使用這個功能標示。

問了一下朋友,他提到 UKSM “Ultra Kernel Samepage Merging" ,是一個第三方 kernel 模組,不像 mainline KSM 有 MADV_MERGABLE 的限制,UKSM 會主動去合併所有程序的記憶體分頁,不需要程序主動標示。

另外還有 PKSM ,說是 UKSM 的改良版,看來已經很久沒有更新了。不過現在 UKSM 的 repo 雖然看起來沒有新功能的開發,但經常會對新版的 Linux kernel rebase 他們的 code ,讓新版的也能使用。

除了上面的各種 KSM ,也有人嘗試直接提 patch 到 mainline ,像這位想讓 KSM 可以不受限於有標示的分頁。不過後來有位 Jann Horn (不確定他是不是 kernel 開發者)指出 KSM 有嚴重的安全性問題,如果讓 KSM 可以合併所有分頁的話那資安問題會變更嚴重,然後就沒有後續討論了。(不太確定為啥,如果 Jann 不是 kernel 開發者的話,那這個 patchset 就沒有被明確拒絕啊,原來 kernel 開發者都可以這樣收 patch 當作沒看到的哦?)(雖然 kernel 開發者沒有義務要回覆送進來的 patch ,但就這樣不明確拒絕人家,竟然是旗艦開源專案 Linux 會發生的事情?我孤陋寡聞了。)

後記一:關於 SWAP 的迷思

這篇快寫完的時候我發現一篇 Facebook 工程師寫的文章在講大家對 SWAP 的迷思。覺得清晰了不少,在這邊我直接翻譯他列的幾個重點,全文非常值得一讀:

  1. SWAP 是運作良好的系統當中合理且重要的元件,沒有它就難以達成良好的記憶體管理。
  2. 大致上來說 swap 並不應該被拿來當作「緊急記憶體」使用, swap 的目的是讓記憶體分頁的釋放更為公平和有效率。而且實際上,如果把 swap 當成緊急記憶體使用的話一般是有害的。
  3. 停用 swap 並不會解決可用記憶體稀缺的狀況下磁碟 IO 過慢而造成的問題(可用記憶體稀缺的時候會造成分頁不斷被逐出主記憶體然後又被載入,也就是 contention);停用 swap 只會使得磁碟頻寬從「被匿名分頁傳輸吃滿」變成「被快取分頁傳輸吃滿」。
    這樣可能比較沒效率,因為這樣我們在主記憶體當中可以取回的分頁的範圍就變小了(有 swap 的話我們可以選擇從主記憶體逐出匿名分頁和快取分頁,沒有 swap 的話我們就只能選擇逐出快取分頁)。更糟的是,因為沒有 swap ,永遠只能選擇逐出快取分頁,這可能反過來成為記憶體稀缺,然後 contention 的主因。
    (這邊對「快取分頁」的定義是「在其他儲存空間上有資料副本」,也就是 “backed by other storage" ;「匿名分頁」的定義是「在其他儲存空間上沒有資料副本,記憶體當中的是唯一一份」。因此,逐出匿名分頁需要先把資料複製到 SWAP,逐出快取分頁只要把這筆快取的參照刪除就可以了。)
  4. swap 模組在 kernel 4.0 以前有很多問題,例如會過度主動地 swap out 分頁,這造成很多人對 swap 的負面印象。 4.0 以後狀況改善很多。
  5. 使用 SSD 的狀況下,swap out (把分頁從主記憶體搬到 swap)匿名分頁,以及,重新取得快取分頁的內容,這兩件事情基本上效能和延遲差不多。使用機械磁碟的狀況下,swap 讀取因為是隨機存取所以會比較慢,所以把 vm.swappiness 調低是合理的。
  6. 停用 swap 並不會防止記憶體幾乎用完 (OOM) 的時候系統完全卡住,雖然,的確,如果有 swap 的話卡住的時間可能會比較長。無論 OOM killer 何時、在有或是沒有 swap 的狀況下開始動作,結果都一樣:系統會進入一個無法預測的狀態,有 swap 或是沒有 swap 都一樣。

原文另外有連結到一個 Facebook 的專案: Pressure Stall Information。 PSI 已經內建於 Linux kernel 4.20+ ,主要功能是提供一個簡單的針對系統資源的壓力測量。不過這部分我還沒有深入研究可以怎麼利用,看來 Facebook 他們是搭配 cgroup2 和 oomd 去分配資源。

後記二:ZSWAP 一個月使用心得

調查過以上這麼多解決方案之後,最後決定啟用 ZSWAP。有人寫了一個管理 ZSWAP / ZRAM 等相關功能的管理工具 systemd-swap ,為了方便管理,我使用這個。但後來發現 Ubuntu 上面裝起來不會動,有個看起來很像的問題回報,開發者說已經解決了但我還是不能用,所以我就乾脆直接手動跑了:

/usr/bin/systemd-swap start

除了他的預設設定,我額外調整了幾個參數:

  • swappiness 調整為 100:根據後記一提到的那篇文章,swappiness 其實是一個成本的比值,比較匿名分頁和快取分頁逐出再載入的成本。 swappiness 越高表示匿名分頁逐出再載入的成本和快取分頁相近,越低則表示匿名分頁逐出再載入的成本高於快取分頁。因此通常 SSD 的系統可以直接設定 100 (成本相等),機械硬碟的系統就要設低一點。
  • zswap_max_pool_percent 調整為 30 (預設是 25):主記憶體有百分之多少可以被拿來存壓縮的分頁。我有 16GB 的記憶體,想說開大一點看他可以用到多少。

接下來我使用了一個多月,感到系統順暢度有明顯改善:

  • 平常如果沒有注意記憶體用量,經常會不小心開一個很吃記憶體的應用程式,主記憶體不夠用,這時候如果沒有 ZSWAP,圖形界面就會直接卡住沒辦法操作,現在有 ZSWAP,圖形界面就只會頓頓的而已,勉強還可以操作。
  • 偶爾檢查記憶體用量,發現 ZSWAP 的壓縮倍率通常都在 2~2.5
    • zswap 的相關資訊都在 /sys/kernel/debug/zswap
    • pool_total_size 是 zswap 在主記憶體當中壓縮後實際佔用的大小
    • stored_pages 是在 zswap 壓縮區當中已存的分頁數量,Linux 預設每個分頁是 4096 bytes
    • 計算壓縮率:
# cd /sys/kernel/debug/zswap
# perl -E "say $(cat stored_pages) * 4096 / $(cat pool_total_size)"

不過低記憶體用量的時候圖形界面還是會頓,這我就不是很確定是什麼問題,可能是核心排程,可能是記憶體管理。之後再發一篇記錄一下偵錯的過程。

後記三: Xpra

為了要在 Linux 桌面上並行執行更多應用程式,除了本文原本的主題,設法擠出記憶體之外,我還嘗試了另一個方法,就是直接把應用程式丟到遠端伺服器跑,然後透過網路回傳畫面。 Xpra 可以讓你方便做到這件事情,之後我再寫另一篇文章記錄。

相關閱讀

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s