PostgreSQL 使用的內存去向

  1. 前言 =====

近期遇到了好幾例與 PostgreSQL 有關的內存溢出、內存使用過大等問題,所以特意找了一些資料,整理以備後用。

  1. 內存都去哪了 =========

這個問題會在不同地方出現:PostgreSQL 使用了太多的內存,爲什麼?該如何緩解?

在我們進行 "優化" 之前,我們應該瞭解問題所在,包括 ps 和 top 在內的兩個標準工具都在說謊。

我的本地有一個非常簡單的 PostgreSQL 實例,配置了 4 GB 的 shared_buffers 和 100MB 的 work_mem。我做了一些工作,現在 ps 顯示如下:

top 顯示的數據基本相同,這是可疑的,因爲 "free" 顯示

即僅使用了 1.5GB 的內存 (並且有大約 10GB 的內存用作磁盤緩存,如果有任何應用需要更多的內存,可以隨時釋放它們)。

那麼,爲什麼會在 ps 中看到如此龐大的數字呢?

首先,我們需要忽略 VSZ 列。重要的是 RSS。但它也不是很有用:

可以看到此刻計算出來的總內存是 15GB,比總內存更大。那麼,真實的內存使用情況是什麼呢?如果我殺了 PostgreSQL 的進程,我能獲得多少內存?

幸運的是,我們可以從 / proc 目錄下的相關文件查看內存的確切用途。

Linux 上的每個進程在 / proc 下都有一個目錄。在此目錄中,有許多文件和目錄。不要被文件大小所迷惑,它們都是 "0" 字節,但是它們確實包含信息,這是不可思議的。

我們感興趣的一個文件是 "smaps"。

它的內容看起來像這樣:

該進程的 smaps 展示了內存消耗,裏有超過 2000 多行,所以沒有完全展示它。然而無論怎麼樣,根據 ps 顯示,進程 27713 使用了 4285716 KB 的內存。那麼,它有多大?快速 grep,我們看到:

只有一個區域的大小超過 100MB,它的大小非常接近進程的總大小。

完整信息如下:

這些信息大部分比較神祕,但我們看到了如下幾點:

  1. 它是共享內存 (第一行包含 rw-s,其中 "s" 代表共享)

  2. 通過 (/SYSV… deleted),看起來共享內存是使用 mmaping 刪除文件來完成的,因此在 free 的輸出中,這些內存將在 Cached 中,而不是 Used 列。

  3. 共享塊的大小爲 4317224,而共享塊中的 4280924 實際上駐留在內存中

沒關係,那是 shared_buffers。但事實是大多數後端進程都會使用 shared_buffers。而且,更糟的是,並不總是相同的程度。例如,進程 27722 相同的 shared_buffers 的數據:

在這裏,我們看到該進程只請求 / 使用了 388MB 的內存。

因此計算將很複雜。例如,我們可能有兩個進程,每個進程使用了 400MB 的 shared buffers,但是它沒有告訴我們它實際使用了多少內存,可能他們正在使用 100MB 的相同緩衝區,以及 300MB 的不同緩衝區,因此總的內存使用量將爲 700MB。

我們知道此 shared_buffers 塊的總大小爲 4317224KB。這太好了。但是其他事情呢?例如庫——它們可以由內核在多個進程之間共享。

幸運的是,在 2007 年,Fengguang Wu 發送了 (以前寫過) 一個非常酷的內核補丁——在 smaps 中添加了 "Pss" 信息。

基本上,Pss 最多爲 Rss,但是如果多個進程使用相同的內存頁,則 Pss 也會減少。這就是爲什麼上面的 Pss 遠遠低於 Rss / Size 的原因。例如在最後一個例子中。Rss 是 388652,而 Pss 只有 95756,這意味着這個後端進程使用的大多數頁面也被其他 3 個後端進程使用。

因此,現在知道了 Pss 之後,我們終於可以瞭解正在運行的 PostgreSQL 集羣的實際內存使用情況:

如果你只是說 "天啊,他跑了什麼?",讓我解釋一下。第一個命令:

只返回 pgdba 用戶的 pid(通常是 postgres,但我不同,是以 pgdba 的身份運行 PostgreSQL)

第二,使用 sed 將 pids 更換爲 smaps 文件的路徑:

然後對 sed 給出的文件中包含 ^Pss 的行執行簡單的 grep。它返回很多行,如下:

因此理論上,如果我停止 PostgreSQL,將回收這麼多內存。讓我們看看這是不是真的:

使用的內存從 12145424 下降到 7781960,這意味着我得到了 4363464 kB 的內存。這甚至比預期的 4329040KB 略高,但已經足夠接近了。正如預期的那樣,它大部分來自磁盤緩存,因爲它用於 shared buffers。

這很不錯,但是這個方法可以用來估算殺死單個後端進程可以回收的內存嗎?可以說是也可以說不是。關閉整個 PostgreSQL 實例意味着正在使用的共享內存可以被釋放。在正常環境中,當您終止後端進程時,最終只能釋放該後端的私有內存。這個數字通常很低。

例如,在另一臺機器上,有更令人印象深刻的硬件環境:

也就是說,該進程有 1.7GB 的 RSS(在 ps 輸出中可見),但是隻有 52MB 是私有內存,如果它被殺死,私有內存將被釋放。

所以在這兒你不能使用 Pss,但你可以使用來自 smaps 的 Private_* 數據來獲取私有內存。總而言之,PostgreSQL 使用的內存比乍一看要少得多,雖然可以得到非常準確的數字,但需要執行一些 shell 腳本來獲得它們。

  1. 小結 =====

