使用BPF監視JVM應用程序
參考文章: Fast and secure JVM application monitoring with BPF magic and JVM-BPF
一、Linux跟蹤工具的概況
X軸爲信息詳細程度,Y軸爲易於使用情況
ftrace
是一種內置機制,其已包含在Linux中。它是一種跟蹤性能問題等的工具。但是,它並非設計用於Java。它不能用於跟蹤許多有趣的Java事件,例如垃圾回收,類加載等SystemTap
是一種非常底層的工具,可以跟蹤用戶空間中的內核事件和用戶事件 (包括Java事件)。其主要問題是,要使用此工具,需要在其執行期間編譯內核模塊並將其加載到內核中SysDig
關注於跟蹤容器中的性能,但是它只能與系統調用一起使用,提供的功能較少
二、JVM中的監視點
在運行的Java應用程序中,除了由操作系統執行的操作 (系統調用,網絡事件) 之外,在大多數最新的Java版本中,OpenJDK還配備了許多USDT探針。它們中的大多數都是開箱即用的。
1. 探針
要探索其中的一些探針,請導航至$JAVA_HOME
並查看tapset目錄, 或者查看在線文件.
這些.stp
文件包含一堆探針的描述和聲明,包括其參數。例如,嘗試查找gc_collect_tenured_begin
和gc_collect_tenured_end
探針描述
# 可選, 通過yum安裝1.8.0版本openjdk
$ yum install java-1.8.0-openjdk* -y
# 路徑
$ cd /etc/alternatives/java_sdk
$ ls tapset/
hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp
hotspot_jni-1.8.0.242.b08-0.el7_7.x86_64.stp
hotspot_gc-1.8.0.242.b08-0.el7_7.x86_64.stp
jstack-1.8.0.242.b08-0.el7_7.x86_64.stp
現在,讓我們來看一個更實際的例子。現在,通過使用class_loaded
探針來跟蹤正在運行的Java應用程序,並查看正在加載哪些類。首先,通過運行以下命令: 看起來有四個參數,並且正在加載的類名作爲第一個參數給出
$ grep -A 10 'probe.*class_loaded' tapset/*.stp
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp:probe hotspot.class_loaded =
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp- process("/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/jre/lib/amd64/server/libjvm.so").mark("class__loaded")
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp-{
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp- name = "class_loaded";
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp- class = user_string_n($arg1, $arg2);
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp- classloader_id = $arg3;
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp- is_shared = $arg4;
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp- probestr = sprintf("%s(class='%s',classloader_id=0x%x,is_shared=%d)",
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp- name, class, classloader_id, is_shared);
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp-}
tapset/hotspot-1.8.0.242.b08-0.el7_7.x86_64.stp-
1. readelf
Linux有一個 readelf
工具,一般用於查看ELF格式的文件信息,常見的文件如在Linux上的可執行文件。
這是獲取這些跟蹤點列表的一種方法,其描述了那些嵌入在系統上任何Java應用程序中的跟蹤點。我們看到Java事件: class_loaded
,thread_start
,object_alloc
等
$ find /usr/lib/jvm -name libjvm.so -exec readelf -n {} + | grep -A2 NT_STAPSDT
# output
stapsdt 0x00000078 NT_STAPSDT (SystemTap probe descriptors)
Provider: hotspot
Name: mem__pool__gc__begin
--
stapsdt 0x0000004d NT_STAPSDT (SystemTap probe descriptors)
Provider: hotspot
Name: class__loaded
--
stapsdt 0x0000004e NT_STAPSDT (SystemTap probe descriptors)
Provider: hotspot
Name: object__alloc
--
stapsdt 0x00000065 NT_STAPSDT (SystemTap probe descriptors)
Provider: hotspot
Name: thread__start
...
2. tplist
tplist
是BCC中的一個工具,可以顯示來自二進制文件或正在運行的進程的probes
列表
此時,我們可以運行一個Java應用程序
$ java slowy/App
現在,使用tplist
以下命令發現可用的跟蹤點:
$ ./tplist.py -p `pidof java`
# ./tplist.py -p 19322 '*class*loaded'
/usr/lib/jvm/.../server/libjvm.so hotspot:object__alloc
/usr/lib/jvm/.../server/libjvm.so hotspot:method__entry
/usr/lib/jvm/.../server/libjvm.so hotspot:method__return
/usr/lib/jvm/.../server/libjvm.so hotspot:monitor__waited
/usr/lib/jvm/.../server/libjvm.so hotspot:monitor__wait
/usr/lib/jvm/.../server/libjvm.so hotspot:thread__stop
...
三、方法和堆棧跟蹤
trace
: 參與了系統級事件的跟蹤,並同時從這些事件中接收了一系列Java調用。無需重新啓動Java應用程序進行重新編譯。
示例1: 每當Java應用程序加載類時,您都應該獲得一條跟蹤消息
$ sudo find / -name libjvm.so # 得到libjvm.so文件路徑 path
$ ./trace.py 'u:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/jre/lib/amd64/server/libjvm.so:class__loaded "%s", arg1'
PID TID COMM FUNC -
...
11881 11882 java class__loaded java/security/UnresolvedPermission
11881 11882 java class__loaded java/security/BasicPermissionCollection
11881 11882 java class__loaded slowy/App
11881 11882 java class__loaded sun/launcher/LauncherHelper$FXHelper
11881 11882 java class__loaded java/lang/Class$MethodArray
11881 11882 java class__loaded java/io/IOException
11881 11882 java class__loaded java/lang/Void
11881 11882 java class__loaded java/util/Formatter
11881 11882 java class__loaded java/util/regex/Pattern
11881 11882 java class__loaded java/util/regex/Pattern$Node
...
示例2: Java應用程序將消息打印到控制檯, 我們需要找出發送這些消息的代碼
前提條件: -XX:+PreserveFramePointer
, 可以獲得更精確的堆棧跟蹤,佔用約3%的開銷
當運行JAVA程序, 獲取到一系列報錯信息
$ java … myapp
Error fetching data, cleaning up.
Error fetching data, cleaning up.
Error fetching data, cleaning up.
Error fetching data, cleaning up.
[..]
下述示例獲取了 SyS_write
的系統調用
# -U 指示用戶空間中的調用堆棧; -p 僅保留Java進程的過濾器
$ ./trace.py 'SyS_write (arg1==1) "%s", arg2' -U -p `pidof java`
# 進程和線程標示
PID TID COMM FUNC -
27982 27983 java SyS_write Error fetching data, cleaning up.0
# 調用堆棧
write+0x2d [libpthread-2.24.so]
writeBytes+0x1f0 [libjava.so]
Java_java_io_FileOutputStream_writeBytes+0x1a [libjava.so]
[unknown] [perf-15527.map]
[unknown] [perf-15527.map]
[unknown] [perf-15527.map]
[unknown] [perf-15527.map]
...
JavaCalls::call_helper(JavaValue*, methodHandle*, …)+0xf53 [libjvm.so]
jni_invoke_static(JNIEnv_*, JavaValue*, _jobject*, …)+0x357 [libjvm.so]
jni_CallStaticVoidMethod+0x186 [libjvm.so]
JavaMain+0x6d1 [libjli.so]
start_thread+0xca [libpthread-2.24.
存在與 perf
一樣的問題,需要一個代理來打印JIT編譯代碼的內存中的地址與實際方法名稱之間的對應關係,可以使用perf-map-agent
. (在perf那一節介紹過)
# create-java-perf-map.sh 在/root/perf-map-agent/bin/目錄下
$ create-java-perf-map.sh `pidof java` "unfoldall,dottedclass"
$ tail /tmp/perf-`pidof java`.map
7f6ed52ea5c0 100 DataFetcher::processIt
7f6ed52eaa20 880 java.lang.ClassLoader::loadClass
7f6ed52ebf40 340 DataFetcher::fetchData
[...]
然後重新執行,使用代理的新結果,其解析了函數的名稱,所有這些名稱均從我們剛剛生成的.map文件中提取
$ trace 'SyS_write (arg1==1) "%s", arg2' -U -p `pidof java`
PID TID COMM FUNC -
25335 25336 java SyS_write Error fetching data, cleaning up.f8
write+0x24 [libpthread-2.24.so]
writeBytes+0x1f0 [libjava.so]
Java_java_io_FileOutputStream_writeBytes+0x1a [libjava.so]
java.io.FileOutputStream::writeBytes+0xc6 [perf-15527.map]
java.io.FileOutputStream::write+0x74 [perf-15527.map]
java.io.BufferedOutputStream::flushBuffer+0xa5 [perf-15527.map]
java.io.BufferedOutputStream::flush+0x98 [perf-15527.map]
[...]
JavaCalls::call_helper(JavaValue*, methodHandle*, …)+0xf53 [libjvm.so]
...
示例3: Java應用程序通過直接調用
System.gc()
導致大量垃圾收集
前提條件:
-XX:+PreserveFramePointer
3%左右的開銷-XX:+ExtendedDTraceProbes
昂貴的開銷,僅用於調試方法調用/對象分配
通過查看GC LOG可以看出存在多次 System.gc()
$ java … -XX:+PrintGC myapp
[Full GC (System.gc()) 530K->255K(15872K), 0.0021490 secs]
[Full GC (System.gc()) 255K->255K(15936K), 0.0020310 secs]
[Full GC (System.gc()) 255K->255K(15936K), 0.0017840 secs]
[...]
使用trace
查看堆棧調用, 通過 method_entry
可以跟蹤任何Java方法,爲了表明我們對哪種特定方法感興趣,我們替換一個條件,該條件要求arg4
(此方法名稱) 等於 gc
$ trace 'u:.../libjvm.so:method__entry (STRCMP("gc", arg4)) "induced GC"' -U -p `pidof java`
PID TID COMM FUNC -
25413 25414 java method__entry induced GC
# 具有System.gc()此應用程序中每個調用的調用和堆棧跟蹤的輸出
SharedRuntime::dtrace_method_entry(…)+0x7b [libjvm.so]
java.lang.Runtime::gc+0x80 [perf-15605.map]
java.lang.System::gc+0x40 [perf-15605.map]
DataFetcher::fetchData+0xdc [perf-15605.map]
RequestProcessor::processRequest+0xc0 [perf-15605.map]
Collecty::main+0x14b [perf-15605.map]
call_stub+0x88 [perf-15605.map]
JavaCalls::call_helper(JavaValue*, methodHandle*, …)+0xf53 [libjvm.so]
...
五、GC分析
示例: 瞭解Java應用程序生成的負載,確定它與GC有關,並弄清垃圾來自何處
# ustat
$ ustat -C
15:02:09 loadavg: 0.24 0.05 0.03 3/202 28698
PID CMDLINE METHOD/s GC/s OBJNEW/s CLOAD/s EXC/s THR/s
28689 java -XX:+PreserveFr 0 888 0 0 0 0
# uobjnew: 使用昂貴的探針來跟蹤對象分配,該探針需要-XX:+ExtendedDTraceProbes
TYPE ALLOCS BYTES
java/lang/String 12588 0
[C 12588 0
[...]
# stackcount: 總結導致該探針的Java調用堆棧 (也可以生成火焰圖)
$ stackcount -i 5 -p `pidof java` "u:.../libjvm.so:object__alloc"
SharedRuntime::dtrace_object_alloc(oopDesc*, int)
TypeArrayKlass::allocate_common(int, bool, Thread*)
OptoRuntime::new_array_C(Klass*, int, JavaThread*)
_new_array_Java
ResponseBuilder::addLine
Allocy::main
call_stub
…
JavaMain
start_thread
870