為什么 Linux 需要 Swapping 你知道嗎?
為什么這么設計(Why’s THE Design)是一系列關于計算機領域中程序設計決策的文章,我們在這個系列的每一篇文章中都會提出一個具體的問題并從不同的角度討論這種設計的優缺點、對具體實現造成的影響。
對 Linux 稍有了解的人都知道,Linux 會將物理的隨機讀取內存(Random Access Memory、RAM)按頁分割成 4KB 大小的內存塊,而今天要介紹的 Swapping 機制就與內存息息相關,它是操作系統將物理內存頁中的內容拷貝到硬盤上交換空間(Swap Space)以釋放內存的過程,物理內存和硬盤上的交換分區組成了操作系統上可用的虛擬內存,而這些交換空間都是系統管理員預先配置好的[^1]。
圖 1 - Linux Swapping
正是因為 Linux 上的所有進程都會通過虛擬內存這一層抽象間接與物理內存打交道,而 Swapping 也充分利用了該特性,它能夠讓應用程序看到操作系統內存充足的假象,然而并不知道它使用的部分虛擬內存其實在磁盤上,因為內存和磁盤的讀寫速度上的巨大差異,這部分虛擬內存的讀寫非常緩慢,我們在 為什么 CPU 訪問硬盤很慢 曾經介紹過:
在 SSD 中隨機訪問 4KB 數據所需要的時間是訪問主存的 1,500 倍,機械磁盤的尋道時間是訪問主存的 100,000 倍[^2]
如此巨大的性能差異使得觸發 Swapping 的進程可能會遇到性能損失,同一個頁面的頻繁換入換出會導致極其明顯的性能抖動,如果沒有相應的背景知識,遇到類似的問題可能會很難查到根本原因,例如 MySQL 在錯誤配置 NUMA 時就會出現內存頁頻繁換入換出,影響服務質量的問題。
Linux 提供了兩種不同的方法啟用 Swapping,分別是 Swap 分區(Swap Partition)和 Swap 文件(Swapfile):
- Swap 分區是硬盤上的獨立區域,該區域只會用于交換分區,其他的文件不能存儲在該區域上,我們可以使用 swapon -s 命令查看當前系統上的交換分區;
- Swap 文件是文件系統中的特殊文件,它與文件系統中的其他文件也沒有太多的區別;
Swap 分區的大小是需要系統管理員手動設定的,然而不同的場景最好設置不同交換分區大小,例如:桌面系統的交換分區大小可以是系統內存的兩倍,這可以讓我們同時運行更多的應用程序;服務器的交換分區應該關閉或者使用少量的交換分區,不過一旦啟用交換分區,就應該引入監控監控應用程序的性能。
我們到現在已經對 Linux 上的 Swapping 有了一定的了解,接下來回到這篇文章想要討論的問題 — 『為什么 Linux 需要 Swapping』,我們將從以下兩個方面介紹 Swapping 解決的問題、觸發入口和執行路徑:
Swapping 可以直接將進程中使用相對較少的頁面換出內存,立刻給正在執行的進程分配內存;
Swapping 可以將進程中的閑置頁面換出內存,為其他進程未來使用內存做好準備;
內存不足
當系統需要的內存超過了可用的物理內存時,內核會將內存中不常使用的內存頁交換到磁盤上為當前進程讓出內存,保證正在執行的進程的可用性,這個內存回收的過程是強制的直接內存回收(Direct Page Reclaim)。
圖 2 - 直接內存回收
直接內存回收是在 Linux 調用 __alloc_pages_nodemask 申請新內存頁時觸發的,該函數會先在空閑頁列表中查找是否有可用的頁面,如果不存在可用頁面,就會進入 __alloc_pages_slowpath 函數分配內存頁,與從空閑列表中直接查找內存也相比,該函數會通過以下步驟分配內存:
- static inline struct page * __alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order, struct alloc_context *ac) {
- ...
- if (alloc_flags & ALLOC_KSWAPD)
- wake_all_kswapds(order, gfp_mask, ac);
- page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
- if (page) goto got_pg;
- if (can_direct_reclaim && (costly_order || (order > 0 && ac->migratetype != MIGRATE_MOVABLE)) && !gfp_pfmemalloc_allowed(gfp_mask)) {
- page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac, INIT_COMPACT_PRIORITY, &compact_result);
- if (page) goto got_pg;
- ...
- }
- retry:
- page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac, &did_some_progress);
- page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac, compact_priority, &compact_result);
- page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
- got_pg:
- return page;
- }
- 喚醒 kswapd 線程在后臺回收內存并嘗試調用 get_page_from_freelist 從空閑列表中快速獲取內存頁;
- 昂貴的內存申請會先調用 __alloc_pages_direct_compact 嘗試壓縮內存頁,并在壓縮后的內存中調用 get_page_from_freelist 查找空閑的內存頁;
- 調用 __alloc_pages_direct_reclaim 直接回收并分配新的內存頁;
- 再次調用 __alloc_pages_direct_compact 嘗試壓縮內存并獲取空閑內存頁;
- 調用 __alloc_pages_may_oom 分配內存,如果內存分配失敗會觸發內存不足警告隨機殺死操作系統上的幾個進程;
雖然獲取內存頁的步驟已經經過了大量的刪減,但是其中展示了 Linux 在內存也不足時獲取內存的幾個常見方法:內存壓縮、直接回收以及觸發內存不足錯誤殺掉部分進程。
內存閑置
應用程序在啟動階段使用的大量內存在啟動后往往都不會使用,通過后臺運行的守護進程,我們可以將這部分只使用一次的內存交換到磁盤上為其他內存的申請預留空間。kswapd 是 Linux 負責頁面置換(Page replacement)的守護進程,它也是負責交換閑置內存的主要進程,它會在空閑內存低于一定水位時,回收內存頁中的空閑內存保證系統中的其他進程可以盡快獲得申請的內存,如下圖所示:
圖 3 - Linux 空閑頁面水位
當空閑頁面小于 WMARK_LOW 時,kswapd 進程才會開始工作,它會將內存頁交換到磁盤上直到空閑頁面的水位回到 WMARK_HIGH,不過當空閑頁面的水位低于 WMARK_MIN 時會觸發上一節提到的內存直接回收,而水位高于 WMARK_HIGH 則意味著空閑內存充足,不需要進行回收。
Linux 操作系統采用最近最少使用(Least Recently Used、LRU)算法置換內存中的頁面,系統中的每個區都會在內存中持有 active_list 和 inactive_list 兩種鏈表,其中前者包含活躍的內存頁,后者中存儲的內存頁都是回收的候選頁面,除此之外,Linux 還會在將 lru_list 根據內存頁的特性分成如下幾種:
- enum lru_list {
- LRU_INACTIVE_ANON = LRU_BASE,
- LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
- LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
- LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
- LRU_UNEVICTABLE,
- NR_LRU_LISTS
- };
其中包含 ANON 的表示匿名內存頁,這些內存頁存儲了與文件無關的進程堆棧等內容,而包含 FILE 的表示與文件相關的內存,也就是程序文件或者數據對應的內存,而最后的 LRU_UNEVICTABLE 表示禁止回收的內存頁。
圖 4 - 活躍鏈表和不活躍鏈表
每當內存頁被訪問時,Linux 都會將被訪問的內存頁移到鏈表的頭部,所以在活躍鏈表末尾的是鏈表中『最老的』內存頁,守護進程 kswapd 的作用是平衡兩個鏈表的長度,將活躍鏈表末尾的內存頁移至不活躍鏈表的隊首等待回收,而函數 shrink_zones 會負責回收 LRU 鏈表中的不活躍內存頁。
總結
很多人認為當系統內存不足時應該立即觸發內存不足(Out of memory、OOM)并殺掉進程,但是 Swapping 其實為系統管理員提供了另外一種選擇,利用磁盤的交換空間避免程序被直接退出,以降低服務質量的代價換取服務的部分可用性。Linux 中的 Swapping 機制主要是為內存不足和內存閑置兩種常見的情況存在的
- Swapping 可以直接將進程中使用相對較少的頁面換出內存:當系統需要的內存超過了可用的物理內存時,內核會將內存中不常使用的內存頁交換到磁盤上為當前進程讓出內存,保證正在執行的進程的可用性;
- Swapping 可以將進程中的閑置頁面換出內存:應用程序在啟動階段使用的大量內存在啟動后往往都不會使用,通過后臺運行的守護進程,我們可以將這部分只使用一次的內存交換到磁盤上為其他內存申請預留空間;
關于是否應該開啟 Swapping 的討論其實非常多,我們在今天也不應該一刀切地認為必須開啟或者禁用 Swapping,我們仍然需要分析場景并利用好 Linux 為我們提供的這一機制,例如 Kubernetes 要求禁用 Swapping,我們就應該遵循社區提出的建議,在部署 Kubernetes 的機器上關閉這一特性[^3]。到最后,我們還是來看一些比較開放的相關問題,有興趣的讀者可以仔細思考一下下面的問題:
- Linux 提供了哪些參數來控制 Swapping 的行為?
- 通過降低服務質量的代價換取部分可用在哪些場景下是可取的?
如果對文章中的內容有疑問或者想要了解更多軟件工程上一些設計決策背后的原因,可以在博客下面留言,作者會及時回復本文相關的疑問并選擇其中合適的主題作為后續的內容。
參考資料
Kubelet/Kubernetes should work with Swap Enabled #53533 https://github.com/kubernetes/kubernetes/issues/53533
Linux Performance: Why You Should Almost Always Add Swap Space https://haydenjames.io/linux-performance-almost-always-add-swap-space/
Do we really need swap on modern systems? https://www.redhat.com/en/blog/do-we-really-need-swap-modern-systems
本文轉載自微信公眾號「真沒什么邏輯」,可以通過以下二維碼關注。轉載本文請聯系真沒什么邏輯公眾號。