天天看點

騰訊一面:記憶體滿了,會發生什麼?

作者:小林coding

計算機八股文刷題網站:https://xiaolincoding.com

大家好,我是小林。

前幾天有位讀者留言說,面騰訊時,被問了兩個記憶體管理的問題:

騰訊一面:記憶體滿了,會發生什麼?
騰訊一面:記憶體滿了,會發生什麼?

先來說說第一個問題:虛拟記憶體有什麼作用?

  • 第一,由于每個程序都有自己的頁表,是以每個程序的虛拟記憶體空間就是互相獨立的。程序也沒有辦法通路其他程序的頁表,是以這些頁表是私有的。這就解決了多程序之間位址沖突的問題。
  • 第二,頁表裡的頁表項中除了實體位址之外,還有一些标記屬性的比特,比如控制一個頁的讀寫權限,标記該頁是否存在等。在記憶體通路方面,作業系統提供了更好的安全性。

然後今天主要是聊聊第二個問題,「系統記憶體緊張時,會發生什麼?」

發車!

騰訊一面:記憶體滿了,會發生什麼?

記憶體配置設定的過程是怎樣的?

應用程式通過 malloc 函數申請記憶體的時候,實際上申請的是虛拟記憶體,此時并不會配置設定實體記憶體。

當應用程式讀寫了這塊虛拟記憶體,CPU 就會去通路這個虛拟記憶體, 這時會發現這個虛拟記憶體沒有映射到實體記憶體, CPU 就會産生缺頁中斷,程序會從使用者态切換到核心态,并将缺頁中斷交給核心的 Page Fault Handler (缺頁中斷函數)處理。

缺頁中斷處理函數會看是否有空閑的實體記憶體,如果有,就直接配置設定實體記憶體,并建立虛拟記憶體與實體記憶體之間的映射關系。

如果沒有空閑的實體記憶體,那麼核心就會開始進行回收記憶體的工作,回收的方式主要是兩種:直接記憶體回收和背景記憶體回收。

  • 背景記憶體回收(kswapd):在實體記憶體緊張的時候,會喚醒 kswapd 核心線程來回收記憶體,這個回收記憶體的過程異步的,不會阻塞程序的執行。
  • 直接記憶體回收(direct reclaim):如果背景異步回收跟不上程序記憶體申請的速度,就會開始直接回收,這個回收記憶體的過程是同步的,會阻塞程序的執行。

如果直接記憶體回收後,空閑的實體記憶體仍然無法滿足此次實體記憶體的申請,那麼核心就會放最後的大招了 ——觸發 OOM (Out of Memory)機制。

OOM Killer 機制會根據算法選擇一個占用實體記憶體較高的程序,然後将其殺死,以便釋放記憶體資源,如果實體記憶體依然不足,OOM Killer 會繼續殺死占用實體記憶體較高的程序,直到釋放足夠的記憶體位置。

申請實體記憶體的過程如下圖:

騰訊一面:記憶體滿了,會發生什麼?

哪些記憶體可以被回收?

系統記憶體緊張的時候,就會進行回收内測的工作,那具體哪些記憶體是可以被回收的呢?

主要有兩類記憶體可以被回收,而且它們的回收方式也不同。

  • 檔案頁(File-backed Page):核心緩存的磁盤資料(Buffer)和核心緩存的檔案資料(Cache)都叫作檔案頁。大部分檔案頁,都可以直接釋放記憶體,以後有需要時,再從磁盤重新讀取就可以了。而那些被應用程式修改過,并且暫時還沒寫入磁盤的資料(也就是髒頁),就得先寫入磁盤,然後才能進行記憶體釋放。是以,回收幹淨頁的方式是直接釋放記憶體,回收髒頁的方式是先寫回磁盤後再釋放記憶體。
  • 匿名頁(Anonymous Page):應用程式通過 mmap 動态配置設定的堆記憶體叫作匿名頁,這部分記憶體很可能還要再次被通路,是以不能直接釋放記憶體,它們回收的方式是通過 Linux 的 Swap 機制,Swap 會把不常通路的記憶體先寫到磁盤中,然後釋放這些記憶體,給其他更需要的程序使用。再次通路這些記憶體時,重新從磁盤讀入記憶體就可以了。

