1. 程式人生 > >Linux之《荒島餘生》(三)記憶體篇

Linux之《荒島餘生》(三)記憶體篇

記憶體問題,腦瓜疼腦瓜疼。腦瓜疼的意思,就是腦袋運算空間太小,撐的疼。本篇是《荒島餘生》系列第三篇,讓人腦瓜疼的記憶體篇。其餘參見:

Linux之《荒島餘生》(一)準備篇

Linux之《荒島餘生》(二)CPU篇

小公司請求量小,但喜歡濫用記憶體,開一堆執行緒,大把大把往jvm塞物件,最終問題是記憶體溢位。

大公司併發大,但喜歡強調HA,所以通常保留swap,最終問題是服務卡頓。

而喜歡用全域性集合變數的某些同仁,把java程式碼當c寫,物件塞進去但忘了銷燬,最終問題是記憶體洩漏。

如何避免? 合理引數、優雅程式碼、禁用swap,三管齊下, trouble shooter。

從一個故事開始

老王的疑問

一個陽光明媚的下午,一條報警簡訊彈了出來。老王微微一笑,是cpu問題,idle瞬時值,大概是某批請求比較大引起的峰值問題。老王每天都會收到這樣的簡訊,這樣的一個小峰值,在數千臺伺服器中,不過是滄海一慄,繼續喝茶就是了。

但,這次不一樣。幾分鐘之後,幾百個服務的超時報警鋪天蓋地到來。事後老王算了一下,大概千分之零點幾的服務超時了,不過這已經很恐怖了。 事態升級,恐怕沒時間喝茶了。

大面積報警,應該是全域性問題,是網路卡頓?還是資料庫抽風?老王挑了一臺最近報警的伺服器,輪流監控了各種狀態,總結如下:

  • cpu偶爾有瞬時峰值,但load非常正常

  • 記憶體雖然free不多了,但cached還有不少

  • 網路各種ping,基本正常

  • 磁碟I/O一般,畢竟是服務計算節點

  • 資料庫連線池穩定,應該不是db抽風

  • swap用了不少,但好像每臺機器都用了,沒啥大不了

全域性性的東西不太多,閘道器、LVS、註冊中心、DB、MQ,好像都沒問題。老王開始腦瓜疼了。

讓老王休息一下,我們把鏡頭轉向小王。

小王的操作

小王不是老王的兒子,他是老王的徒弟。徒弟一思考,導師就發笑。這次小王用的是vim,想查詢一個Exception,他打開了一個8GB的日誌檔案,然後樂呵呵的在那等著載入。然後,伺服器就死了。

答案

這裡直接給出答案,原因等讀完本文自然會了解。

老王的問題最終定位到是由於某個運維工程師使用ansible批量執行了一句命令

find / | grep "x"
複製程式碼

他是想找一個叫做x的檔案,看看在哪臺伺服器上。結果,這些老伺服器由於檔案太多,掃描後這些檔案資訊都快取到了slab區。而伺服器開了swap,作業系統發現物理記憶體佔滿後,並沒有立即釋放cache,導致每次GC,都和硬碟打一次交道。然後,所有服務不間歇卡頓了...

最終,只能先關閉swap分割槽,然後強制核心釋放cache,然後再開啟swap。當然這個過程也不會順利,因為開、關swap,同樣會引起大量I/O交換,所以不能批量去執行。這幾千臺機器,是要忙活一陣嘍。

小王的問題就簡單多了。他使用vim開啟大檔案,所有檔案的內容都會先載入到記憶體。結果,記憶體佔滿、接著swap也滿了,然後oom-killer殺死了服務程序,給一頭霧水的小王留下了個莫名其妙。

排查記憶體的一些命令

記憶體分兩部分,實體記憶體和swap。實體記憶體問題主要是記憶體洩漏,而swap的問題主要是用了swap~,我們先上一點命令。