Linux 的內存管理確實十分複雜,PostgreSQL 本身基於共享內存,同時還有 double buffer,再加上 local memory,要得到精確的數字比較困難。

這張圖可以大概知道涉及到了哪些內存,包括 pagetables、slab(內核爲了提高性能每個需要重複使用的對象都會有個池,這個 slab 池會 cache 大量常用的對象,所以會消耗大量的內存) 以及進程本身的消耗:

3.1. 何爲 smaps

/proc/PID/smaps 文件是基於 / proc/PID/maps 的擴展,他展示了一個進程的內存消耗,比同一目錄下的 maps 文件更爲詳細。

上面看到的就是 VMA,每一個 VMA(虛擬內存區域,即一個 vm_area_struct 結構指向的內存區域) 都有如下的一系列數據:

3.2. 混淆的內存指標

另外,對於 ps 和 top 等命令看到的 RSS、PSS、USS 等相關概念容易混淆,特意整理了一下。

Size:虛擬內存空間大小。但是這個內存值不一定是物理內存實際分配的大小,因爲在用戶態上,虛擬內存總是延遲分配的。這個值計算也非常簡單,就是該 VMA 的開始位置減結束位置。

延遲分配就是當進程申請內存的時候,Linux 會給他先分配頁,但是並不會區建立頁與頁框的映射關係,意思就是說並不會分配物理內存,而當真正使用的時候,就會產生一個缺頁異常,硬件跳轉 page fault 處理程序執行,在其中分配物理內存,然後修改頁表 (創建頁表項)。異常處理完畢,返回程序用戶態,繼續執行。

VSS : Virtual Set Size 虛擬耗用內存 (包含共享庫佔用的內存),即單個進程全部可訪問的地址空間,其大小可能包括還尚未在內存中駐留的部分。對於確定單個進程實際內存使用大小,VSS 用處不大。

RSS(Resident set size),使用 top 命令可以查詢到,是最常用的內存指標,即單個進程實際佔用的內存大小,表示進程佔用的物理內存大小。但是,將各進程的 RSS 值相加,通常會超出整個系統的內存消耗,這是因爲 RSS 中包含了各進程間共享的內存。RSS 不太準確的地方在於它包括該進程所使用共享庫全部內存大小。對於一個共享庫,可能被多個進程使用,實際該共享庫只會被裝入內存一次。

RSS 的計算方式如下:Rss=Shared_Clean+Shared_Dirty+Private_Clean+Private_Dirty

share/private:該頁面是共享還是私有。

dirty/clean:該頁面是否被修改過,如果修改過 (dirty),在頁面被淘汰的時候,就會把該髒頁面回寫到交換分區 (換出,swap out)。有一個標誌位用於表示頁面是否 dirty。

share/private_dirty/clean 計算邏輯:查看該 page 的引用數,如果引用 > 1,則歸爲 shared,如果是 1,則歸爲 private,同時也查看該 page 的 flag,是否標記爲_PAGE_DIRTY,如果不是,則認爲乾淨的。

PSS(Proportional set size) 所有使用某共享庫的程序均分該共享庫佔用的內存時,每個進程佔用的內存。顯然所有進程的 PSS 之和就是系統的內存使用量。它會更準確一些,它將共享內存的大小進行平均後,再分攤到各進程上去。

實際上包含下面 private_clean+private_dirty,和按比例均分的 shared_clean、shared_dirty。

舉個計算 Pss 的例子:

如果進程 A 有 x 個 private_clean 頁面,有 y 個 private_dirty 頁面,有 z 個 shared_clean 僅和進程 B 共享,有 h 個 shared_dirty 頁面和進程 B、C 共享。那麼進程 A 的 Pss 爲:

x + y + z/2 + h/3

USS(Unique set size)進程獨自佔用的物理內存 (不包含共享庫佔用的內存) 即單個進程私有的內存大小,即該進程獨佔的內存部分。USS 揭示了運行一個特定進程在的真實內存增量大小。如果進程終止,USS 就是實際被返還給系統的內存大小。

3.3. 常用的統計命令

  1. 使用 ps 命令找出佔用內存資源最多的 20 個進程:

ps aux | head -1;ps aux |grep -v PID |sort -rn -k +4 | head -20

ps -aux --sort -pmem | head -n 20

  1. 查看進程佔用的實際物理內存,這個也包含了共享庫等:

ps -eo size,pid,user,command --sort -size | awk '{hr=$1/1024 ; printf("%13.2f Mb ",hr) } { for ( x=4 ; x<=NF ; x++ ) { printf("%s ",$x) } print"" }'|cut -d"" -f2 | cut -d "-" -f1

  1. 通過 python 腳本觀察私有內存和共享內存:

python ps_mem.py -p 999

腳本位置在 https://raw.githubusercontent.com/pixelb/ps_mem/master/ps_mem.py,直接 cp 即可

  1. 通過 smaps 觀察私有內存,這個和 python 腳本得出的數字差不多,大約爲 5.7MB

ps uxf -p 999 | sort -nk6 | tail -n 1 | tee >(cat ->&2) | awk '{system("cat /proc/"$2"/smaps")}' | grep ^Private | awk '{A+=$2} END{print A}'

  1. 通過 smem 命令觀察,這個得出來的 USS 和 python 腳本以及 smaps 差不多,此例都約爲 6.1MB

smem -k | grep -w '/usr/pgsql-13/bin/postgres'

  1. 追查內存使用情況,用到腳本:

注:前文譯自 How much RAM is PostgreSQL using?

參考:linux 中 top 命令 VSS,RSS,PSS,USS 四個內存字段的解讀。

參考:Linux 內存管理 -- /proc/{pid}/smaps 講解

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_UX0H0tN0EmYOi3jBeuovA