檔案頁和匿名頁的回收都是基于 LRU 算法,也就是優先回收不常通路的記憶體。LRU 回收算法,實際上維護着 active 和 inactive 兩個雙向連結清單,其中:

  • active_list 活躍記憶體頁連結清單,這裡存放的是最近被通路過(活躍)的記憶體頁;
  • inactive_list 不活躍記憶體頁連結清單,這裡存放的是很少被通路(非活躍)的記憶體頁;

越接近連結清單尾部,就表示記憶體頁越不常通路。這樣,在回收記憶體時,系統就可以根據活躍程度,優先回收不活躍的記憶體。

活躍和非活躍的記憶體頁,按照類型的不同,又分别分為檔案頁和匿名頁。可以從 /proc/meminfo 中,查詢它們的大小,比如:

# grep表示隻保留包含active的名額(忽略大小寫)
# sort表示按照字母順序排序
[root@xiaolin ~]# cat /proc/meminfo | grep -i active | sort
Active:           901456 kB
Active(anon):     227252 kB
Active(file):     674204 kB
Inactive:         226232 kB
Inactive(anon):    41948 kB
Inactive(file):   184284 kB
           

回收記憶體帶來的性能影響

在前面我們知道了回收記憶體有兩種方式。

  • 一種是背景記憶體回收,也就是喚醒 kswapd 核心線程,這種方式是異步回收的,不會阻塞程序。
  • 一種是直接記憶體回收,這種方式是同步回收的,會阻塞程序,這樣就會造成很長時間的延遲,以及系統的 CPU 使用率會升高,最終引起系統負荷飙高。

可被回收的記憶體類型有檔案頁和匿名頁:

  • 檔案頁的回收:對于幹淨頁是直接釋放記憶體,這個操作不會影響性能,而對于髒頁會先寫回到磁盤再釋放記憶體,這個操作會發生磁盤 I/O 的,這個操作是會影響系統性能的。
  • 匿名頁的回收:如果開啟了 Swap 機制,那麼 Swap 機制會将不常通路的匿名頁換出到磁盤中,下次通路時,再從磁盤換入到記憶體中,這個操作是會影響系統性能的。

可以看到,回收記憶體的操作基本都會發生磁盤 I/O 的,如果回收記憶體的操作很頻繁,意味着磁盤 I/O 次數會很多,這個過程勢必會影響系統的性能,整個系統給人的感覺就是很卡。

下面針對回收記憶體導緻的性能影響,說說常見的解決方式。

調整檔案頁和匿名頁的回收傾向

從檔案頁和匿名頁的回收操作來看,檔案頁的回收操作對系統的影響相比匿名頁的回收操作會少一點,因為檔案頁對于幹淨頁回收是不會發生磁盤 I/O 的,而匿名頁的 Swap 換入換出這兩個操作都會發生磁盤 I/O。

Linux 提供了一個

/proc/sys/vm/swappiness

選項,用來調整檔案頁和匿名頁的回收傾向。

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

[root@xiaolin ~]# cat /proc/sys/vm/swappiness
0
           

一般建議 swappiness 設定為 0(預設就是 0),這樣在回收記憶體的時候,會更傾向于檔案頁的回收,但是并不代表不會回收匿名頁。

盡早觸發 kswapd 核心線程異步回收記憶體

如何檢視系統的直接記憶體回收和背景記憶體回收的名額?

我們可以使用

sar -B 1

指令來觀察:

騰訊一面:記憶體滿了,會發生什麼?

圖中紅色框住的就是背景記憶體回收和直接記憶體回收的名額,它們分别表示:

  • pgscank/s : kswapd(背景回收線程) 每秒掃描的 page 個數。
  • pgscand/s: 應用程式在記憶體申請過程中每秒直接掃描的 page 個數。
  • pgsteal/s: 掃描的 page 中每秒被回收的個數(pgscank+pgscand)。

