Linux之《荒岛余生》(三)内存篇
前言
小公司請求量小,但喜歡濫用內(nèi)存,開一堆線程,大把大把往jvm塞對象,最終問題是內(nèi)存溢出。
大公司并發(fā)大,但喜歡強調(diào)HA,所以通常保留swap,最終問題是服務卡頓。
而喜歡用全局集合變量的某些同仁,把java代碼當c寫,對象塞進去但忘了銷毀,最終問題是內(nèi)存泄漏。
如何避免?
合理參數(shù)、優(yōu)雅代碼、禁用swap,三管齊下,trouble shooter(解決問題的人)。
從一個故事開始
老王的疑問
一個陽光明媚的下午,一條報警短信彈了出來。老王微微一笑,是cpu問題,idle瞬時值,大概是某批請求比較大引起的峰值問題。老王每天都會收到這樣的短信,這樣的一個小峰值,在數(shù)千臺服務器中,不過是滄海一栗,繼續(xù)喝茶就是了。
但,這次不一樣。幾分鐘之后,幾百個服務的超時報警鋪天蓋地到來。事后老王算了一下,大概千分之零點幾的服務超時了,不過這已經(jīng)很恐怖了。
事態(tài)升級,恐怕沒時間喝茶了。
大面積報警,應該是全局問題,是網(wǎng)絡卡頓?還是數(shù)據(jù)庫抽風?老王挑了一臺最近報警的服務器,輪流監(jiān)控了各種狀態(tài),總結(jié)如下:
- cpu偶爾有瞬時峰值,但load非常正常
- 內(nèi)存雖然free不多了,但cached還有不少
- 網(wǎng)絡各種ping,基本正常
- 磁盤I/O一般,畢竟是服務計算節(jié)點
- 數(shù)據(jù)庫連接池穩(wěn)定,應該不是db抽風
- swap用了不少,但好像每臺機器都用了,沒啥大不了
全局性的東西不太多,網(wǎng)關(guān)、LVS、注冊中心、DB、MQ,好像都沒問題。老王開始腦瓜疼了。
讓老王休息一下,我們把鏡頭轉(zhuǎn)向小王。
小王的操作
小王不是老王的兒子,他是老王的徒弟。徒弟一思考,導師就發(fā)笑。這次小王用的是vim,想查找一個Exception(例外),他打開了一個8GB的日志文件,然后樂呵呵的在那等著加載。然后,服務器就死了。
答案
這里直接給出答案,原因等讀完本文自然會了解。
老王的問題最終定位到是由于某個運維工程師使用ansible批量執(zhí)行了一句命令
find / | grep "x"他是想找一個叫做x的文件,看看在哪臺服務器上。結(jié)果,這些老服務器由于文件太多,掃描后這些文件信息都緩存到了slab區(qū)。而服務器開了swap,操作系統(tǒng)發(fā)現(xiàn)物理內(nèi)存占滿后,并沒有立即釋放cache,導致每次GC,都和硬盤打一次交道。然后,所有服務不間歇卡頓了…
最終,只能先關(guān)閉swap分區(qū),然后強制內(nèi)核釋放cache,然后再開啟swap。當然這個過程也不會順利,因為開、關(guān)swap,同樣會引起大量I/O交換,所以不能批量去執(zhí)行。這幾千臺機器,是要忙活一陣嘍。
小王的問題就簡單多了。他使用vim打開大文件,所有文件的內(nèi)容都會先加載到內(nèi)存。結(jié)果,內(nèi)存占滿、接著swap也滿了,然后oom-killer殺死了服務進程,給一頭霧水的小王留下了個莫名其妙。
排查內(nèi)存的一些命令
內(nèi)存分兩部分,物理內(nèi)存和swap。物理內(nèi)存問題主要是內(nèi)存泄漏,而swap的問題主要是用了swap~,我們先上一點命令。
(#1) 物理內(nèi)存
#根據(jù)使用量排序查看RES top -> shift + m #查看進程使用的物理內(nèi)存 ps -p 75 -o rss,vsz #顯示內(nèi)存的使用情況 free -h #使用sar查看內(nèi)存信息 sar -r #顯示內(nèi)存每個區(qū)的詳情 cat /proc/meminfo #查看slab區(qū)使用情況 slabtop通常,通過查看物理內(nèi)存的占用,你發(fā)現(xiàn)不了多少問題,頂多發(fā)現(xiàn)那個進程占用內(nèi)存高(比如vim等旁路應用)。meminfo和slabtop對系統(tǒng)的全局判斷幫助很大,但掌握這兩點坡度陡峭。
(#2) swap
#查看si,so是否異常 vmstat 1 #使用sar查看swap sar -W #禁用swap swapoff #查詢swap優(yōu)先級 sysctl -q vm.swappiness #設(shè)置swap優(yōu)先級 sysctl vm.swappiness=10建議關(guān)注非0 swap的所有問題,即使你用了ssd。swap用的多,通常伴隨著I/O升高,服務卡頓。swap一點都不好玩,不信搜一下《swap罪與罰》這篇文章看下,千萬不要更暈哦。
(#3) jvm
# 查看系統(tǒng)級別的故障和問題 dmesg # 統(tǒng)計實例最多的類前十位 jmap -histo pid | sort -n -r -k 2 | head -10 # 統(tǒng)計容量前十的類 jmap -histo pid | sort -n -r -k 3 | head -10以上命令是看堆內(nèi)的,能夠找到一些濫用集合的問題。堆外內(nèi)存,依然推薦
《Java堆外內(nèi)存排查小結(jié)》
(#4) 其他
# 釋放內(nèi)存 echo 3 > /proc/sys/vm/drop_caches #查看進程物理內(nèi)存分布 pmap -x 75 | sort -n -k3 #dump內(nèi)存內(nèi)容 gdb --batch --pid 75 -ex "dump memory a.dump 0x7f2bceda1000 0x7f2bcef2b000"內(nèi)存模型
二王的問題表象都是CPU問題,CPU都間歇性的增高,那是因為Linux的內(nèi)存管理機制引起的。你去監(jiān)控Linux的內(nèi)存使用率,大概率是沒什么用的。因為經(jīng)過一段時間,剩余的內(nèi)存都會被各種緩存迅速占滿。一個比較典型的例子是ElasticSearch(
彈性搜索),分一半內(nèi)存給JVM,剩下的一半會迅速被Lucene(全文搜索)索引占滿。
如果你的App進程啟動后,經(jīng)過兩層緩沖后還不能落地,迎接它的,將會是oom killer。
接下來的知識有些燒腦,但有些名詞,可能是你已經(jīng)聽過多次的了。
操作系統(tǒng)視角
我們來解釋一下上圖,第一部分是邏輯內(nèi)存和物理內(nèi)存的關(guān)系;第二部分是top命令展示的一個結(jié)果,詳細的列出了每一個進程的內(nèi)存使用情況;第三部分是free命令展示的結(jié)果,它的關(guān)系比較亂,所以給加上了箭頭來作說明。
- 學過計算機組成結(jié)構(gòu)的都知道,程序編譯后的地址是邏輯內(nèi)存,需要經(jīng)過翻譯才能映射到物理內(nèi)存。這個管翻譯的硬件,就叫MMU;TLB就是存放這些映射的小緩存。內(nèi)存特別大的時候,會涉及到hugepage,在某些時候,是進行性能優(yōu)化的殺手锏,比如優(yōu)化redis (THP,注意理解透徹前不要妄動)
- 物理內(nèi)存的可用空間是有限的,所以邏輯內(nèi)存映射一部分地址到硬盤上,以便獲取更大的物理內(nèi)存地址,這就是swap分區(qū)。swap是很多性能場景的萬惡之源,建議禁用
- 像top展示的字段,RES才是真正的物理內(nèi)存占用(不包括swap,ps命令里叫RSS)。在java中,代表了堆內(nèi)+堆外內(nèi)存的總和。而VIRT、SHR等,幾乎沒有判斷價值(某些場景除外)
- 系統(tǒng)的可用內(nèi)存,包括:free + buffers + cached,因為后兩者可以自動釋放。但不要迷信,有很大一部分,你是釋放不了的
- slab區(qū),是內(nèi)核的緩存文件句柄等信息等的特殊區(qū)域,slabtop命令可以看到具體使用
更詳細的,從/proc/meminfo文件中可以看到具體的邏輯內(nèi)存塊的大小。有多達40項的內(nèi)存信息,這些信息都可以通過/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
以下問題已經(jīng)不止一個小伙伴問了:我的java進程沒了,什么都沒留下,就像個屁一樣蒸發(fā)不見了
why?是因為對象太多了么?
執(zhí)行dmesg命令,大概率會看到你的進程崩潰信息躺尸在那里。
為了能看到發(fā)生的時間,我們習慣性加上參數(shù)T
由于linux系統(tǒng)采用的是虛擬內(nèi)存,進程的代碼,庫,堆和棧的使用都會消耗內(nèi)存,但是申請出來的內(nèi)存,只要沒真正access過,是不算的,因為沒有真正為之分配物理頁面。
第一層防護墻就是swap;當swap也用的差不多了,會嘗試釋放cache;當這兩者資源都耗盡,殺手就出現(xiàn)了。oom killer會在系統(tǒng)內(nèi)存耗盡的情況下跳出來,選擇性的干掉一些進程以求釋放一點內(nèi)存。2.4內(nèi)核殺新進程;2.6殺用的最多的那個。所以,買內(nèi)存吧。
這個oom和jvm的oom可不是一個概念。順便,瞧一下我們的JVM堆在什么位置。
例子
jvm內(nèi)存溢出排查
應用程序發(fā)布后,jvm持續(xù)增長。使用jstat命令,可以看到old區(qū)一直在增長。
jstat -gcutil 28266 1000在jvm參數(shù)中,加入-XX:+HeapDumpOnOutOfMemoryError,在jvm oom的時候,生成hprof快照。然后,使用Jprofile、VisualVM、Mat等打開dump文件進行分析。
你要是個急性子,可以使用jmap立馬dump一份
jmap -heap:format=b pid最終發(fā)現(xiàn),有一個全局的Cache對象,不是guava的,也不是commons包的,是一個簡單的ConcurrentHashMap,結(jié)果越積累越多,最終導致溢出。
溢出的情況也有多種區(qū)別,這里總結(jié)如下:
| Java.lang.OutOfMemoryError: Java heap space | 堆內(nèi)存不夠了,或者存在內(nèi)存溢出 |
| java.lang.OutOfMemoryError: PermGen space | Perm區(qū)不夠了,可能使用了大量動態(tài)加載的類,比如cglib |
| java.lang.OutOfMemoryError: Direct buffer memory | 堆外內(nèi)存、操作系統(tǒng)沒內(nèi)存了,比較嚴重的情況 |
| java.lang.StackOverflowError | 調(diào)用或者遞歸層次太深,修正即可 |
| java.lang.OutOfMemoryError: unable to create new native thread | 無法創(chuàng)建線程,操作系統(tǒng)內(nèi)存沒有了,一定要預留一部分給操作系統(tǒng),不要都給jvm |
| java.lang.OutOfMemoryError: Out of swap space | 同樣沒有內(nèi)存資源了,swap都用光了 |
jvm程序內(nèi)存問題,除了真正的內(nèi)存泄漏,大多數(shù)都是由于太貪心引起的。一個4GB的內(nèi)存,有同學就把jvm設(shè)置成了3840M,只給操作系統(tǒng)256M,不死才怪。
另外一個問題就是swap了,當你的應用真正的高并發(fā)了,swap絕對能讓你體驗到它魔鬼性的一面:進程倒是死不了了,但GC時間長的無法忍受。
我的ES性能低
業(yè)務方的ES集群宿主機是32GB的內(nèi)存,隨著數(shù)據(jù)量和訪問量增加,決定對其進行擴容=>內(nèi)存改成了64GB。
內(nèi)存升級后,發(fā)現(xiàn)ES的性能沒什么變化,某些時候,反而更低了。
通過查看配置,發(fā)現(xiàn)有兩個問題引起。
一、64GB的機器分配給jvm的有60G,預留給文件緩存的只有4GB,造成了文件緩存和硬盤的頻繁交換,比較低效。
二、JVM大小超過了32GB,內(nèi)存對象的指針無法啟用壓縮,造成了大量的內(nèi)存浪費。由于ES的對象特別多,所以留給真正緩存對象內(nèi)容的內(nèi)存反而減少了。
解決方式:給jvm的內(nèi)存30GB即可。
其他
基本上了解了內(nèi)存模型,上手幾次內(nèi)存溢出排查,內(nèi)存問題就算掌握了。但還有更多,這條知識系統(tǒng)可以深挖下去。
JMM
還是拿java來說。java中有一個經(jīng)典的內(nèi)存模型,一般面試到volitile關(guān)鍵字的時候,都會問到。其根本原因,就是由于線程引起的。
當兩個線程同時訪問一個變量的時候,就需要加所謂的鎖了。由于鎖有讀寫,所以java的同步方式非常多樣。wait,notify、lock、cas、volitile、synchronized等,我們僅放上volitile的讀可見性圖作下示例。
線程對共享變量會拷貝一份到工作區(qū)。線程1修改了變量以后,其他線程讀這個變量的時候,都從主存里刷新一份,此所謂讀可見。
JMM問題是純粹的內(nèi)存問題,也是高級java必備的知識點。
CacheLine & False Sharing
是的,內(nèi)存的工藝制造還是跟不上CPU的速度,于是聰明的硬件工程師們,就又給加了一個緩存(哦不,是多個)。而Cache Line為CPU Cache中的最小緩存單位。
這個緩存是每個核的,而且大小固定。如果存在這樣的場景,有多個線程操作不同的成員變量,但是相同的緩存行,這個時候會發(fā)生什么?。沒錯,偽共享(False Sharing)問題就發(fā)生了!
偽共享也是高級java的必備技能(雖然幾乎用不到),趕緊去探索吧。
HugePage
回頭看我們最長的那副圖,上面有一個TLB,這個東西速度雖然高,但容量也是有限的。當訪問頻繁的時候,它會成為瓶頸。
TLB是存放Virtual Address和Physical Address的映射的。如圖,把映射闊上一些,甚至闊上幾百上千倍,TLB就能容納更多地址了。像這種將Page Size加大的技術(shù)就是Huge Page。
HugePage有一些副作用,比如競爭加劇(比如redis: https://redis.io/topics/latency )。但在大內(nèi)存的現(xiàn)代,開啟后會一定程度上增加性能(比如oracle: https://docs.oracle.com/cd/E11882_01/server.112/e10839/appi_vlm.htm )。
Numa
本來想將Numa放在cpu篇,結(jié)果發(fā)現(xiàn)numa改的其實是內(nèi)存控制器。這個東西,將內(nèi)存分段,分別”綁定”在不同的CPU上。也就是說,你的某核CPU,訪問一部分內(nèi)存速度賊快,但訪問另外一些內(nèi)存,就慢一些。
所以,Linux識別到NUMA架構(gòu)后,默認的內(nèi)存分配方案就是:優(yōu)先嘗試在請求線程當前所處的CPU的內(nèi)存上分配空間。如果綁定的內(nèi)存不足,先去釋放綁定的內(nèi)存。
以下命令可以看到當前是否是NUMA架構(gòu)的硬件。
numactl --hardwareNUMA也是由于內(nèi)存速度跟不上給加的折衷方案。Swap一些難搞的問題,大多是由于NUMA引起的。
總結(jié)
本文的其他,是給想更深入理解內(nèi)存結(jié)構(gòu)的同學準備的提綱。Linux內(nèi)存牽扯的東西實在太多,各種緩沖區(qū)就是魔術(shù)。如果你遇到了難以理解的現(xiàn)象,費了九牛二虎之力才找到原因,不要感到奇怪。對發(fā)生的這一切,我深表同情,并深切的渴望通用量子計算機的到來。
那么問題來了,內(nèi)存尚且如此,磁盤呢?
總結(jié)
以上是生活随笔為你收集整理的Linux之《荒岛余生》(三)内存篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Shopee面试问题整理
- 下一篇: Part14:Pandas批量拆分与合并