(#1) 實體記憶體

#根據使用量排序檢視RES
top -> shift + m
#檢視程序使用的實體記憶體
ps -p 75 -o rss,vsz
#顯示記憶體的使用情況
free -h 
#使用sar檢視記憶體資訊
sar -r
#顯示記憶體每個區的詳情
cat /proc/meminfo 
#檢視slab區使用情況
slabtop
複製程式碼

通常,通過檢視實體記憶體的佔用,你發現不了多少問題,頂多發現那個程序佔用記憶體高(比如vim等旁路應用)。meminfo和slabtop對系統的全域性判斷幫助很大,但掌握這兩點坡度陡峭。

(#2) swap

#檢視si,so是否異常
vmstat 1 
#使用sar檢視swap
sar -W
#禁用swap
swapoff 
#查詢swap優先順序
sysctl -q vm.swappiness
#設定swap優先順序
sysctl vm.swappiness=10
複製程式碼

建議關注非0 swap的所有問題,即使你用了ssd。swap用的多,通常伴隨著I/O升高,服務卡頓。swap一點都不好玩,不信搜一下《swap罪與罰》這篇文章看下,千萬不要更暈哦。

(#3) jvm

# 檢視系統級別的故障和問題
dmesg
# 統計例項最多的類前十位 
jmap -histo pid | sort -n -r -k 2 | head -10
# 統計容量前十的類 
jmap -histo pid | sort -n -r -k 3 | head -10
複製程式碼

以上命令是看堆內的,能夠找到一些濫用集合的問題。堆外記憶體,依然推薦 《Java堆外記憶體排查小結》

(#4) 其他

# 釋放記憶體
echo 3 > /proc/sys/vm/drop_caches
#檢視程序實體記憶體分佈
pmap -x 75  | sort -n -k3
#dump記憶體內容
gdb --batch --pid 75 -ex "dump memory a.dump 0x7f2bceda1000 0x7f2bcef2b000"
複製程式碼

記憶體模型

二王的問題表象都是CPU問題,CPU都間歇性的增高,那是因為Linux的記憶體管理機制引起的。你去監控Linux的記憶體使用率,大概率是沒什麼用的。因為經過一段時間,剩餘的記憶體都會被各種快取迅速佔滿。一個比較典型的例子是ElasticSearch,分一半記憶體給JVM,剩下的一半會迅速被Lucene索引佔滿。

如果你的App程序啟動後,經過兩層緩衝後還不能落地,迎接它的,將會是oom killer

接下來的知識有些燒腦,但有些名詞,可能是你已經聽過多次的了。

作業系統視角

我們來解釋一下上圖,第一部分是邏輯記憶體和實體記憶體的關係;第二部分是top命令展示的一個結果,詳細的列出了每一個程序的記憶體使用情況;第三部分是free命令展示的結果,它的關係比較亂,所以給加上了箭頭來作說明。

  • 學過計算機組成結構的都知道,程式編譯後的地址是邏輯記憶體,需要經過翻譯才能對映到實體記憶體。這個管翻譯的硬體,就叫MMUTLB就是存放這些對映的小快取。記憶體特別大的時候,會涉及到**hugepage,在某些時候,是進行效能優化的殺手鐗,比如優化redis (THP,注意理解透徹前不要妄動)**

  • 實體記憶體的可用空間是有限的,所以邏輯記憶體對映一部分地址到硬碟上,以便獲取更大的實體記憶體地址,這就是swap分割槽。swap是很多效能場景的萬惡之源,建議禁用

  • top展示的欄位,RES才是真正的實體記憶體佔用(不包括swap,ps命令裡叫RSS)。在java中,代表了堆內+堆外記憶體的總和。而VIRT、SHR等,幾乎沒有判斷價值(某些場景除外)

  • 系統的可用記憶體,包括:free + buffers + cached,因為後兩者可以自動釋放。但不要迷信,有很大一部分,你是釋放不了的

  • slab區,是核心的快取檔案控制代碼等資訊等的特殊區域,slabtop命令可以看到具體使用

更詳細的,從/proc/meminfo檔案中可以看到具體的邏輯記憶體塊的大小。有多達40項的記憶體資訊,這些資訊都可以通過/proc一些檔案的遍歷獲取,本文只挑重點說明。

[[email protected] ~]$ cat /proc/meminfo
MemTotal:        3881692 kB
MemFree:          249248 kB
MemAvailable:    1510048 kB
Buffers:           92384 kB
Cached:          1340716 kB
40+ more ...
複製程式碼

oom-killer

以下問題已經不止一個小夥伴問了:我的java程序沒了,什麼都沒留下,就像個屁一樣蒸發不見了

why?是因為物件太多了麼?

執行dmesg命令,大概率會看到你的程序崩潰資訊躺屍在那裡。

為了能看到發生的時間,我們習慣性加上引數T

dmesg -T
複製程式碼

由於linux系統採用的是虛擬記憶體,程序的程式碼的使用都會消耗記憶體,但是申請出來的記憶體,只要沒真正access過,是不算的,因為沒有真正為之分配物理頁面。

第一層防護牆就是swap;當swap也用的差不多了,會嘗試釋放cache;當這兩者資源都耗盡,殺手就出現了。oom killer會在系統記憶體耗盡的情況下跳出來,選擇性的幹掉一些程序以求釋放一點記憶體。2.4核心殺新程序;2.6殺用的最多的那個。所以,買記憶體吧。

這個oom和jvm的oom可不是一個概念。順便,瞧一下我們的JVM堆在什麼位置。

例子

jvm記憶體溢位排查

應用程式釋出後,jvm持續增長。使用jstat命令,可以看到old區一直在增長。

jstat  -gcutil 28266 1000
複製程式碼

在jvm引數中,加入-XX:+HeapDumpOnOutOfMemoryError,在jvm oom的時候,生成hprof快照。然後,使用Jprofile、VisualVM、Mat等開啟dump檔案進行分析。

你要是個急性子,可以使用jmap立馬dump一份

jmap -heap:format=b pid
複製程式碼

最終發現,有一個全域性的Cache物件,不是guava的,也不是commons包的,是一個簡單的ConcurrentHashMap,結果越積累越多,最終導致溢位。

溢位的情況也有多種區別,這裡總結如下:

關鍵字 原因
Java.lang.OutOfMemoryError: Java heap space 堆記憶體不夠了,或者存在記憶體溢位
java.lang.OutOfMemoryError: PermGen space Perm區不夠了,可能使用了大量動態載入的類,比如cglib
java.lang.OutOfMemoryError: Direct buffer memory 堆外記憶體、作業系統沒記憶體了,比較嚴重的情況
java.lang.StackOverflowError 呼叫或者遞迴層次太深,修正即可
java.lang.OutOfMemoryError: unable to create new native thread 無法建立執行緒,作業系統記憶體沒有了,一定要預留一部分給作業系統,不要都給jvm
java.lang.OutOfMemoryError: Out of swap space 同樣沒有記憶體資源了,swap都用光了

jvm程式記憶體問題,除了真正的記憶體洩漏,大多數都是由於太貪心引起的。一個4GB的記憶體,有同學就把jvm設定成了3840M,只給作業系統256M,不死才怪。

另外一個問題就是swap了,當你的應用真正的高併發了,swap絕對能讓你體驗到它魔鬼性的一面:程序倒是死不了了,但GC時間長的無法忍受。

我的ES效能低

業務方的ES叢集宿主機是32GB的記憶體,隨著資料量和訪問量增加,決定對其進行擴容=>記憶體改成了64GB。

記憶體升級後,發現ES的效能沒什麼變化,某些時候,反而更低了。

通過檢視配置,發現有兩個問題引起。 一、64GB的機器分配給jvm的有60G,預留給檔案快取的只有4GB,造成了檔案快取和硬碟的頻繁交換,比較低效。 二、JVM大小超過了32GB,記憶體物件的指標無法啟用壓縮,造成了大量的記憶體浪費。由於ES的物件特別多,所以留給真正快取物件內容的記憶體反而減少了。

解決方式:給jvm的記憶體30GB即可。

其他

基本上了解了記憶體模型,上手幾次記憶體溢位排查,記憶體問題就算掌握了。但還有更多,這條知識系統可以深挖下去。

JMM

還是拿java來說。java中有一個經典的記憶體模型,一般面試到volitile關鍵字的時候,都會問到。其根本原因,就是由於執行緒引起的。

當兩個執行緒同時訪問一個變數的時候,就需要加所謂的鎖了。由於鎖有讀寫,所以java的同步方式非常多樣。wait,notify、lock、cas、volitile、synchronized等,我們僅放上volitile的讀可見性圖作下示例。

執行緒對共享變數會拷貝一份到工作區。執行緒1修改了變數以後,其他執行緒讀這個變數的時候,都從主存裡重新整理一份,此所謂讀可見。

JMM問題是純粹的記憶體問題,也是高階java必備的知識點。

CacheLine & False Sharing

是的,記憶體的工藝製造還是跟不上CPU的速度,於是聰明的硬體工程師們,就又給加了一個快取(哦不,是多個)。而Cache Line為CPU Cache中的最小快取單位。

這個快取是每個核的,而且大小固定。如果存在這樣的場景,有多個執行緒操作不同的成員變數,但是相同的快取行,這個時候會發生什麼?。沒錯,偽共享(False Sharing)問題就發生了!

偽共享也是高階java的必備技能(雖然幾乎用不到),趕緊去探索吧。

HugePage

回頭看我們最長的那副圖,上面有一個TLB,這個東西速度雖然高,但容量也是有限的。當訪問頻繁的時候,它會成為瓶頸。 TLB是存放Virtual Address和Physical Address的對映的。如圖,把對映闊上一些,甚至闊上幾百上千倍,TLB就能容納更多地址了。像這種將Page Size加大的技術就是Huge Page。

HugePage有一些副作用,比如競爭加劇(比如redis: redis.io/topics/late… )。但在大記憶體的現代,開啟後會一定程度上增加效能(比如oracle: docs.oracle.com/cd/E11882_0… )。

Numa

本來想將Numa放在cpu篇,結果發現numa改的其實是記憶體控制器。這個東西,將記憶體分段,分別"繫結"在不同的CPU上。也就是說,你的某核CPU,訪問一部分記憶體速度賊快,但訪問另外一些記憶體,就慢一些。

所以,Linux識別到NUMA架構後,預設的記憶體分配方案就是:優先嚐試在請求執行緒當前所處的CPU的記憶體上分配空間。如果繫結的記憶體不足,先去釋放繫結的記憶體。

以下命令可以看到當前是否是NUMA架構的硬體。

numactl --hardware
複製程式碼

NUMA也是由於記憶體速度跟不上給加的折衷方案。Swap一些難搞的問題,大多是由於NUMA引起的。

總結

本文的其他,是給想更深入理解記憶體結構的同學準備的提綱。Linux記憶體牽扯的東西實在太多,各種緩衝區就是魔術。如果你遇到了難以理解的現象,費了九牛二虎之力才找到原因,不要感到奇怪。對發生的這一切,我深表同情,並深切的渴望通用量子計算機的到來。

那麼問題來了,記憶體尚且如此,磁碟呢?