如果系統時不時發生抖動,并且在抖動的時間段裡如果通過 sar -B 觀察到 pgscand 數值很大,那大機率是因為「直接記憶體回收」導緻的。

針對這個問題,解決的辦法就是,可以通過盡早的觸發「背景記憶體回收」來避免應用程式進行直接記憶體回收。

什麼條件下才能觸發 kswapd 核心線程回收記憶體呢?

核心定義了三個記憶體門檻值(watermark,也稱為水位),用來衡量目前剩餘記憶體(pages_free)是否充裕或者緊張,分别是:

  • 頁最小門檻值(pages_min);
  • 頁低門檻值(pages_low);
  • 頁高門檻值(pages_high);

這三個記憶體門檻值會劃分為四種記憶體使用情況,如下圖:

騰訊一面:記憶體滿了,會發生什麼?

kswapd 會定期掃描記憶體的使用情況,根據剩餘記憶體(pages_free)的情況來進行記憶體回收的工作。

  • 圖中綠色部分:如果剩餘記憶體(pages_free)大于 頁高門檻值(pages_high),說明剩餘記憶體是充足的;
  • 圖中藍色部分:如果剩餘記憶體(pages_free)在頁高門檻值(pages_high)和頁低門檻值(pages_low)之間,說明記憶體有一定壓力,但還可以滿足應用程式申請記憶體的請求;
  • 圖中橙色部分:如果剩餘記憶體(pages_free)在頁低門檻值(pages_low)和頁最小門檻值(pages_min)之間,說明記憶體壓力比較大,剩餘記憶體不多了。這時 kswapd0 會執行記憶體回收,直到剩餘記憶體大于高門檻值(pages_high)為止。雖然會觸發記憶體回收,但是不會阻塞應用程式,因為兩者關系是異步的。
  • 圖中紅色部分:如果剩餘記憶體(pages_free)小于頁最小門檻值(pages_min),說明使用者可用記憶體都耗盡了,此時就會觸發直接記憶體回收,這時應用程式就會被阻塞,因為兩者關系是同步的。

可以看到,當剩餘記憶體頁(pages_free)小于頁低門檻值(pages_low),就會觸發 kswapd 進行背景回收,然後 kswapd 會一直回收到剩餘記憶體頁(pages_free)大于頁高門檻值(pages_high)。

也就是說 kswapd 的活動空間隻有 pages_low 與 pages_min 之間的這段區域,如果剩餘内測低于了 pages_min 會觸發直接記憶體回收,高于了 pages_high 又不會喚醒 kswapd。

頁低門檻值(pages_low)可以通過核心選項

/proc/sys/vm/min_free_kbytes

(該參數代表系統所保留白閑記憶體的最低限)來間接設定。

min_free_kbytes 雖然設定的是頁最小門檻值(pages_min),但是頁高門檻值(pages_high)和頁低門檻值(pages_low)都是根據頁最小門檻值(pages_min)計算生成的,它們之間的計算關系如下:

pages_min = min_free_kbytes
pages_low = pages_min*5/4
pages_high = pages_min*3/2
           

如果系統時不時發生抖動,并且通過 sar -B 觀察到 pgscand 數值很大,那大機率是因為直接記憶體回收導緻的,這時可以增大 min_free_kbytes 這個配置選項來及早地觸發背景回收,然後繼續觀察 pgscand 是否會降為 0。

增大了 min_free_kbytes 配置後,這會使得系統預留過多的空閑記憶體,進而在一定程度上降低了應用程式可使用的記憶體量,這在一定程度上浪費了記憶體。極端情況下設定 min_free_kbytes 接近實際實體記憶體大小時,留給應用程式的記憶體就會太少而可能會頻繁地導緻 OOM 的發生。

是以在調整 min_free_kbytes 之前,需要先思考一下,應用程式更加關注什麼,如果關注延遲那就适當地增大 min_free_kbytes,如果關注記憶體的使用量那就适當地調小 min_free_kbytes。

NUMA 架構下的記憶體回收政策

什麼是 NUMA 架構?

再說 NUMA 架構前,先給大家說說 SMP 架構,這兩個架構都是針對 CPU 的。

