使用BPF監視JVM應用程序

使用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_begingc_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_loadedthread_startobject_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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章