天天看點

Linux 為什麼系統的Swap變高了(上)

Linux 為什麼系統的Swap變高了(上)

上一節,我通過一個斐波那契數列的案例,帶你學習了記憶體洩漏的分析。如果在程式中直接或間接地配置設定了動态記憶體,你一定要記得釋放掉它們,否則就會導緻記憶體洩漏,嚴重時甚至會耗盡系統記憶體。

不過,反過來講,當發生了記憶體洩漏時,或者運作了大記憶體的應用程式,導緻系統的記憶體資源緊張時,系統又會如何應對呢?

在記憶體基礎篇我們已經學過,這其實會導緻兩種可能結果,記憶體回收和 OOM 殺死程序。 

我們先來看後一個可能結果,記憶體資源緊張導緻的 OOM(Out Of Memory),相對容易了解,指的是系統殺死占用大量記憶體的程序,釋放這些記憶體,再配置設定給其他更需要的程序。

這一點我們前面詳細講過,這裡就不再重複了。接下來再看第一個可能的結果,記憶體回收,也就是系統釋放掉可以回收的記憶體,比如我前面講過的緩存和緩沖區,就屬于可回收記憶體。它們在記憶體管理中,通常被叫做檔案頁(File-backed Page)。 

大部分檔案頁,都可以直接回收,以後有需要時,再從磁盤重新讀取就可以了。而那些被應用程式修改過,并且暫時還沒寫入磁盤的資料(也就是髒頁),就得先寫入磁盤,然後才能進行記憶體釋放。 

這些髒頁,一般可以通過兩種方式寫入磁盤。

(1)可以在應用程式中,通過系統調用 fsync  ,把髒頁同步到磁盤中;

(2)也可以交給系統,由核心線程 pdflush 負責這些髒頁的重新整理。 

除了緩存和緩沖區,通過記憶體映射擷取的檔案映射頁,也是一種常見的檔案頁。它也可以被釋放掉,下次再通路的時候,從檔案重新讀取。

除了檔案頁外,還有沒有其他的記憶體可以回收呢?比如,應用程式動态配置設定的堆記憶體,也就是我們在記憶體管理中說到的匿名頁(Anonymous Page),是不是也可以回收呢? 

我想,你肯定會說,它們很可能還要再次被通路啊,當然不能直接回收了。非常正确,這些記憶體自然不能直接釋放。

但是,如果這些記憶體在配置設定後很少被通路,似乎也是一種資源浪費。是不是可以把它們暫時先存在磁盤裡,釋放記憶體給其他更需要的程序? 

其實,這正是 Linux 的 Swap 機制。Swap 把這些不常通路的記憶體先寫到磁盤中,然後釋放這些記憶體,給其他更需要的程序使用。再次通路這些記憶體時,重新從磁盤讀入記憶體就可以了。

在前幾節的案例中,我們已經分别學過緩存和 OOM 的原理和分析。那 Swap 又是怎麼工作的呢?因為内容比較多,接下來,我将用兩節課的内容,帶你探索 Swap 的工作原理,以及 Swap 升高後的分析方法。今天我們先來看看,Swap 究竟是怎麼工作的。

Swap 原理與直接記憶體回收

前面提到,Swap 說白了就是把一塊磁盤空間或者一個本地檔案(以下講解以磁盤為例),當成記憶體來使用。它包括換出和換入兩個過程。

  • 所謂換出,就是把程序暫時不用的記憶體資料存儲到磁盤中,并釋放這些資料占用的記憶體。
  • 而換入,則是在程序再次通路這些記憶體的時候,把它們從磁盤讀到記憶體中來。

是以你看,Swap 其實是把系統的可用記憶體變大了。這樣,即使伺服器的記憶體不足,也可以運作大記憶體的應用程式。 

還記得我最早學習 Linux 作業系統時,記憶體實在太貴了,一個普通學生根本就用不起大的記憶體,那會兒我就是開啟了 Swap 來運作 Linux 桌面。當然,現在的記憶體便宜多了,伺服器一般也會配置很大的記憶體,那是不是說 Swap 就沒有用武之地了呢? 

當然不是。事實上,記憶體再大,對應用程式來說,也有不夠用的時候。

一個很典型的場景就是,即使記憶體不足時,有些應用程式也并不想被 OOM 殺死,而是希望能緩一段時間,等待人工介入,或者等系統自動釋放其他程序的記憶體,再配置設定給它。

除此之外,我們常見的筆記本電腦的休眠和快速開機的功能,也基于 Swap 。休眠時,把系統的記憶體存入磁盤,這樣等到再次開機時,隻要從磁盤中加載記憶體就可以。這樣就省去了很多應用程式的初始化過程,加快了開機速度。 

話說回來,既然 Swap 是為了回收記憶體,那麼 Linux 到底在什麼時候需要回收記憶體呢?前面一直在說記憶體資源緊張,又該怎麼來衡量記憶體是不是緊張呢? 

一個最容易想到的場景就是,有新的大塊記憶體配置設定請求,但是剩餘記憶體不足。這個時候系統就需要回收一部分記憶體(比如前面提到的緩存),進而盡可能地滿足新記憶體請求。這個過程通常被稱為直接記憶體回收。