SMP 指的是一種多個 CPU 處理器共享資源的電腦硬體架構,也就是說每個 CPU 地位平等,它們共享相同的實體資源,包括總線、記憶體、IO、作業系統等。每個 CPU 通路記憶體所用時間都是相同的,是以,這種系統也被稱為一緻存儲通路結構(UMA,Uniform Memory Access)。

随着 CPU 處理器核數的增多,多個 CPU 都通過一個總線通路記憶體,這樣總線的帶寬壓力會越來越大,同時每個 CPU 可用帶寬會減少,這也就是 SMP 架構的問題。

騰訊一面:記憶體滿了,會發生什麼?

SMP 與 NUMA 架構

為了解決 SMP 架構的問題,就研制出了 NUMA 結構,即非一緻存儲通路結構(Non-uniform memory access,NUMA)。

NUMA 架構将每個 CPU 進行了分組,每一組 CPU 用 Node 來表示,一個 Node 可能包含多個 CPU 。

每個 Node 有自己獨立的資源,包括記憶體、IO 等,每個 Node 之間可以通過互聯子產品總線(QPI)進行通信,是以,也就意味着每個 Node 上的 CPU 都可以通路到整個系統中的所有記憶體。但是,通路遠端 Node 的記憶體比通路本地記憶體要耗時很多。

NUMA 架構跟回收記憶體有什麼關系?

在 NUMA 架構下,當某個 Node 記憶體不足時,系統可以從其他 Node 尋找空閑記憶體,也可以從本地記憶體中回收記憶體。

具體選哪種模式,可以通過 /proc/sys/vm/zone_reclaim_mode 來控制。它支援以下幾個選項:

  • 0 (預設值):在回收本地記憶體之前,在其他 Node 尋找空閑記憶體;
  • 1:隻回收本地記憶體;
  • 2:隻回收本地記憶體,在本地回收記憶體時,可以将檔案頁中的髒頁寫回硬碟,以回收記憶體。
  • 4:隻回收本地記憶體,在本地回收記憶體時,可以用 swap 方式回收記憶體。

在使用 NUMA 架構的伺服器,如果系統出現還有一半記憶體的時候,卻發現系統頻繁觸發「直接記憶體回收」,導緻了影響了系統性能,那麼大機率是因為 zone_reclaim_mode 沒有設定為 0 ,導緻當本地記憶體不足的時候,隻選擇回收本地記憶體的方式,而不去使用其他 Node 的空閑記憶體。

雖然說通路遠端 Node 的記憶體比通路本地記憶體要耗時很多,但是相比記憶體回收的危害而言,通路遠端 Node 的記憶體帶來的性能影響還是比較小的。是以,zone_reclaim_mode 一般建議設定為 0。

如何保護一個程序不被 OOM 殺掉呢?

在系統空閑記憶體不足的情況,程序申請了一個很大的記憶體,如果直接記憶體回收都無法回收出足夠大的空閑記憶體,那麼就會觸發 OOM 機制,核心就會根據算法選擇一個程序殺掉。

Linux 到底是根據什麼标準來選擇被殺的程序呢?這就要提到一個在 Linux 核心裡有一個

oom_badness()

函數,它會把系統中可以被殺掉的程序掃描一遍,并對每個程序打分,得分最高的程序就會被首先殺掉。

程序得分的結果受下面這兩個方面影響:

  • 第一,程序已經使用的實體記憶體頁面數。
  • 第二,每個程序的 OOM 校準值 oom_score_adj。它是可以通過

    /proc/[pid]/oom_score_adj

    來配置的。我們可以在設定 -1000 到 1000 之間的任意一個數值,調整程序被 OOM Kill 的幾率。

函數 oom_badness() 裡的最終計算方法是這樣的:

// points 代表打分的結果
// process_pages 代表程序已經使用的實體記憶體頁面數
// oom_score_adj 代表 OOM 校準值
// totalpages 代表系統總的可用頁面數
points = process_pages + oom_score_adj*totalpages/1000
           

