問題 1:內存回收與 OOM

- 怎么理解 LRU 內存回收?
- 回收后的內存又到哪里去了?
- OOM 是按照虛擬內存還是實際內存來打分?
- 怎么估計應用程序的最小內存?
其實在 Linux 內存的原理篇和 Swap 原理篇中我曾經講到,一旦發現內存緊張,系統會通過三種方式回收內存。我們來復習一下,這三種方式分別是 :
- 基於 LRU(Least Recently Used)算法,回收緩存;
- 基於 Swap 機制,回收不常訪問的匿名頁;
- 基於 OOM(Out of Memory)機制,殺掉占用大量內存的進程。
前兩種方式,緩存回收和 Swap 回收,實際上都是基於 LRU 算法,也就是優先回收不常訪問的內存。LRU 回收算法,實際上維護着 active 和 inactive 兩個雙向鏈表,其中:
- active 記錄活躍的內存頁;
- inactive 記錄非活躍的內存頁。
越接近鏈表尾部,就表示內存頁越不常訪問。這樣,在回收內存時,系統就可以根據活躍程度,優先回收不活躍的內存。
活躍和非活躍的內存頁,按照類型的不同,又分別分為文件頁和匿名頁,對應着緩存回收和 Swap 回收。
當然,你可以從 /proc/meminfo 中,查詢它們的大小,比如:
# grep 表示只保留包含 active 的指標(忽略大小寫)
# sort 表示按照字母順序排序
$ cat /proc/meminfo | grep -i active | sort
Active(anon): 167976 kB
Active(file): 971488 kB
Active: 1139464 kB
Inactive(anon): 720 kB
Inactive(file): 2109536 kB
Inactive: 2110256 kB
第三種方式,OOM 機制按照 oom_score 給進程排序。oom_score 越大,進程就越容易被系統殺死。
當系統發現內存不足以分配新的內存請求時,就會嘗試直接內存回收。這種情況下,如果回收完文件頁和匿名頁后,內存夠用了,當然皆大歡喜,把回收回來的內存分配給進程就可以了。但如果內存還是不足,OOM 就要登場了。
OOM 發生時,你可以在 dmesg 中看到 Out of memory 的信息,從而知道是哪些進程被 OOM 殺死了。比如,你可以執行下面的命令,查詢 OOM 日志:
$ dmesg | grep -i "Out of memory"
Out of memory: Kill process 9329 (java) score 321 or sacrifice child
當然了,如果你不希望應用程序被 OOM 殺死,可以調整進程的 oom_score_adj,減小 OOM 分值,進而降低被殺死的概率。或者,你還可以開啟內存的 overcommit,允許進程申請超過物理內存的虛擬內存(這兒實際上假設的是,進程不會用光申請到的虛擬內存)。
這三種方式,我們就復習完了。接下來,我們回到開始的四個問題,相信你自己已經有了答案。
LRU 算法的原理剛才已經提到了,這里不再重復。
內存回收后,會被重新放到未使用內存中。這樣,新的進程就可以請求、使用它們。
OOM 觸發的時機基於虛擬內存。換句話說,進程在申請內存時,如果申請的虛擬內存加上服務器實際已用的內存之和,比總的物理內存還大,就會觸發 OOM。
要確定一個進程或者容器的最小內存,最簡單的方法就是讓它運行起來,再通過 ps 或者 smap ,查看它的內存使用情況。不過要注意,進程剛啟動時,可能還沒開始處理實際業務,一旦開始處理實際業務,就會占用更多內存。所以,要記得給內存留一定的余量。
問題 2: 文件系統與磁盤的區別
文件系統和磁盤的原理,我將在下一個模塊中講解,它們跟內存的關系也十分密切。不過,在學習 Buffer 和 Cache 的原理時,我曾提到,Buffer 用於磁盤,而 Cache 用於文件。因此,有不少同學困惑了,比如 JJ 留言中的這兩個問題。
讀寫文件最終也是讀寫磁盤,到底要怎么區分,是讀寫文件還是讀寫磁盤呢?
讀寫磁盤難道可以不經過文件系統嗎?

如果你也有相同的疑問,主要還是沒搞清楚,磁盤和文件的區別。我在“怎么理解內存中的 Buffer 和 Cache”文章的留言區簡單回復過,不過擔心有同學沒有看到,所以在這里重新講一下。
磁盤是一個存儲設備(確切地說是塊設備),可以被划分為不同的磁盤分區。而在磁盤或者磁盤分區上,還可以再創建文件系統,並掛載到系統的某個目錄中。這樣,系統就可以通過這個掛載目錄,來讀寫文件。
換句話說,磁盤是存儲數據的塊設備,也是文件系統的載體。所以,文件系統確實還是要通過磁盤,來保證數據的持久化存儲。
你在很多地方都會看到這句話, Linux 中一切皆文件。換句話說,你可以通過相同的文件接口,來訪問磁盤和文件(比如 open、read、write、close 等)。
我們通常說的“文件”,其實是指普通文件。
而磁盤或者分區,則是指塊設備文件。
你可以執行 “ls -l < 路徑 >” 查看它們的區別。如果不懂 ls 輸出的含義,別忘了 man 一下就可以。執行 man ls 命令,以及 info ‘(coreutils) ls invocation’ 命令,就可以查到了。
在讀寫普通文件時,I/O 請求會首先經過文件系統,然后由文件系統負責,來與磁盤進行交互。而在讀寫塊設備文件時,會跳過文件系統,直接與磁盤交互,也就是所謂的“裸 I/O”。
這兩種讀寫方式使用的緩存自然不同。文件系統管理的緩存,其實就是 Cache 的一部分。而裸磁盤的緩存,用的正是 Buffer。
更多關於文件系統、磁盤以及 I/O 的原理,你先不要着急,往后我們都會講到。
問題 3: 如何統計所有進程的物理內存使用量
這其實是 怎么理解內存中的 Buffer 和 Cache 的課后思考題,無名老卒、Griffin、JohnT3e 等少數幾個同學,都給出了一些思路。
比如,無名老卒同
學的方法,是把所有進程的 RSS 全部累加:

這種方法,實際上導致不少地方會被重復計算。RSS 表示常駐內存,把進程用到的共享內存也算了進去。所以,直接累加會導致共享內存被重復計算,不能得到准確的答案。
留言中好幾個同學的答案都有類似問題。你可以重新檢查一下自己的方法,弄清楚每個指標的定義和原理,防止重復計算。
當然,也有同學的思路非常正確,比如 JohnT3e 提到的,這個問題的關鍵在於理解 PSS 的含義。

你當然可以通過 stackexchange 上的鏈接找到答案,不過,我還是更推薦,直接查 proc 文件系統的文檔:
The “proportional set size” (PSS) of a process is the count of pages it has in memory, where each page is divided by the number of processes sharing it. So if a process has 1000 pages all to itself, and 1000 shared with one other process, its PSS will be 1500.
這里我簡單解釋一下,每個進程的 PSS ,是指把共享內存平分到各個進程后,再加上進程本身的非共享內存大小的和。
就像文檔中的這個例子,一個進程的非共享內存為 1000 頁,它和另一個進程的共享進程也是 1000 頁,那么它的 PSS=1000/2+1000=1500 頁。
這樣,你就可以直接累加 PSS ,不用擔心共享內存重復計算的問題了。
比如,你可以運行下面的命令來計算:
# 使用 grep 查找 Pss 指標后,再用 awk 計算累加值
$ grep Pss /proc/[1-9]*/smaps | awk '{total+=$2}; END {printf "%d kB\n", total }'
391266 kB
問題 4: CentOS 系統中如何安裝 bcc-tools
很多同學留言說用的是 CentOS 系統。雖然我在文章中也給出了一個參考文檔,不過 bcc-tools 工具安裝起來還是有些困難。
比如白華同學留言表示,網絡上的教程不太完整,步驟有些亂:

在這里,我也統一回復一下,在 CentOS 中安裝 bcc-tools 的步驟。以 CentOS 7 為例,整個安裝主要可以分兩步。
第一步,升級內核。你可以運行下面的命令來操作:
# 升級系統
yum update -y
# 安裝 ELRepo
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
rpm -Uvh https://www.elrepo.org/elrepo-release-7.0-3.el7.elrepo.noarch.rpm
# 安裝新內核
yum remove -y kernel-headers kernel-tools kernel-tools-libs
yum --enablerepo="elrepo-kernel" install -y kernel-ml kernel-ml-devel kernel-ml-headers kernel-ml-tools kernel-ml-tools-libs kernel-ml-tools-libs-devel
# 更新 Grub 后重啟
grub2-mkconfig -o /boot/grub2/grub.cfg
grub2-set-default 0
reboot
# 重啟后確認內核版本已升級為 4.20.0-1.el7.elrepo.x86_64
uname -r
第二步,安裝 bcc-tools:
# 安裝 bcc-tools
yum install -y bcc-tools
# 配置 PATH 路徑
export PATH=$PATH:/usr/share/bcc/tools
# 驗證安裝成功
cachestat
問題 5: 內存泄漏案例的優化方法
這是我在 內存泄漏了,我該如何定位和處理 中留的一個思考題。這個問題是這樣的:
在內存泄漏案例的最后,我們通過增加 free() 調用,釋放了函數 fibonacci() 分配的內存,修復了內存泄漏的問題。就這個案例而言,還有沒有其他更好的修復方法呢?
很多同學留言寫下了自己的想法,都很不錯。這里,我重點表揚下郭江偉同學,他給出的方法非常好:

他的思路是不用動態內存分配的方法,而是用數組來暫存計算結果。這樣就可以由系統自動管理這些棧內存,也不存在內存泄漏的問題了。
這種減少動態內存分配的思路,除了可以解決內存泄漏問題,其實也是常用的內存優化方法。比如,在需要大量內存的場景中,你就可以考慮用棧內存、內存池、HugePage 等方法,來優化內存的分配和管理。
除了這五個問題,還有一點我也想說一下。很多同學在說工具的版本問題,的確,生產環境中的 Linux 版本往往都比較低,導致很多新工具不能在生產環境中直接使用。
不過,這並不代表我們就無能為力了。畢竟,系統的原理都是大同小異的。這其實也是我一直強調的觀點。
在學習時,最好先用最新的系統和工具,它們可以為你提供更簡單直觀的結果,幫你更好的理解系統的原理。
在你掌握了這些原理后,回過頭來,再去理解舊版本系統中的工具和原理,你會發現,即便舊版本中的很多工具並不是那么好用,但是原理和指標是類似的,你依然可以輕松掌握它們的使用方法。