你在監控生產環境伺服器時,明明看到還有數 GB 的空閒 RAM,應用程式卻突然因記憶體不足錯誤而崩潰,或出現嚴重的延遲尖峰。資料的持續分配與釋放,會隨著時間推移逐步破壞連續記憶體區塊,把它們切分成許多細小且難以利用的碎片。

工程師將這種現象稱為記憶體碎片化。未使用的位元組會散落在系統位址空間中,即使總可用記憶體仍然很高,Linux 核心也可能無法滿足較大的記憶體申請。

記憶體碎片化的運作機制

要理解伺服器記憶體是如何逐步「劣化」的,可以從兩個核心過程著手:內部分配效率以及核心空間管理。

內部碎片與外部碎片

記憶體分配問題通常分為兩類:

碎片類型產生方式系統影響
內部碎片你分配了記憶體區塊,但實際資料沒有填滿整個空間。區塊內多餘的空閒空間會被閒置,無法使用。
外部碎片你長時間持續地分配與釋放動態物件。總空閒記憶體依然很多,但空間被不可用的小間隙分割開來。

內部碎片浪費的是已分配區塊內部的位元組;外部碎片則會把空閒 RAM 打散成許多微小間隙,進而妨礙較大記憶體區塊的分配。

長期快取中的堆碎片

長時間運行的應用程式會在記憶體快取中存放不同大小的物件,以實現快速存取。你可能會將使用者工作階段、動態資料庫查詢結果,或 API 回應存放在 RAM 中。

應用程式會不斷建立和刪除這些大小不一的快取物件。這樣的持續循環會把堆空間切割成許多零散的小型空閒區域。經過數週甚至數月後,即使系統中仍然有數 GB 可用 RAM,程式也可能無法為新的大型物件找到一塊連續空間。

Linux 核心頁面分配器的機制

當應用程式請求記憶體時,夥伴分配器(buddy allocator)會依照特定流程運作:

  1. 檢查目標階數空閒鏈結串列:先在與所需大小完全匹配的 order 空閒鏈結串列中查找。若存在合適區塊,系統會立即完成分配。
  2. 檢查更高階鏈結串列:如果目標 order 鏈結串列為空,就繼續查找更大的空閒區塊鏈結串列。
  3. 遞迴拆分:將較大的區塊拆分為兩個大小相等的「夥伴區塊(buddies)」。核心使用其中一半滿足請求,並將剩餘一半放回較低 order 的空閒鏈結串列。

高負載伺服器會不斷拆分這些記憶體區塊,隨著運行時間增長,記憶體碎片化會進一步加劇。

記憶體碎片化對伺服器穩定性的影響

嚴重的記憶體碎片化會在不知不覺中持續削弱系統效能。你也許會看到總空閒 RAM 依然很高,但伺服器實際上已經出現隱藏的效能瓶頸與運行不穩定問題。

分配變慢與 RAM 浪費

當記憶體被分裂成大量細小且不連續的區塊時,伺服器就會失去高效率。Linux 核心必須掃描多個空閒鏈結串列,才能為新行程找到合適的區塊。這種額外搜尋會延長分配時間,並提高應用程式延遲。

關鍵結論:已占用頁面之間的不可用間隙會形成「擱淺容量」。系統無法把這些分散的小間隙重新拼接起來滿足較大的分配請求,因此可實際利用的 RAM 容量會顯著下降。

下表展示了記憶體碎片化如何影響分配速度與 RAM 利用效率:

狀態分配搜尋時間可用容量整體伺服器效率
連續記憶體快(直接命中 Buddy 鏈結串列)高(95–99%)最佳
輕度碎片化中等(需掃描多個鏈結串列)中等(70–85%)可接受
嚴重碎片化慢(深度遍歷)低(低於 50%)較差

核心記憶體壓縮引發的延遲尖峰

當夥伴分配器無法找到連續空閒頁面時,Linux 核心會觸發直接記憶體壓縮(direct memory compaction)。核心會嘗試在實體 RAM 中移動已分配頁面,以整理出更大的連續記憶體區塊。

[ 已分配頁面 ] [ 空閒空間 ] [ 已分配頁面 ] [ 空閒空間 ]
       │                                │
       ▼(核心壓縮過程)               ▼
[ 已分配頁面 ] [ 已分配頁面 ] [ 空閒空間 ] [ 空閒空間 ]

這一壓縮過程會消耗大量 CPU 資源,並干擾正常運作:

  • 頁面回收:核心掃描活躍記憶體,尋找可遷移頁面。
  • 同步暫停:在移動頁面時,核心會暫停你的應用程式執行緒。
  • 快取失效:頻繁的頁面移動會沖刷 CPU 快取並降低存取速度。