核心線程kswapd0用來定期回收記憶體

除了直接記憶體回收,還有一個專門的核心線程用來定期回收記憶體,也就是 kswapd0。

為了衡量記憶體的使用情況,kswapd0 定義了三個記憶體門檻值(watermark,也稱為水位),分别是頁最小門檻值(pages_min)、頁低門檻值(pages_low)和頁高門檻值(pages_high)。剩餘記憶體,則使用 pages_free 表示。 

[root@docker ~]# ps -ef | grep kswapd0
root        48     2  0 09:58 ?        00:00:00 [kswapd0]      

這裡,我畫了一張圖表示它們的關系。 

kswapd0 定期掃描記憶體的使用情況,并根據剩餘記憶體落在這三個門檻值的空間位置,進行記憶體的回收操作。 

  • 剩餘記憶體小于頁最小門檻值,說明程序可用記憶體都耗盡了,隻有核心才可以配置設定記憶體。
  • 剩餘記憶體落在頁最小門檻值和頁低門檻值中間,說明記憶體壓力比較大,剩餘記憶體不多了。這時 kswapd0 會執行記憶體回收,直到剩餘記憶體大于高門檻值為止。
  • 剩餘記憶體落在頁低門檻值和頁高門檻值中間,說明記憶體有一定壓力,但還可以滿足新記憶體請求。
  • 剩餘記憶體大于頁高門檻值,說明剩餘記憶體比較多,沒有記憶體壓力。

我們可以看到,一旦剩餘記憶體小于頁低門檻值,就會觸發記憶體的回收。頁最小門檻值,其實可以通過核心選項 /proc/sys/vm/min_free_kbytes 來間接設定。min_free_kbytes 設定了頁最小門檻值,而其他兩個門檻值,都是根據頁最小門檻值計算生成的,計算方法如下 :

[root@docker ~]# cat /proc/sys/vm/min_free_kbytes 
67584      
pages_low = pages_min*5/4
pages_high = pages_min*3/2      

NUMA 與 Swap

很多情況下,你明明發現了 Swap 升高,可是在分析系統的記憶體使用時,卻很可能發現,系統剩餘記憶體還多着呢。為什麼剩餘記憶體很多的情況下,也會發生 Swap 呢?

看到上面的标題,你應該已經想到了,這正是處理器的 NUMA (Non-Uniform Memory Access)架構導緻的。

關于 NUMA,我在 CPU 子產品中曾簡單提到過。在 NUMA 架構下,多個處理器被劃分到不同 Node 上,且每個 Node 都擁有自己的本地記憶體空間。

而同一個 Node 内部的記憶體空間,實際上又可以進一步分為不同的記憶體域(Zone),比如直接記憶體通路區(DMA)、普通記憶體區(NORMAL)、僞記憶體區(MOVABLE)等,如下圖所示:

先不用特别關注這些記憶體域的具體含義,我們隻要會檢視門檻值的配置,以及緩存、匿名頁的實際使用情況就夠了。

既然 NUMA 架構下的每個 Node 都有自己的本地記憶體空間,那麼,在分析記憶體的使用時,我們也應該針對每個 Node 單獨分析。

你可以通過 numactl 指令,來檢視處理器在 Node 的分布情況,以及每個 Node 的記憶體使用情況。比如,下面就是一個 numactl 輸出的示例:

$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1
node 0 size: 7977 MB
node 0 free: 4416 MB
...



[root@docker ~]# lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                4
On-line CPU(s) list:   0-3

[root@docker ~]# numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3
node 0 size: 3944 MB
node 0 free: 3324 MB
node distances:
node   0 
  0:  10       

這個界面顯示,我的系統中隻有一個 Node,也就是 Node 0 ,而且編号為 0 和 1 的兩個 CPU, 都位于 Node 0 上。另外,Node 0 的記憶體大小為 7977 MB,剩餘記憶體為 4416 MB。

了解了 NUNA 的架構和 NUMA 記憶體的檢視方法後,你可能就要問了這跟 Swap 有什麼關系呢?

實際上,前面提到的三個記憶體門檻值(頁最小門檻值、頁低門檻值和頁高門檻值),都可以通過記憶體域在 proc 檔案系統中的接口 /proc/zoneinfo 來檢視。

比如,下面就是一個 /proc/zoneinfo 檔案的内容示例:

$ cat /proc/zoneinfo
...
Node 0, zone   Normal
 pages free     227894
       min      14896
       low      18620
       high     22344
...
     nr_free_pages 227894
     nr_zone_inactive_anon 11082
     nr_zone_active_anon 14024
     nr_zone_inactive_file 539024
     nr_zone_active_file 923986
...      
[root@docker ~]# cat /proc/zoneinfo 
........................................
Node 0, zone      DMA
Node 0, zone    DMA32
........................................
Node 0, zone   Normal
  pages free     87216
        min      4128
        low      5160
        high     6192
        spanned  262144
        present  262144
        managed  245363
        protection: (0, 0, 0, 0, 0)
      nr_free_pages 87216

      nr_zone_inactive_anon 2088
      nr_zone_active_anon 28142

      nr_zone_inactive_file 68446
      nr_zone_active_file 35546      

