Java應用異常狀態監測
老闆最近分派了一個任務,說線上客戶在部署應用的時候發生了系統級別的OOM,觸發了OOM Killer殺掉了應用,讓我們解決這個問題。
對於這個任務,我從如下幾點開始調研、分析與解決。
1、什麼是系統級別的OOM(Out-Of-Memory)?
當建立程式時,程式都會建立起自己的虛擬地址空間(對於32位系統來說為4g)。這些虛擬地址空間並不等同於實體記憶體,只有程式訪問這些地址空間時,作業系統才會為其分配實體記憶體並建立對映。關於虛擬記憶體和實體記憶體有很多資料,這裡不再贅述,這篇文章寫的通俗易懂,可以看下。
通過虛擬記憶體技術,作業系統可以允許多個程式同時執行,即便它們的虛擬記憶體加起來遠超過系統的實體記憶體(和swap空間)。如果這些程式不斷訪問其虛擬地址,作業系統不得不為它們分配實體記憶體,當到達一個臨界點時,作業系統耗盡了所有的實體記憶體和swap空間,此時OOM就發生了。
2、系統發生了OOM會怎麼樣?
當發生了OOM,作業系統有兩個選擇:1)重啟系統;2)根據策略殺死特定的程式並且釋放其記憶體空間。這兩種策略當然是第二種影響面較小,由於我們線上系統也是採取殺死特定程式的策略,因此這裡只展開第二種。
- 第二種行為也稱之為OOM Killer。那系統會殺死什麼樣的程式釋放其記憶體呢?這篇文件的“Selecting a Process”部分大概描述了Linux核心的作業系統選取演算法:首先,根據
badness_for_task = total_vm_for_task / (sqrt(cpu_time_in_seconds) * sqrt(sqrt(cpu_time_in_minutes)))
來算起始值,total_vm_for_task為程式佔用的實際記憶體,cpu_time_in_seconds為執行時間,這個公式會選取佔用記憶體多且執行時間短的程式; - 如果程式是root程式或者擁有超級使用者許可權,那麼上述得分會除以4;
- 如果程式能夠直接訪問硬體(也就是硬體驅動),那麼將得分再除以4。
但文件中描述並不完整,這個是Linux核心OOM_Killer的相關程式碼,然後這篇文章對程式碼進行了分析,除了上述因素之外還包含子程式記憶體、nice值、omkill_adj等因素。
作業系統會對每個程式進行計算得分,並記錄在/proc/[pid]/oom_score檔案中;當發生系統OOM時,作業系統會選取評分最高的程式進行殺死。
3、如何實現系統OOM告警?
OOM告警有兩種方式,如下:
- 提前OOM告警:在系統即將發生OOM時,發出告警資訊。
- 事中/事後告警:在系統完成OOM Killer殺死程式後,發出告警資訊。
提前OOM告警是最好的方式,但實際上如果想達到不誤報、不漏報,實現難度極大。我們線上應用為Java應用,考慮這麼個場景:客戶應用不斷申請記憶體,當系統實體記憶體佔用率達到90%的時候,系統及應用下一步行為會是什麼樣?個人認為有三種可能性:1)Java應用停止申請記憶體,並且進行了垃圾回收釋放記憶體,這樣系統將會恢復正常;2)應用繼續申請記憶體導致應用記憶體超過了堆大小,但此時系統仍然有部分實體記憶體,這樣會發生Java應用的OOM;3)應用繼續申請記憶體導致系統耗盡實體記憶體,但此時沒有超過堆記憶體的最大值,這樣會發生作業系統的OOM。對於這個場景來說,我們想準確預判出系統及應用的下一步行為難度極大。
另一方面,我們線上其實已經有基於機器記憶體使用率的報警,這個報警其實已經包含了三種可能性:1)應用本身有問題但不會導致堆溢位或者系統OOM;2)應用可能會導致堆溢位;3)應用可能會導致系統OOM。無論實際情況為哪一種,這個報警都是有意義的。
事中/事後告警也是一種可取的方式,原因在於:1)這種方式能夠實現不誤報、不漏報;2)對於即將發生OOM的應用來說,事中報警與事前報警時間相差其實並不大。另外,到目前為止客訴的情況都是抱怨其應用死了沒有任何通知,排查起來既浪費了客戶時間,也浪費了研發排查問題的時間。
綜合考慮,如果能夠實現Java應用的異常狀態檢測並提供事中/事後報警與現場分析,也是很有意義的!
4、Java應用的異常狀態為哪些?
這裡定義的Java應用異常狀態有:
- Java應用被使用者殺死(Kill、Kill -9);
- Java應用發生堆溢位;
- Java應用被系統OOM(Kill -9)。
5、如何檢測出上述Java應用異常狀態?
首先,Java應用發生堆溢位可以通過-XX:+HeapDumpOnOutOfMemoryError
引數來生成dump資訊,我們可以通過輪詢方式即可發現是否發生堆溢位(當然基於事件通知方式更好,待調研)。
因此,現在問題在於我們怎麼發現一個Java應用被使用者殺死或者被系統OOM Kill掉?
5.1 ShutdownHook/sun.misc.Signal
老司機可能很快就想到,通過註冊shutdownHook就可以檢測到系統訊號了呀!註冊shutdownHook的確能檢測到SIGTERM訊號(也就是通常不帶引數的Kill命令,如Kill pid),但不能檢測到SIGKILL訊號(Kill -9)。另外,調研發現也可以通過sun.misc.Signal.handle
方法來檢測系統訊號,但遺憾的是還是不能檢測到SIGKILL訊號。
5.2 strace
這個工具非常強大,它能夠攔截所有的系統呼叫(包括SIGKILL),並且具有系統已經內建、使用方便、輸出資訊可讀性好等優點。下圖是我的一個實驗(程式24063是一個觸發系統OOM的Java程式):
但這個工具的缺點是,被跟蹤的應用的效能影響非常大。應用原來進行系統呼叫(比如open、read、write、close)時會發生一次上下文切換(從使用者態到核心態),使用了strace之後會變成多次上下文呼叫,如下圖所示:
(更多資訊可以參考這篇文章)
但無論如何,我們已經找到一種可行的解決方案,雖然效能影響很大,但可以作為debug方案開放給客戶。
5.3 ftrace + 系統日誌
ftrace是Linux系統已經內建的工具(debugfs掛載情況見附錄),它的作用是幫助開發人員瞭解 Linux 核心的執行時行為,以便進行故障除錯或效能分析。重要的是,它對應用本身的效能影響極小,而且我們可以只檢測Kill事件,這樣對客戶應用幾乎零影響(效能分析見第6節)。在我們的場景下,它也支援核心事件(包括程式SIGKILL訊號)監聽。ftrace使用起來非常方便,可以參考這篇文件,或者直接使用這個GITHUB指令碼即可。下面是執行該GITHUB指令碼的一個截圖:
在上圖中,SIGNAL為15的是我執行Kill 29265
命令,SIGNAL為9的是我執行Kill -9 29428
命令。但這個工具的問題在於,當Java程式觸發系統級別的OOM Killer時,並沒有檢測到相應的訊號(待進一步調研)。
另外,當系統觸發OOM Killer時,會在系統日誌(Centos的為/var/log/messages)中記錄下特定資訊,如下所示:
5.4 auditd + 系統日誌
(系統日誌用來發現OOM資訊,不再贅述,下文主要介紹auditd)
同事建議可以嘗試下auditd,因此這裡調研auditd,發現它能滿足需求,而且測試效能影響比ftrace更小(效能分析見第6節)。auditd是Linux Auditing System(Linux審計系統)的一部分,它負責接收核心中發生的事件(系統呼叫、檔案訪問),並將這些事件寫入日誌供使用者分析。
下圖是Linux審計系統的框架:
其中:
- 左邊是我們的應用程式;
- 中間為Linux核心,核心中包含了審計模組,可以記錄三類事件:1)User:記錄使用者產生的事件;2)Task:記錄任務型別(如fork子程式)事件;3)Exit:在系統呼叫結束時記錄該事件。同時,可以結合Exclude規則來過濾事件,最終將這些事件傳送到使用者空間的auditd守護程式;
- 右邊是在使用者空間的應用程式,其中auditd是核心的守護程式,主要接收核心中產生的事件,並記錄到audit.log中,然後我們可以通過ausearch或者aureport來檢視這些日誌;auditd在啟動時會讀取auditd.conf檔案來配置守護程式的各種行為(如日誌檔案存放位置),並讀取audit.rules中的事件規則來控制核心中的事件監聽及過濾行為;另外,我們也可以通過auditctl來控制核心事件監聽和過濾規則。
關於更多資訊可以自行搜尋或者看下這篇文章。
核心已經內建審計模組,而auditd守護程式也預設在centos(>=6.8)中啟動,下面我們來測試下該工具。首先,我們執行如下命令:
auditctl -a always,exit -F arch=b64 -S kill -k test_kill
這條命令作用是,在kill系統呼叫返回時記錄事件,並且繫結test_kill標記(以便後面進行日誌篩選)。然後,我們可以隨便執行一個指令碼並kill掉,可以在/var/log/audit/audit.log中看到如下輸出:
第一條SYSCALL日誌記錄傳送SIGKILL訊號的程式資訊,第二條OBJ_PID日誌記錄接收SIGKILL訊號的程式資訊。
5.5 Shell + dmesg
如果我們能夠控制Java應用的啟動指令碼,那麼此方式是影響最小的方案。先看下面這個shell指令碼:
#!/bin/bash
java -Xms4g -Xmx4g Main
ret=$?
#
# returns > 127 are a SIGNAL
#
if [ $ret -gt 127 ]; then
sig=$((ret - 128))
echo "Got SIGNAL $sig"
if [ $sig -eq $(kill -l SIGKILL) ]; then
echo "process was killed with SIGKILL"
dmesg > $HOME/dmesg-kill.log
fi
fi
這個指令碼做了這幾個事情:
- 使用Java -Xms4g -Xmx4g Main來啟動一個Java應用;
- Java應用退出後通過$?獲取程式退出狀態碼;
- 如果退出碼大於128,則為應用收到SIGNAL退出;如果為SIGKILL,則通過dmesg收集kernal ring buffer中的資訊。
如果應用由於被OOM Killer殺死而退出,則dmesg-kill.log中會有如下資訊:
此方案優點在於影響面最小,但程式殺死資訊量相比auditd少,只知道收到何種SIGNAL訊號;而auditd能夠知道SIGNAL訊號來源於哪個程式、使用者、組。
6、效能測試
6.1 測試環境
測試機器 | ecs.n1.medium |
CPU | 2 vCPU |
處理器型號 | Intel Xeon E5-2680v3 |
處理器主頻 | 2.5 GHz |
記憶體 | 4 GB |
系統映象 | CentOS 7.4 64位 |
6.2 測試指令碼
6.2.1 測試一:系統呼叫效能影響
測試方法
從/dev/zero中讀取500個位元組資料並寫入到/dev/null中,迴圈執行1億次(也就是100M):
dd if=/dev/zero of=/dev/null bs=500 count=100M
該指令碼會產生大約2億次系統呼叫(read 1億次,write 1億次)。
測試結果
測試目標 | 總耗時(s) | 平均耗時(μs) |
不加任何事件監聽 | 41.7 | 0.2085 |
auditd | 47.1 | 0.2355 |
ftrace | 77.3 | 0.3865 |
strace | > 3600 | > 18 |
6.2.2 測試二:JAVA應用效能影響
測試方法:
構造consumer和provider應用,consumer向provider發起HSF呼叫,provider返回預定義資料,迴圈呼叫1百萬次,觀察consumer耗時。
測試結果:
測試目標 | 總耗時(s) | 平均耗時(ms) |
不加任何事件監聽 | 492 | 0.492 |
consumer和provider都開啟auditd | 484 | 0.482 |
consumer和provider都開啟ftrace | 493 | 0.493 |
consumer和provider都開啟strace | > 3600 | > 3.6 |
7、總結
綜上,我們可以通過如下手段來解決客戶的應用OOM問題:
- 1. 使用機器的基於記憶體使用率報警來事前通知客戶;
- 2. JVM啟動引數可以新增
-XX:+HeapDumpOnOutOfMemoryError
等引數來協助收集JVM記憶體溢位資訊; - 3. 通過系統日誌(/var/log/messages)或者dmesg來收集系統OOM Killer資訊;
- 4. 使用啟動shell指令碼(見5.5節)或auditd(見5.4節) ftrace 來獲取應用被Kill掉的資訊(可能被客戶自身Kill掉)。
- 5.【可選】開放strace工具來幫助客戶debug問題。
8、其他工具
8.1 trap
trap命令用於指定在接收到訊號後將要採取的動作,通常在指令碼程式被中斷時完成清理工作。當shell接收到sigspec指定的訊號時,arg引數(命令)將會被讀取,並被執行。下面我試圖攔截當前指令碼的SIGTERM和SIGKILL訊號:
#!/bin/bash
sighdl ()
{
echo "signal caught"
#do something
exit 0
}
trap sighdl SIGKILL SIGTERM
### main script
X=0
while :
do
echo "X=$X"
X=`expr ${X} + 1`
sleep 1
done
測試發現,trap命令能夠檢測到當前程式的SIGTERM訊號,但是無法檢測SIGKILL訊號。這個命令相當於Java應用中的shutdownHook或者Signal。
9、附錄
9.1 ftrace系統debugfs掛載情況
作業系統 | 系統版本 | debugfs是否已經預設掛載 | 備註 |
CentOS | |||
7.4(64位) | 是 | ||
7.3(64位) | 是 | ||
7.2(64位) | 是 | ||
6.9(64位) | 否 | mount -t debugfs nodev /sys/kernel/debug | |
6.8(64位) | 否 | mount -t debugfs nodev /sys/kernel/debug | |
6.8(32位) | 否 | mount -t debugfs nodev /sys/kernel/debug | |
Aliyun Linux | |||
17.1(64位) | 是 | ||
Ubuntu | |||
16.04(64位) | 是 | ||
16.04(32位) | 是 | ||
14.04(64位) | 是 | ||
14.04(32位) | 是 | ||
Debian | |||
9.2(64位) | 是 | ||
8.9(64位) | 是 | ||
SUSE Linux | |||
Enterprise Server 12 SP2(64位) | 是 | ||
Enterprise Server 11 SP2(64位) | 是 | ||
OpenSUSE | |||
42.3(64位) | 是 | ||
CoreOS | |||
1465.8.0(64位) | 是 | ||
FreeBSD | |||
11.1(64位) | 否 |
相關文章
- 如何實現UPS電源開關機狀態遠端監測和異常告警?
- 使用jstack檢測Java應用的死鎖(deadlock)狀態JSJava
- openwrt,狀態監測netdata
- 使用 sentry 除錯應用和監控異常除錯
- SAP BSP應用有狀態和無狀態行為差異比較
- TCP連線狀態異常記錄TCP
- zabbix應用-監控mysql slave 主從狀態MySql
- Flutter異常監測與上報Flutter
- 伺服器異常狀態怎麼解決伺服器
- dns解析狀態異常怎麼處理 dns解析異常怎麼修復DNS
- python應用:異常處理Python
- 前端異常監控前端
- java程式佔用cpu異常升高Java
- 【java】異常Java
- java 異常Java
- Java 異常Java
- Java異常Java
- 異常JavaJava
- Java 異常(二) 自定義異常Java
- Win10提示lsp狀態異常如何解決 lsp網路連線異常的方法Win10
- Elastic 使用Heartbeat監測服務執行狀態AST
- K8S線上叢集排查,實測排查Node節點NotReady異常狀態K8S
- Flutter異常監控 - 伍 | 關於異常監控框架設計的思考Flutter框架
- 異常檢測
- 探索FSM (有限狀態機)應用
- 基於多模態對抗學習的無監督時間序列異常檢測
- python requests檢測響應狀態碼Python
- [轉載] java避免空指標異常_第1部分:在現代Java應用程式中避免空指標異常Java指標
- jmu-Java-06異常-01-常見異常Java
- Java 異常表與異常處理原理Java
- 小程式異常監控收集
- Oracle 監聽異常處理Oracle
- 前端異常監控實踐前端
- 異常監控和判斷
- 使用 Provider 管理 Flutter 應用狀態 (下)IDEFlutter
- 使用 Provider 管理 Flutter 應用狀態 (上)IDEFlutter
- ASP.NET Core 應用程式狀態ASP.NET
- Kubernetes 實戰——有狀態應用(StatefulSet)