作者:京東科技 徐傳樂
背景
在高并發下,Java程式的GC問題屬于很典型的一類問題,帶來的影響往往會被進一步放大,不管是「GC頻率過快」還是「GC耗時太長」,由于GC期間都存在Stop The World問題,因此很容易導致服務超時,引發性能問題,
事情最初是線上某應用垃圾收集出現Full GC例外的現象,應用中個別實體Full GC時間特別長,持續時間約為15~30秒,平均每2周左右觸發一次;
JVM引數配置“-Xms2048M –Xmx2048M –Xmn1024M –XX:MaxPermSize=512M”
排查程序
? 分析GC 日志
GC 日志它記錄了每一次的 GC 的執行時間和執行結果,通過分析 GC 日志可以調優堆設定和 GC 設定,或者改進應用程式的物件分配模式,
這里Full GC的reason是Ergonomics,是因為開啟了UseAdaptiveSizePolicy,jvm自己進行自適應調整引發的Full GC,
這份日志主要體現GC前后的變化,目前為止看不出個所以然來,
開啟GC日志,需要添加如下 JVM 啟動引數:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/export/log/risk_pillar/gc.log
常見的 Young GC、Full GC 日志含義如下:
? 進一步查看服務器性能指標
獲取到了GC耗時的時間后,通過監控平臺獲取到各個監控項,開始排查這個時點有例外的指標,最終分析發現,在5.06分左右(GC的時點),CPU占用顯著提升,而SWAP出現了釋放資源、memory資源增長出現拐點的情況(詳見下圖紅色框,橙色框中的變化是因修改配置導致,后面會介紹,暫且可忽略)
JVM用到了swap?是因為GC導致的CPU突然飆升,并且釋放了swap交換區這部分記憶體到memory?
為了驗證JVM是否用到swap,我們通過檢查proc下的行程記憶體資源占用情況
| for i in $( cd /proc;ls |grep "^[0-9]"|awk ' $0 >100') ;do awk '/Swap:/{a=a+$2}END{print '"$i"',a/1024"M"}' /proc/$i/smaps 2>/dev/null ; done | sort -k2nr | head -10 # head -10 表示 取出 前10個記憶體占用高的行程 # 取出的第一列為行程的id 第二列行程占用swap大小 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
看到確實有用到305MB的swap
這里簡單介紹下什么是swap?
swap指的是一個交換磁區或檔案,主要是在記憶體使用存在壓力時,觸發記憶體回收,這時可能會將部分記憶體的資料交換到swap空間,以便讓系統不會因為記憶體不夠用而導致oom或者更致命的情況出現,
當某行程向OS請求記憶體發現不足時,OS會把記憶體中暫時不用的資料交換出去,放在swap磁區中,這個程序稱為swap out,
當某行程又需要這些資料且OS發現還有空閑物理記憶體時,又會把swap磁區中的資料交換回物理記憶體中,這個程序稱為swap in,
為了驗證GC耗時與swap操作有必然關系,我抽查了十幾臺機器,重點關注耗時長的GC日志,通過時間點確認到GC耗時的時間點與swap操作的時間點確實是一致的,
進一步查看虛擬機各實體 swappiness 引數,一個普遍現象是,凡是發生較長Full GC的實體都配置了引數 vm.swappiness = 30(值越大表示越傾向于使用swap);而GC時間相對正常的實體配置引數 vm.swappiness = 0(最大限度地降低使用swap),
swappiness 可以設定為 0 到 100 之間的值,它是Linux的一個內核引數,控制系統在進 行swap時,記憶體使用的相對權重,
? swappiness=0: 表示最大限度使用物理記憶體,然后才是 swap空間
? swappiness=100: 表示積極的使用swap磁區,并且把記憶體上的資料及時的交換到swap空間里面
對應的物理記憶體使用率和swap使用情況如下
至此,矛頭似乎都指向了swap,
? 問題分析
當記憶體使用率達到水位線(vm.swappiness)時,linux會把一部分暫時不使用的記憶體資料放到磁盤swap去,以便騰出更多可用記憶體空間;
當需要使用位于swap區的資料時,再將其換回記憶體中,當JVM進行GC時,需要對相應堆磁區的已用記憶體進行遍歷;
假如GC的時候,有堆的一部分內容被交換到swap空間中,遍歷到這部分的時候就需要將其交換回記憶體,由于需要訪問磁盤,所以相比物理記憶體,它的速度肯定慢的令人發指,GC停頓的時間一定會非常非常恐怖;
進而導致Linux對swap磁區的回收滯后(記憶體到磁盤換入換出操作十分占用CPU與系統IO),在高并發/QPS服務中,這種滯后帶來的結果是致命的(STW),
? 問題解決
至此,答案似乎很清晰,我們只需嘗試把swap關倍訓釋放掉,看看能否解決問題?
如何釋放swap?
- 設定vm.swappiness=0(重啟應用釋放swap后生效),表示盡可能不使用交換記憶體
a、 臨時設定方案,重啟后不生效
設定vm.swappiness為0
sysctl vm.swappiness=0
查看swappiness值
cat /proc/sys/vm/swappiness
b、 永久設定方案,重啟后仍然生效
vi /etc/sysctl.conf
添加
vm.swappiness=0
- 關閉交換磁區swapoff –a
前提:首先要保證記憶體剩余要大于等于swap使用量,否則會報Cannot allocate memory!swap磁區一旦釋放,所有存放在swap磁區的檔案都會轉存到物理記憶體上,可能會引發系統IO或者其他問題,
a、 查看當前swap磁區掛載在哪?
b、 關停磁區
關閉swap交換區后的記憶體變化見下圖橙色框,此時swap磁區的檔案都轉存到了物理記憶體上
關閉Swap交換區后,于2.23再次發生Full GC,耗時190ms,問題得到解決,
? 疑惑
1、 是不是只要開啟了swap交換區的JVM,在GC的時候都會耗時較長呢?
2、 既然JVM對swap如此不待見,為何JVM不明令禁止使用呢?
3、 swap作業機制是怎樣的?這臺物理記憶體為8g的server,使用了交換區記憶體(swap),說明物理記憶體不夠使用了,但是通過free命令查看記憶體使用情況,實際物理記憶體似乎并沒有占用那么多,反而Swap已占近1G?
free:除了buff/cache剩余了多少記憶體
shared:共享記憶體
buff/cache:緩沖、快取區記憶體數(使用過高通常是程式頻繁存取檔案)
available:真實剩余的可用記憶體數
大家可以想想,關閉交換磁盤快取意味著什么?
其實大可不必如此激進,要知道這個世界永遠不是非0即1的,大家都會或多或少選擇走在中間,不過有些偏向0,有些偏向1而已,
很顯然,在swap這個問題上,JVM可以選擇偏向盡量少用,從而降低swap影響,要降低swap影響有必要弄清楚Linux記憶體回收是怎么作業的,這樣才能不遺漏任何可能的疑點,
先來看看swap是如何觸發的?
Linux會在兩種場景下觸發記憶體回收,一種是在記憶體分配時發現沒有足夠空閑記憶體時會立刻觸發記憶體回收;另一種是開啟了一個守護行程(kswapd行程)周期性對系統記憶體進行檢查,在可用記憶體降低到特定閾值之后主動觸發記憶體回收,
通過如下圖示可以很容易理解,詳細資訊參見:http://hbasefly.com/2017/05/24/hbase-linux/
解答是不是只要開啟了swap交換區的JVM,在GC的時候都會耗時較長
筆者去查了一下另外的一個應用,相關指標資訊請見下圖,
實名服務的QPS是非常高的,同樣能看到應用了swap,GC平均耗時 576ms,這是為什么呢?
通過把時間范圍聚焦到發生GC的某一時間段,從監控指標圖可以看到swapUsed沒有任何變化,也就是說沒有swap活動,進而沒有影響到垃級回收的總耗時,
通過如下命令列舉出各行程swap空間占用情況,很清楚的看到實名這個服務swap空間占用的較少(僅54.2MB)
另一個顯著的現象是實名服務Full GC間隔較短(幾個小時一次),而我的服務平均間隔2周一次Full GC
基于以上推測
1、 實名服務由于 GC 間隔較短,記憶體中的東西根本沒有機會置換到swap中就被回收了,GC的時候不需要將swap磁區中的資料交換回物理記憶體中,完全基于記憶體計算,所以要快很多
2、 將哪些記憶體資料置換進swap交換區的篩選策略應該是類似于LRU演算法(最近最少使用原則)
為了證實上述猜測,我們只需跟蹤swap變更日志,監控資料變化即可得到答案,這里采用一段shell 腳本實作
#!/bin/bash
echo -e `date +%y%m%d%H%M%S`
echo -e "PID\t\tSwap\t\tProc_Name"
#拿出/proc目錄下所有以數字為名的目錄(行程名是數字才是行程,其他如sys,net等存放的是其他資訊)
for pid in `ls -l /proc | grep ^d | awk '{ print $9 }'| grep -v [^0-9]`
do
if [ $pid -eq 1 ];then continue;fi
grep -q "Swap" /proc/$pid/smaps 2>/dev/null
if [ $? -eq 0 ];then
swap=$(gawk '/Swap/{ sum+=$2;} END{ print sum }' /proc/$pid/smaps) #統計占用的swap磁區的 大小 單位是KB
proc_name=$(ps aux | grep -w "$pid" | awk '!/grep/{ for(i=11;i<=NF;i++){ printf("%s ",$i); }}') #取出行程的名字
if [ $swap -gt 0 ];then #判斷是否占用swap 只有占用才會輸出
echo -e "${pid}\t${swap}\t${proc_name:0:100}"
fi
fi
done | sort -k2nr | head -10 | gawk -F'\t' '{ #排序取前 10
pid[NR]=$1;
size[NR]=$2;
name[NR]=$3;
}
END{
for(id=1;id<=length(pid);id++)
{
if(size[id]<1024)
printf("%-10s\t%15sKB\t%s\n",pid[id],size[id],name[id]);
else if(size[id]<1048576)
printf("%-10s\t%15.2fMB\t%s\n",pid[id],size[id]/1024,name[id]);
else
printf("%-10s\t%15.2fGB\t%s\n",pid[id],size[id]/1048576,name[id]);
}
}'
由于上面圖中 2022.3.2 19:57:00 至 2022.3.2 19:58:00 發生了一次Full GC,我們重點關注下這一分鐘內swap交換區的變化即可,我這里每10s做一次資訊采集,可以看到在GC時點前后,swap確實沒有變化
通過上述分析,回歸本文核心問題上,現在看來我的處理方式過于激進了,其實也可以不用關閉swap,通過適當降低堆大小,也是能夠解決問題的,
這也側面的說明,部署Java服務的Linux系統,在記憶體分配上并不是無腦大而全,需要綜合考慮不同場景下JVM對Java永久代 、Java堆(新生代和老年代)、執行緒堆疊、Java NIO所使用記憶體的需求,
總結
綜上,我們得出結論,swap和GC同一時候發生會導致GC時間非常長,JVM嚴重卡頓,極端的情況下會導致服務崩潰,
主要原因是:JVM進行GC時,需要對對應堆磁區的已用記憶體進行遍歷,假如GC的時候,有堆的一部分內容被交換到swap中,遍歷到這部分的時候就須要將其交換回記憶體;更極端情況同一時刻因為記憶體空間不足,就需要把記憶體中堆的另外一部分換到SWAP中去,于是在遍歷堆磁區的程序中,會把整個堆磁區輪流往SWAP寫一遍,導致GC時間超長,線上應該限制swap區的大小,如果swap占用比例較高應該進行排查和解決,適當的時候可以通過降低堆大小,或者添加物理記憶體,
因此,部署Java服務的Linux系統,在記憶體分配上要慎重,
以上內容希望可以起到拋轉引玉的作用,如有理解不到位的地方煩請指出,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/541802.html
標籤:Java
