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