這些核心行為會在生產環境應用中造成嚴重的尾端延遲尖峰。你的 Web 服務可能會在激烈壓縮週期中,從原本毫秒級回應陡然上升到秒級。

低餘裕系統中的 OOM 崩潰

在記憶體餘裕有限的系統中,嚴重碎片化可能引發災難性故障。你可能在監控面板中看到伺服器仍有 30% 甚至更多空閒 RAM,但應用程式卻突然因 Out-Of-Memory(OOM)異常而崩潰。

這是因為應用程式請求了一個高階連續區塊,例如 order-3(32KB)或 order-4(64KB)頁框。此時核心會嘗試直接壓縮,但活躍且被固定(pinned)的區塊會阻止頁面遷移。

如果壓縮仍然無法整理出連續空間,Linux 核心就會觸發 OOM killer。OOM killer 會選擇並終止關鍵應用程式行程以回收系統資源,從而導致毫無預警的服務中斷。

在生產環境中診斷記憶體碎片化

你必須在應用程式崩潰之前識別伺服器問題。Linux 原生命令可以幫助你檢查實體記憶體配置,並即時偵測記憶體碎片化。

檢查 Proc Buddyinfo 與 Meminfo

你可以讀取 /proc/buddyinfo 檔案來查看可用的連續頁面區塊。該檔案會顯示各個記憶體區域中每個 order 的空閒區塊數量。如果低階數量很多而高階為 0,就代表實體記憶體已經出現嚴重碎片化。

cat /proc/buddyinfo

接著,查看 /proc/meminfo 以檢查總容量。重點關注 MemAvailableSUnreclaim。如果空閒 RAM 與可實際使用空間之間存在較大差距,就表示分散的記憶體間隙正在阻礙分配請求。

監控頁面分配失敗

Linux 核心會直接將分配問題記錄到系統日誌中。你可以使用 dmesg 指令搜尋 page allocation failure 警告:

dmesg -T | grep -i "page allocation failure"

主動提示:這些日誌項會顯示失敗的具體分配 order。它們能夠證明:即使總 RAM 看起來充足,系統也缺乏足夠的連續記憶體區塊。

透過 Vmstat 和 Sar 追蹤系統趨勢

你可以透過追蹤核心記憶體指標來監控長期系統健康狀況。/proc/vmstat 檔案提供了一系列計數器統計資訊,可用於識別嚴重的壓縮活動與分配延遲。

你應重點監控以下 vmstat 指標,以發現不斷加劇的效能趨勢:

指標名稱作用 / 追蹤活動
compact_stall記錄行程因直接壓縮而被阻塞的次數。
pgscan_direct統計行程在直接回收過程中同步掃描的頁面數量。
pgsteal_direct統計透過直接回收成功釋放的頁面數量。
allocstall_normal統計發生在 Normal 記憶體區域中的直接回收進入次數。
allocstall_dma32統計發生在 DMA32 記憶體區域中的直接回收進入次數。
allocstall_dma統計發生在 DMA 記憶體區域中的直接回收進入次數。
allocstall_movable統計發生在 Movable 記憶體區域中的直接回收進入次數。

如果這些計數器持續上升,就表示核心正在為湊出空閒頁面而付出過高代價。

緩解與記憶體管理策略

你可以主動管理伺服器記憶體,以防止嚴重的效能退化。無論是在核心層還是應用層,採用有效的緩解手段都能幫助系統維持長期可用性。

調校核心壓縮參數

Linux 核心在 /proc/sys/vm/ 下提供了多個設定項,用於管理頁面配置效率。你可以在不重新啟動生產節點的前提下即時調整這些參數。

# 查看當前核心壓縮設定
sysctl vm.compaction_proactiveness vm.extfrag_threshold vm.min_free_kbytes

你可以透過以下三個主要參數來優化核心背景行為:

  • vm.compaction_proactiveness:此參數控制核心在分配請求失敗前,會多積極地預先整理實體頁面。提高 vm.compaction_proactiveness 的值,會促使核心更主動地進行記憶體壓縮,以維持連續記憶體可用性。不過,由於壓縮本身會帶來執行期開銷,更高的設定也會增加系統負擔;而將其設為 0 則表示完全關閉主動壓縮。系統中會有一個專門的 kcompactd 執行緒在運行期間持續工作,活躍時最多可能占用單個 CPU 核心的 100%。
  • vm.extfrag_threshold:此參數決定核心何時從頁面回收切換到直接壓縮。閾值越高,越傾向於直接嘗試分配頁面;閾值越低,則越早觸發壓縮。
  • vm.min_free_kbytes:提高此值會迫使核心保留更大的空閒頁面儲備。這樣的儲備可在突發記憶體使用高峰時,為夥伴分配器提供緊急餘裕。