用「系統總的可用頁面數」乘以 「OOM 校準值 oom_score_adj」再除以 1000,最後再加上程序已經使用的實體頁面數,計算出來的值越大,那麼這個程序被 OOM Kill 的幾率也就越大。

每個程序的 oom_score_adj 預設值都為 0,是以最終得分跟程序自身消耗的記憶體有關,消耗的記憶體越大越容易被殺掉。我們可以通過調整 oom_score_adj 的數值,來改成程序的得分結果:

  • 如果你不想某個程序被首先殺掉,那你可以調整該程序的 oom_score_adj,進而改變這個程序的得分結果,降低該程序被 OOM 殺死的機率。
  • 如果你想某個程序無論如何都不能被殺掉,那你可以将 oom_score_adj 配置為 -1000。

我們最好将一些很重要的系統服務的 oom_score_adj 配置為 -1000,比如 sshd,因為這些系統服務一旦被殺掉,我們就很難再登陸進系統了。

但是,不建議将我們自己的業務程式的 oom_score_adj 設定為 -1000,因為業務程式一旦發生了記憶體洩漏,而它又不能被殺掉,這就會導緻随着它的記憶體開銷變大,OOM killer 不停地被喚醒,進而把其他程序一個個給殺掉。

參考資料:

  • https://time.geekbang.org/column/article/277358
  • https://time.geekbang.org/column/article/75797
  • https://www.jianshu.com/p/e40e8813842f

總結

核心在給應用程式配置設定實體記憶體的時候,如果空閑實體記憶體不夠,那麼就會進行記憶體回收的工作,主要有兩種方式:

  • 背景記憶體回收:在實體記憶體緊張的時候,會喚醒 kswapd 核心線程來回收記憶體,這個回收記憶體的過程異步的,不會阻塞程序的執行。
  • 直接記憶體回收:如果背景異步回收跟不上程序記憶體申請的速度,就會開始直接回收,這個回收記憶體的過程是同步的,會阻塞程序的執行。

可被回收的記憶體類型有檔案頁和匿名頁:

  • 檔案頁的回收:對于幹淨頁是直接釋放記憶體,這個操作不會影響性能,而對于髒頁會先寫回到磁盤再釋放記憶體,這個操作會發生磁盤 I/O 的,這個操作是會影響系統性能的。
  • 匿名頁的回收:如果開啟了 Swap 機制,那麼 Swap 機制會将不常通路的匿名頁換出到磁盤中,下次通路時,再從磁盤換入到記憶體中,這個操作是會影響系統性能的。

檔案頁和匿名頁的回收都是基于 LRU 算法,也就是優先回收不常通路的記憶體。回收記憶體的操作基本都會發生磁盤 I/O 的,如果回收記憶體的操作很頻繁,意味着磁盤 I/O 次數會很多,這個過程勢必會影響系統的性能。

針對回收記憶體導緻的性能影響,常見的解決方式。

  • 設定 /proc/sys/vm/swappiness,調整檔案頁和匿名頁的回收傾向,盡量傾向于回收檔案頁;
  • 設定 /proc/sys/vm/min_free_kbytes,調整 kswapd 核心線程異步回收記憶體的時機;
  • 設定 /proc/sys/vm/zone_reclaim_mode,調整 NUMA 架構下記憶體回收政策,建議設定為 0,這樣在回收本地記憶體之前,會在其他 Node 尋找空閑記憶體,進而避免在系統還有很多空閑記憶體的情況下,因本地 Node 的本地記憶體不足,發生頻繁直接記憶體回收導緻性能下降的問題;

在經曆完直接記憶體回收後,空閑的實體記憶體大小依然不夠,那麼就會觸發 OOM 機制,OOM killer 就會根據每個程序的記憶體占用情況和 oom_score_adj 的值進行打分,得分最高的程序就會被首先殺掉。

我們可以通過調整程序的 /proc/[pid]/oom_score_adj 值,來降低被 OOM killer 殺掉的機率。

完!

微信搜尋公衆号:「小林coding」 ,回複「圖解」即可免費獲得「圖解網絡、圖解系統、圖解MySQL、圖解Redis」PDF 電子書