這個輸出中有大量名額,我來解釋一下比較重要的幾個。

  • pages 處的 min、low、high,就是上面提到的三個記憶體門檻值,而 free 是剩餘記憶體頁數,它跟後面的 nr_free_pages 相同
  • nr_zone_active_anon 和 nr_zone_inactive_anon,分别是活躍和非活躍的匿名頁數
  • nr_zone_active_file 和 nr_zone_inactive_file,分别是活躍和非活躍的檔案頁數

從這個輸出結果可以發現,剩餘記憶體遠大于頁高門檻值,是以此時的 kswapd0 不會回收記憶體。

當然,某個 Node 記憶體不足時,系統可以從其他 Node 尋找空閑記憶體,也可以從本地記憶體中回收記憶體。具體選哪種模式,你可以通過 /proc/sys/vm/zone_reclaim_mode 來調整。它支援以下幾個選項: 

  • 預設的 0 ,也就是剛剛提到的模式,表示既可以從其他 Node 尋找空閑記憶體,也可以從本地回收記憶體。
[root@docker ~]# cat  /proc/sys/vm/zone_reclaim_mode
0      
  • 1、2、4 都表示隻回收本地記憶體,2 表示可以回寫髒資料回收記憶體,4 表示可以用 Swap 方式回收記憶體。

swappiness

到這裡,我們就可以了解記憶體回收的機制了。這些回收的記憶體既包括了檔案頁,又包括了匿名頁。

  • 對檔案頁的回收,當然就是直接回收緩存,或者把髒頁寫回磁盤後再回收。
  • 而對匿名頁的回收,其實就是通過 Swap 機制,把它們寫入磁盤後再釋放記憶體。

不過,你可能還有一個問題。既然有兩種不同的記憶體回收機制,那麼在實際回收記憶體時,到底該先回收哪一種呢?

其實,Linux 提供了一個 /proc/sys/vm/swappiness 選項,用來調整使用 Swap 的積極程度。 

swappiness 的範圍是 0-100,數值越大,越積極使用 Swap,也就是更傾向于回收匿名頁;數值越小,越消極使用 Swap,也就是更傾向于回收檔案頁。 

雖然 swappiness 的範圍是 0-100,不過要注意,這并不是記憶體的百分比,而是調整 Swap 積極程度的權重,即使你把它設定成 0,當剩餘記憶體 + 檔案頁小于頁高門檻值時,還是會發生 Swap。 

swappiness 的值是 0

還有一種情況, 當 swappiness 的值是 0 的時候,會發生什麼呢?這種情況下,Linux 系統是不允許匿名記憶體寫入 Swap 空間了嗎?

我們可以回到前面,再看一下那段 swappiness 的英文定義,裡面特别強調了 swappiness 為 0 的情況。 

當空閑記憶體少于記憶體一個 zone 的"high water mark"中的值的時候,Linux 還是會做記憶體交換,也就是把匿名記憶體寫入到 Swap 空間後釋放記憶體。

在這裡 zone 是 Linux 劃分實體記憶體的一個區域,裡面有 3 個水位線(water mark),水位線可以用來警示空閑記憶體的緊張程度。

這裡我們可以再做個試驗來驗證一下,先運作 echo 0 > /proc/sys/vm/swappiness 指令把 swappiness 設定為 0, 然後用我們之前例子裡的 mem_alloc 程式來申請記憶體。 

比如我們的這個節點上記憶體有 12GB,同時有 2GB 的 Swap,用 mem_alloc 申請 12GB 的記憶體,我們可以看到 Swap 空間在 mem_alloc 調用之前,used=0,輸出結果如下圖所示。 

Linux 為什麼系統的Swap變高了(上)

接下來,調用 mem_alloc 之後,Swap 空間就被使用了。

Linux 為什麼系統的Swap變高了(上)

因為 mem_alloc 申請 12GB 記憶體已經和節點最大記憶體差不多了,我們如果檢視 cat /proc/zoneinfo ,也可以看到 normal zone 裡 high (water mark)的值和 free 的值差不多,這樣在 free<high 的時候,系統就會回收匿名記憶體頁面并寫入 Swap 空間。

Linux 為什麼系統的Swap變高了(上)

好了,在這裡我們介紹了 Linux 系統裡 swappiness 的概念,它是用來決定在記憶體緊張時候,回收匿名記憶體和 Page Cache 記憶體的比例。

swappiness 的取值範圍在 0 到 100,值為 100 的時候系統平等回收匿名記憶體和 Page Cache 記憶體;一般預設值為 60,就是優先回收 Page Cache;即使 swappiness 為 0,也不能完全禁止 Swap 分區的使用,就是說在記憶體緊張的時候,也會使用 Swap 來回收匿名記憶體。 

小結 

  • 檔案頁的回收比較容易了解,直接清空,或者把髒資料寫回磁盤後再釋放。
  • 而對匿名頁的回收,需要通過 Swap 換出到磁盤中,下次通路時,再從磁盤換入到記憶體中。

繼續閱讀