一、SWAP 不是“垃圾站”,而是“冷藏庫”
很多運維把 SWAP 當成“內存垃圾站”,認為它只是物理內存不足時的臨時傾倒場。實際上,SWAP 更接近“冷藏庫”:內核會把“最近很少訪問”的匿名頁(堆、棧、共享內存)壓縮后搬到磁盤,讓出物理內存給更急需的進程。搬出過程稱為換出(swap-out),再次訪問時觸發缺頁異常,把頁從磁盤讀回內存,稱為換入(swap-in)。換出/換入本身不致命,但伴隨隨機磁盤 I/O,延遲從納秒級驟升到毫秒級,導致“物理內存有空閑,系統卻很卡”的詭異現象。理解“冷藏庫”本質,就能明白:SWAP 使用率高的根源,不一定是“內存不夠用”,也可能是“內核覺得這些頁不值得留在內存”。
二、內存水位與回收閾值:什么時候才往“冷藏庫”搬貨
內核通過水位線(watermark)把物理內存劃分為三個區域:HIGH、LOW、MIN。當剩余內存跌到 LOW 水位,后臺回收線程 kswapd 被喚醒,嘗試釋放頁緩存、匿名頁、 slab 緩存;若繼續跌到 MIN 水位,會觸發“直接回收”,申請內存的進程自己進入回收邏輯,此時再分配不到頁,就會選擇“換出”匿名頁。換句話說:SWAP 的寫入,不一定等到“內存耗盡”,只要“水位低于預期”就可能發生。于是出現“8 GB 空閑,卻瘋狂換出”的怪象——也許是水位線被手動調低,也許是某進程瞬間申請大塊內存,把水位瞬間砸穿。
三、匿名頁、頁緩存、Slab:誰最容易被“冷藏”
匿名頁(AnonPages)沒有文件后端,換出只能進 SWAP;頁緩存(PageCache)有文件后端,回收時直接丟棄,下次讀文件再加載,無需 SWAP;Slab 是內核數據結構緩存,回收代價小。因此,SWAP 里“住”的絕大部分是匿名頁。若應用大量使用 malloc/new、創建大數組、使用匿名共享內存,就等于給“冷藏庫”持續供貨。反觀 PageCache,除非手動 drop,否則內核更傾向于丟棄它而非搬進 SWAP。定位 SWAP 暴漲時,第一步就是區分“匿名頁激增”還是“PageCache 被擠占”,前者聚焦應用內存泄漏,后者關注文件讀寫模式。
四、工具鏈:從 top、vmstat 到 /proc/meminfo 的“破案路線”
top 的 RES 代表進程常駐內存,SWAP 列顯示該進程被換出的量;vmstat 的 si/so 每秒換入/換出頁數,是“實時冷藏速度”晴雨表;/proc/meminfo 的 AnonPages、SwapCached、SlabReclaimable 給出全局分布。若 si/so 持續大于 200 頁/秒,且 AnonPages 不斷縮小,SwapCached 增加,即可判定“正在積極換出”。進一步用 `perf top -p $PID` 觀察缺頁異常占比,若 `page_fault` 高居榜首,說明進程頻繁訪問已被換出的內存,觸發“換入”風暴,導致 CPU 空轉在磁盤 I/O 上。
五、缺頁異常與“換入”風暴:為什么物理內存有空閑,系統卻很卡
進程訪問被換出的頁,會觸發 major page fault,內核同步讀磁盤,把頁重新搬進內存。若應用訪問模式隨機(如大數組遍歷),且 SWAP 分布在機械盤不同扇區,就會產生大量隨機讀,磁盤 seek 時間疊加,導致“CPU 利用率低、load 高、響應慢”的怪相。SSD 能緩解 seek,卻無法避免內核鎖競爭:換入過程需要加鎖修改頁表,多線程并發缺頁時,鎖爭用會把延遲再次放大。因此,SWAP 使用率只是“表面溫度”,缺頁異常率才是“真正熱度”。
六、應用層泄漏:內存泄漏的“SWAP 化”表現
Java 應用出現內存泄漏,Old 區持續增長,GC 無法釋放,最終觸發 `OutOfMemoryError`;但在到達 OOM 之前,內核會把 Old 區的匿名頁陸續換出,于是 SWAP 使用率一路飆升,GC 停頓時間也變長——因為每次 Major GC 都要把換出的頁重新讀回,再掃描標記。C/C++ 應用若忘記 free,匿名頁同樣永不回收;Python、Node.js 的對象循環引用,也會讓堆區膨脹。定位思路:
- Java:jstat 觀察 Old 區增長曲線,jmap dump 后 MAT 分析 GC Root;
- C/C++:Valgrind 或 AddressSanitizer 跑壓測,找出未釋放塊;
- Python:tracemalloc 統計內存增量,檢查 __del__ 循環引用。
解決泄漏后,SWAP 使用率往往“斷崖式”下降,驗證“匿名頁激增”才是元兇。
七、內核調優:水位線、swappiness、oom_score 的“三角平衡”
swappiness=60 是默認妥協值:0 表示“除非內存耗盡,否則不 SWAP”;100 表示“積極 SWAP”。調低 swappiness 能減少換出,但可能導致“內存耗盡時直接 OOM”;調高則讓 SWAP 提前參與,換取更大文件緩存。經驗法則:
- 數據庫、緩存、消息隊列:swappiness=10,盡量保留內存給緩存;
- 計算密集型、短期任務:swappiness=60,默認即可;
- 嵌入式、小內存:swappiness=100,充分利用 SWAP,避免 OOM 殺死關鍵任務。
再配合 `/proc/$PID/oom_score_adj`,讓不重要進程更容易被 OOM Kill,減少“同歸于盡”概率。三角平衡的核心是:讓 SWAP 成為“有選擇”的冷藏,而非“隨機”的傾倒。
八、應用改造:減少匿名頁的四把“手術刀”
1. 對象池:避免頻繁 malloc/new,把短生命周期對象變成長生命周期池,減少匿名頁波動;
2. 堆外內存:Java 的 DirectByteBuffer、C 的 mmap,把內存映射到文件,而非匿名頁,換出時直接丟棄,無需 SWAP;
3. 壓縮算法:啟用 zswap,在內存里先壓縮匿名頁,再決定是否搬去磁盤,可減少 30~50% 換出量;
4. 本地緩存轉遠程:把本地大緩存搬到 Redis 等外部節點,讓“高頻但可重建”的數據離開匿名頁,既減少 SWAP,也降低單點內存壓力。
四把手術刀并非“一刀切”,而是根據業務特性“能池化就池化,能映射就映射,能壓縮就壓縮,能外遷就外遷”。
九、監控與告警:把“冷藏庫”變成“透明冷庫”
SWAP 使用率≠絕對指標,需要組合監控:
- 匿名頁占比:超過 60% 且持續增長,預示泄漏;
- 缺頁異常速率:major fault > 100/s 持續 5min,預示“換入”風暴;
- load 與 CPU 利用率背離:CPU 低、load 高,典型換入阻塞;
- 進程 swap 量:/proc/$PID/status 的 VmSwap 字段,單進程超過 500MB 即告警。
把以上指標接入時序數據庫,配合“預測型”告警(如“匿名頁七天同比增長 30%”),能在“真正卡死”前提前介入,把“事后救火”變成“事前干預”。
十、真實案例:一次“8 GB 空閑,SWAP 7 GB”的追兇筆記
生產環境 32 GB 物理內存,剩余 8 GB,SWAP 卻占用 7 GB,應用響應極慢。top 顯示某 Java 進程 RES 僅 6 GB,但 /proc/$PID/status 里 VmSwap 高達 5 GB。jstat 發現 Old 區占用 90%,Major GC 每秒一次。進一步用 jmap 看堆 histogram,發現某緩存類實例 600 萬條,占 12 GB。結論:緩存泄漏 → Old 區暴漲 → 內核換出 → Major GC 觸發缺頁異常 → 隨機讀磁盤 → load 飆高。解決:縮小緩存 TTL,把 12 GB 降到 3 GB,SWAP 瞬間釋放 5 GB,響應時間恢復。案例印證:SWAP 高只是“癥狀”,匿名頁泄漏才是“病根”,調優 swappiness 治標不治本,減少匿名頁才是核心。
十一、常見誤區:這些“急救動作”可能越救越忙
- 盲目 drop_caches:echo 3 > drop_caches 只能釋放 PageCache,對匿名頁無效,反而讓文件讀請求直接落盤,增加 IO 壓力;
- 直接關閉 SWAP:swapoff -a 會把換出的頁全部讀回內存,若匿名頁高達數十 GB,會導致幾分鐘“假死”,甚至觸發 OOM;
- 一味調低 swappiness:從 60 調到 10,看似減少換出,但內存耗盡時 OOM 殺手隨機掃射,可能把數據庫進程殺死,代價更大;
- 加大物理內存卻不改應用:內存泄漏速度隨內存增大而加快,最終 SWAP 依舊爆滿,只是時間延后。
正確順序:先定位匿名頁來源 → 解決泄漏/壓縮/外遷 → 再調 swappiness → 最后才考慮加內存或關 SWAP。
十二、未來趨勢:從“磁盤 SWAP”到“內存壓縮”與“分布式換頁”
Linux 5.x 引入 zswap、zram,把匿名頁壓縮后留在內存,而非搬到磁盤,I/O 延遲從毫秒級降到微秒級;云原生場景下,出現“分布式換頁”——把冷頁壓縮后放到遠端 NVMeoF 存儲,節點本地幾乎無 SWAP,卻擁有百倍于磁盤的換頁帶寬。技術演進并未改變“換出”本質,只是讓“冷藏庫”離 CPU 更近。掌握傳統 SWAP 調優思路,才能在新技術來臨時,迅速理解“壓縮率 vs. CPU”、“遠端帶寬 vs. 本地內存”的權衡邏輯。
SWAP 使用率過高,從來不是“磁盤不夠大”,而是“內存用法不對”。理解水位線、匿名頁、缺頁異常、回收策略,才能把“冷藏庫”變成“可控冷庫”:該冷藏的冷藏,該丟棄的丟棄,該外遷的外遷。下一次監控大屏跳出 SWAP 90% 時,你不再匆忙重啟,而是氣定神閑地打開工具鏈,沿著“水位→匿名頁→缺頁→泄漏”的路線,一步步找到那個貪吃的進程,然后優雅地掐掉它的“內存水龍頭”。愿你的系統,再也不會被“隱形內存”拖垮,而是讓每一份物理內存都用在真正有價值的計算上。