記憶體問題,腦瓜疼腦瓜疼。腦瓜疼的意思,就是腦袋運算空間太小,撐的疼。本篇是《荒島餘生》系列第三篇,讓人腦瓜疼的記憶體篇。其餘參見:
小公司請求量小,但喜歡濫用記憶體,開一堆執行緒,大把大把往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命令展示的結果,它的關係比較亂,所以給加上了箭頭來作說明。-
學過計算機組成結構的都知道,程式編譯後的地址是邏輯記憶體,需要經過翻譯才能對映到實體記憶體。這個管翻譯的硬體,就叫
MMU
;TLB
就是存放這些對映的小快取。記憶體特別大的時候,會涉及到**hugepage
,在某些時候,是進行效能優化的殺手鐗,比如優化redis (THP,注意理解透徹前不要妄動)** -
實體記憶體的可用空間是有限的,所以邏輯記憶體對映一部分地址到硬碟上,以便獲取更大的實體記憶體地址,這就是
swap
分割槽。swap是很多效能場景的萬惡之源,建議禁用 -
像
top
展示的欄位,RES
才是真正的實體記憶體佔用(不包括swap,ps命令裡叫RSS
)。在java中,代表了堆內+堆外記憶體的總和。而VIRT、SHR等,幾乎沒有判斷價值(某些場景除外) -
系統的可用記憶體,包括:
free
+buffers
+cached
,因為後兩者可以自動釋放。但不要迷信,有很大一部分,你是釋放不了的 -
slab區,是核心的快取檔案控制程式碼等資訊等的特殊區域,slabtop命令可以看到具體使用
更詳細的,從/proc/meminfo
檔案中可以看到具體的邏輯記憶體塊的大小。有多達40項的記憶體資訊,這些資訊都可以通過/proc一些檔案的遍歷獲取,本文只挑重點說明。
[xjj@localhost ~]$ 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記憶體牽扯的東西實在太多,各種緩衝區就是魔術。如果你遇到了難以理解的現象,費了九牛二虎之力才找到原因,不要感到奇怪。對發生的這一切,我深表同情,並深切的渴望通用量子計算機的到來。
那麼問題來了,記憶體尚且如此,磁碟呢?