設定建議:對於延遲敏感型資料庫節點,可將 vm.compaction_proactiveness 設為 20 到 50 的中等值。這樣既能降低碎片化風險,也能避免 kcompactd 背景執行緒在業務高峰期獨占整個 CPU 核心。

使用自訂記憶體分配器

預設的系統分配函式庫(如標準 glibc malloc)在面對高頻率、長時間運行的分配模式時,往往容易產生碎片化。你可以使用更現代、更偏向效能優化的分配器來取代預設實作。

分配器名稱主要設計策略關鍵架構優勢
glibc malloc通用型堆管理標準預設實作,但長期運行後容易出現較嚴重的記憶體碎片化。
jemalloc基於 arena 的分配機制與執行緒快取可減少分散分配,並將不再需要的頁面歸還給核心。
tcmalloc執行緒快取型 malloc利用執行緒本地快取減少鎖競爭,並保持更均勻的記憶體區塊大小。

這些專用分配器會將記憶體劃分為固定大小的 bin,並為不同 CPU 執行緒分配獨立 arena。你可以透過 LD_PRELOAD 環境變數,輕鬆將 jemalloctcmalloc 整合到應用程式中:

# 使用 jemalloc 作為底層分配器啟動應用程式
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 ./your_server_binary

應用層記憶體池實務

你可以透過在應用程式程式碼中建立自訂記憶體池,消除動態堆分配的額外開銷。記憶體池允許服務重複使用固定大小的槽位,而不是反覆呼叫動態核心分配。

+--------------------------------------------------------+
|                        記憶體池                        |
|  +--------------+  +--------------+  +--------------+  |
|  | 槽位 1(空閒)|  | 槽位 2(占用)|  | 槽位 3(空閒)|  |
|  +--------------+  +--------------+  +--------------+  |
+--------------------------------------------------------+

你可以採用以下關鍵應用模式,以維持穩定一致的執行效能:

  1. 物件重用池:在伺服器初始化階段預先分配大型陣列緩衝區。將可變資料寫入預分配槽位,並在工作執行緒處理完成後把槽位歸還池中。
  2. Slab 分配器:將小型、同構的資料結構組織到統一且連續的區塊中,即 slab。應用程式可以整塊釋放 slab,從而避免零散頁面間隙。
  3. 環形緩衝區:對於串流式工作負載或傳入 socket 封包,使用固定大小的循環緩衝區。環形緩衝區可以讓記憶體使用保持可預測且有上限。

使用記憶體池能夠讓伺服器位址空間保持穩定,並減少在持續數月運行期間不可預測的核心操作。

若放任不管,記憶體碎片化會悄然侵蝕伺服器的可用性、系統吞吐能力與運行可靠性。你必須主動防範這種隱蔽的故障模式。

你可以透過三項關鍵實務來保護系統:

  • 使用 Linux 原生診斷工具持續追蹤記憶體連續性。
  • 細緻調校核心壓縮參數,在背景整理開銷與業務效能之間取得平衡。
  • 針對高頻分配型工作負載,部署如 jemalloc 或 tcmalloc 之類的專用分配器。

常見問題

如何快速檢測生產環境伺服器上的記憶體碎片化?

你可以使用 Linux 原生命令檢查 /proc/buddyinfo 檔案,重點關注高階記憶體列表中的數值是否偏小。或者,也可以透過 dmesg 監控系統日誌中的 page allocation failure 警告。這些指標都能迅速確認實體位址空間是否已經發生碎片化。

重新啟動伺服器能否徹底解決記憶體碎片化?

關鍵事實:重新啟動會完全清空系統 RAM,並重設實體頁面分配狀態。

然而,重新啟動會帶來不必要的服務中斷。你更應該調校核心壓縮參數,或部署像 jemalloc 這樣的現代分配器,以避免在不重新啟動生產節點的情況下反覆出現碎片化問題。

為什麼在 RAM 仍有剩餘時,OOM killer 仍然會觸發?

當 Linux 核心無法為高階記憶體請求找到足夠的連續實體頁面時,OOM killer 就會觸發。嚴重的外部碎片化會把位址空間切割成許多孤立的小間隙。一旦直接壓縮無法將這些間隙整合成連續空間,核心就會在總空閒 RAM 依然較高的情況下終止行程。

jemalloc 與 glibc malloc 在運行層面的主要差異是什麼?

標準 glibc malloc 以全域方式管理堆空間,長時間運行後更容易造成嚴重碎片化。相比之下,jemalloc 使用執行緒專屬 arena 與固定大小 bin。這種專門設計的架構能夠減少分散分配,並更有效率地將不再需要的記憶體頁面歸還給核心。