深度探索JFR - JFR詳細介紹與生產問題定位落地 - 1. JFR說明與啓動配置

本文基於 OpenJDK 11 並涉及一些之後版本的特性,非 OpenJDK 11 的特性會被特殊標記出來

什麼是 JFR?

我們都知道,黑匣子是用於記錄飛機飛行和性能參數的儀器。在飛機出問題後,用於定位問題原因。JFR 就是 Java 的黑匣子。

JFR 是 Java Flight Record (Java飛行記錄) 的縮寫,是 JVM 內置的基於事件的JDK監控記錄框架。這個起名就是參考了黑匣子對於飛機的作用,將Java進程比喻成飛機飛行。顧名思義,這個記錄主要用於問題定位和持續監控。

如果是利用默認配置啓動這個記錄,性能非常高效,對於業務影響很小,因爲這個框架本來就是用來長期在線上部署的框架。這個記錄可以輸出成二進制文件,用戶可以指定最大記錄時間,或者最大記錄大小,供用戶在需要的時候輸出成文件進行事後分析。

JFR 的前身也是 JFR,只不過這個 J 不是 Java 而是 JRockit。在 JRockit 虛擬機時代,就有這樣一個工具用來記錄 Java 虛擬機運行時各項數據。在 Oracle 收購 Sun 公司之後,Hotspot 虛擬機時代,也一直延續了這個工具:

  • JFR 0.9 版本對應 JDK 7 和JDK 8:JDK 7u40 之後,實現了和 JRockit Flight Recorder 一樣的功能,並添加了各項數據配置,用來打開或者關閉一些統計數據功能。而且,在 JDK 8u40 之後,可以在運行時靈活地打開關閉 JFR。
  • JFR 1.0 版本對應 JDK 9 和 JDK 10: 在這一版本之後,增加了 JFR 事件接口,用戶可以生產或者消費某種事件。
  • JFR 2.0 版本對應 JDK 11,這一版本就是我們今天要詳細討論說明。

這裏我們先來列出一些些關於JFR更新與bug信息的鏈接:

  • JEP 328: Flight Recorder(Release in JDK 11): https://openjdk.java.net/jeps/328
  • hotspot虛擬機JFR相關bug:https://bugs.openjdk.java.net/browse/JDK-8240430?jql=project%20in%20(JDK)%20AND%20component%20in%20(hotspot)%20AND%20Subcomponent%20in%20(jfr)
  • JEP 349: JFR Event Streaming(Release in JDK 14):https://openjdk.java.net/jeps/349

爲什麼用 JFR?

因爲某些異常很難在開發測試階段發現,需要在生產環境纔會出這些問題。爲了能在生產問題發生後,更好的定位生產問題,JDK 提供了這樣一個可以長期開啓,對應用影響很小的持續監控手段。官方說,目標是開啓 JFR 監控(默認配置),對性能的影響在1%之內,對JVM Runtime 和 GC,OS 以及 Java 庫進行全方位的監控。

這裏放出一個本人開啓默認配置的 JFR 監控後,性能對比,JFR是在19:40開啓的:

image

可以看出,在19:40開啓default,之後請求數量回歸峯值之前,Load基本和之前一樣,可以視爲無影響。

再放出一個本人在同一個微服務另一個實例同一時間開啓 profile 配置的 JFR 監控後,性能對比,同樣是在19:40開啓:
image

profile的情況下,峯值load相較於default的峯值load高了很多。profile配置官方說大概影響2%的性能,但是實際上,這個影響,尤其是頻繁發生內存分配的微服務接口應用,影響絕對不止2%,而且profile的確採集的東西要比默認配置的多很多(這個我們後面會詳細說,爲什麼負載會高的原因也會在後面說),所以,線上系統不推薦長期跑profile。

JFR,具有以下關鍵的特性:

  • 低開銷(在配置正確的情況下),可在生產環境核心業務進程中始終在線運行。當然,也可以隨時開啓與關閉。
  • 可以查看出問題時間段內進行分析,可以分析 Java 應用程序,JVM 內部以及當前Java進程運行環境等多因素。
  • JFR基於事件採集,可以分析非常底層的信息,例如對象分配,方法採樣與熱點方法定位與調用堆棧,安全點分析與鎖佔用時長與堆棧分析,GC 相關分析以及 JIT 編譯器相關分析(例如 CodeCache )
  • 完善的 API 定義,用戶可以自定義事件生產與消費。

JFR 的核心 - 事件 Event 說明

在 JFR中,一切皆爲 Event:

  • 任意JVM行爲都是一個Event,例如類加載也是一個 Event,對應 Class Load Event
  • 開啓 JFR 記錄的原因也是一個Event,對應的就是 Recording Reason Event
  • 就算是有 Event 丟失,他也是一個 Event,對應 Data Loss Event

這些 Event 在某些特定的時間點產生,每個事件都有名稱,產生時間戳還有 Event 數據體組成。Event 數據體不同的 Event 數據不同,例如 CP U負載,Event 發生之前還有之後的 Java 堆大小, 獲取鎖的線程 ID 等等。還有一點比較有意思的是,大部分的 Event,都有 Event 是在哪個線程發生的,Event 發生的時候這個線程的調用棧,Event 的持續時間。這就非常有用了,利用這些信息,我們可以回溯 Event 發生當時的情況。

Event 按照採集方式可以分爲三種:

  1. Instant Event:顧名思義,這種 Event 在發生時就立刻採集。例如:Throw Exception Event 還有 Thread Start Event,類似於這種在某一時刻發生的 Event
  2. Duration Event:這種 Event 需要耗費一些時間,在完成的時候會記錄。對於這種類型的 Event,可以設置一個時間限制,超過這個時間限制的纔會記錄。例如 GC Event,Thread Sleep Event。
  3. Sample Event(或者是Requestable Event):按照一定的頻率採集,這個頻率是可以配置的。例如 Thread Dump Event,Method Sampling Event

由於 JFR 會採集很多很多的數據,爲了效率,最好配置自己感興趣的事件採集,並且對於 Duration Event 設置時間限制,一般我們對於時間短的事件並不關心。

Event 會被寫入 .jfr 的二進制文件(二進制文件對於應用來說讀寫效率最高)中,以 little endian base 128 的形式編碼,這裏我們用一個 Event 舉個例子:

Class Load Event

0000FC10 : 98 80 80 00 87 02 95 ae e4 b2 92 03 a2 f7 ae 9a 94 02 02 01 8d 11 00 00
  • 0000FC10: 文件位置
  • 98 80 80 00: Event 大小
  • 87 02: Event ID
  • 95 ae e4 b2 92 03: 時間戳
  • a2 f7 ae 9a 94 02: 持續時間
  • 02: 線程 ID
  • 01: 堆棧 ID
  • PayLoad(每種 Event 的 field 不同):
    • 8d 11: 加載的類
    • 00 : 定義類的 ClassLoader
    • 00 : 初始化類的 ClassLoader

這裏僅僅是舉個例子,實際使用中,我們肯定不會去這麼看每個 Event 的,而是通過可視化工具 JMC 去看,這個我們後面會講到。至於 Event 有哪些種類,也會在後面的章節涉及到。

那麼這些Event是如何產生,如何記錄保持高效的呢?

JFR如何實現的低延遲與低性能損耗

首先,Event肯定是多線程產生的,這點顯而易見。如果 Event 記錄要保證全局有序,那麼肯定需要多線程向一個指定隊列或者緩存輸出,那麼不可避免的會涉及到鎖爭用,這樣是很低效的。 Event本身帶時間戳,那麼可不可以在最後讀取的時候進行排序?在一個線程內,生成的 Event 肯定是有序的;那麼多線程產生的 Event, 就可以看成一個又一個的有序集合。最後,針對這些有序集合的每個元素進行整體排序,算法上快很多。所以我們沒有必要在 Event 產生的時候就進行整體排序。

在 JFR 中,所有的 Event (包括通過JFR API產生的 Event 還有 JVM 產生的 EVENT),會先存儲到每個線程自己的 Thread Buffer 中;在這個 Buffer 滿了之後,會將 Buffer 的內容刷入 Global Buffer 中;Global Buffer 是一個環形 Buffer,保存着所有線程發送來的 Thread Buffer 中的內容。當這個環形 Buffer 存儲到達上限之後,根據配置,會選擇丟棄或者刷入文件

可以看出,不同的 Buffer 之間的數據不會有任何重疊。並且某一塊數據,要麼就是在內存中,要麼就是在磁盤上,不會兩個地方都存在,那麼這樣會帶來數據丟失的問題:

  • 首先,在斷電的時候或者操作系統強制重啓的時候,還未寫入磁盤的 Event 會丟失。
  • 如果只是強制 kill -9掉了Java 進程,那麼刷入文件寫入高速緩衝的 Event 不會丟失,但是 Global Buffer 中還有 Thread Buffer 中的數據會丟失。同樣的,如果JVM崩潰了,這些內存Buffer中的數據也會丟失。正常退出,或者應用異常但是JVM正常退出的,數據不會丟失。
  • 採集的數據在可見之前可能會有很小的延遲。例如數據在從 Thread Buffer 刷入 Global Bufeer 的時候, 你如果去 dump JFR 的數據,可能這部分數據會被忽略而導致看不到。
  • 最後一點,任何情況下導致在從 Global Buffer 刷入磁盤不夠快的時候,這時候要刷入磁盤的數據可能被丟棄。當發生這種情況是,就會記錄下數據丟失事件,這個事件包括是那塊時間的數據丟掉了。通過 JFR 的日誌也能看到這個信息。

image

開啓JFR記錄

可以通過啓動參數配置並且啓用 JFR,也可以通過啓動參數在 JVM 進程啓動的時候就啓動 JFR,或者是利用 jcmd 工具,動態啓用或者關閉 JFR。

通過 JVM 啓動參數啓用以及 JVM 參數說明

在 OpenJDK 11 版本之後,啓動參數被簡化了很多很多;目前JFR涉及的參數僅僅只有兩個,一個負責啓動(-XX:StartFlightRecording),一個負責配置(-XX:FlightRecorderOptions)。JDK 8中的-XX:+FlightRecorder打開 FlightRecorder 狀態位在 OpenJDK 11 中不再需要了,目前僅需一個參數就能啓動 JFR。 這裏我們舉一個例子:

java -XX:StartFlightRecording=disk=true,dumponexit=true,filename=recording.jfr,maxsize=1024m,maxage=1d,settings=profile,path-to-gc-roots=true test.Main

核心就是 -XX:StartFlightRecording,有了這個參數就會啓用 JFR 記錄。其中的涉及配置有:

配置key 默認值 說明
delay 0 延遲多久後啓動 JFR 記錄,支持帶單位配置, 例如 delay=60s(秒), delay=20m(分鐘), delay=1h(小時), delay=1d(天),不帶單位就是秒, 0就是沒有延遲直接開始記錄。一般爲了避免框架初始化等影響,我們會延遲 1 分鐘開始記錄(例如Spring cloud應用,可以看下日誌中應用啓動耗時,來決定下這個時間)。
disk true 是否寫入磁盤,這個就是上文提到的, global buffer 滿了之後,是直接丟棄還是寫入磁盤文件。
dumponexit false 程序退出時,是否要dump出 .jfr文件
duration 0 JFR 記錄持續時間,同樣支持單位配置,不帶單位就是秒,0代表不限制持續時間,一直記錄。
filename 啓動目錄/hotspot-pid-26732-id-1-2020_03_12_10_07_22.jfr,pid 後面就是 pid, id 後面是第幾個 JFR 記錄,可以啓動多個 JFR 記錄。最後就是時間。 dump的輸出文件
name 記錄名稱,由於可以啓動多個 JFR 記錄,這個名稱用於區分,否則只能看到一個記錄 id,不好區分。
maxage 0 這個參數只有在 disk 爲 true 的情況下才有效。最大文件記錄保存時間,就是 global buffer 滿了需要刷入本地臨時目錄下保存,這些文件最多保留多久的。也可以通過單位配置,沒有單位就是秒,默認是0,就是不限制
maxsize 250MB 這個參數只有在 disk 爲 true 的情況下才有效。最大文件大小,支持單位配置, 不帶單位是字節,m或者M代表MB,g或者G代表GB。設置爲0代表不限制大小**。雖然官網說默認就是0,但是實際用的時候,不設置會有提示**: No limit specified, using maxsize=250MB as default. 注意,這個配置不能小於後面將會提到的 maxchunksize 這個參數。
path-to-gc-roots false 是否記錄GC根節點到活動對象的路徑,一般不打開這個,首先這個在我個人定位問題的時候,很難用到,只要你的編程習慣好。還有就是打開這個,性能損耗比較大,會導致FullGC一般是在懷疑有內存泄漏的時候熱啓動這種採集,並且通過產生對象堆棧無法定位的時候,動態打開即可。一般通過產生這個對象的堆棧就能定位,如果定位不到,懷疑有其他引用,例如 ThreadLocal 沒有釋放這樣的,可以在 dump 的時候採集 gc roots
settings 默認是 default.jfc,這個位於 $JAVA_HOME/lib/jfr/default.jfc 採集 Event 的詳細配置,採集的每個 Event 都有自己的詳細配置。另一個 JDK 自帶的配置是 profile.jfc,位於 $JAVA_HOME/lib/jfr/profile.jfc。這個配置文件裏面的配置是怎麼回事,我們後面會涉及。

至於前面章節中提到的那些 Buffer 的大小,是在另一個配置參數中配置,一般我們不改這些配置,用默認的就能滿足我們的需求了,這裏列出下:

-XX:FlightRecorderOptions 相關的參數

配置key 默認值 說明
allow_threadbuffers_to_disk false 是否允許 在 thread buffer 線程阻塞的時候,直接將 thread buffer 的內容寫入文件。默認不啓用,一般沒必要開啓這個參數,只要你設置的參數讓 global buffer 大小合理不至於刷盤很慢,就行了。
globalbuffersize 如果不設置,根據設置的 memorysize 自動計算得出 單個 global buffer 的大小,一般通過 memorysize 設置,不建議自己設置
maxchunksize 12M 存入磁盤的每個臨時文件的大小。默認爲12MB,不能小於1M。可以用單位配置,不帶單位是字節,m或者M代表MB,g或者G代表GB。注意這個大小最好不要比 memorySize 小,更不能比 globalbuffersize 小,否則會導致性能下降
memorysize 10M JFR的 global buffer 佔用的整體內存大小,一般通過設置這個參數,numglobalbuffers 還有 globalbuffersize 會被自動計算出。可以用單位配置,不帶單位是字節,m或者M代表MB,g或者G代表GB。
numglobalbuffers 如果不設置,根據設置的 memorysize 自動計算得出 global buffer的個數,一般通過 memorysize 設置,不建議自己設置
old-object-queue-size 256 對於Profiling中的 Old Object Sample 事件,記錄多少個 Old Object,這個配置並不是越大越好。記錄是怎麼記錄的,會在後面的各種 Event 介紹裏面詳細介紹。我的建議是,一般應用256就夠,時間跨度大的,例如 maxage 保存了一週以上的,可以翻倍
repository 等同於 -Djava.io.tmpdir 指定的目錄 JFR 保存到磁盤的臨時記錄的位置
retransform true 是否通過 JVMTI 轉換 JFR 相關 Event 類,如果設置爲 false,則只在 Event 類加載的時候添加相應的 Java Instrumentation,這個一般不用改,這點內存 metaspace 還是足夠的
samplethreads true 這個是是否開啓線程採集的狀態位配置,只有這個配置爲 true,並且在 Event 配置中開啓線程相關的採集(這個後面會提到),纔會採集這些事件。
stackdepth 64 採集事件堆棧深度,有些 Event 會採集堆棧,這個堆棧採集的深度,統一由這個配置指定。注意這個值不能設置過大,如果你採集的 Event種類很多,堆棧深度大很影響性能。比如你用的是 default.jfc 配置的採集,堆棧深度64基本上就是不影響性能的極限了。你可以自定義採集某些事件,增加堆棧深度。
threadbuffersize 8KB threadBuffer 大小,最好不要修改這個,如果增大,那麼隨着你的線程數增多,內存佔用會增大。過小的話,刷入 global buffer 的次數就會變多。8KB 就是經驗中最合適的。

配置與 JFR 的架構聯繫:

image

注意這些配置的聯繫與區別

1.disk=true 與 dumponexit=true, 這兩個配置完全不是一回事。disk=true,僅僅代表如果 global buffer 滿了,將這個寫入文件並不是用戶可以看到的,只會寫入 repository 配置的目錄,默認是臨時目錄,這個臨時目錄地址是-Djava.io.tmpdir指定的,默認爲:

  • linux: /tmp 目錄
  • windows: C:\Users\你的用戶\AppData\Temp

配置了 disk=true 之後,就會在臨時目錄產生一個文件夾,命名格式是:時間_pid,例如:2020_03_12_08_04_45_10916;裏面的文件就是一個又一個的 Data trunk,表現爲一個又一個的 .jfr 文件。最新的文件 會跟隨一個 .part :

--/2020_03_12_08_04_45_10916
|----2020_03_12_08_04_45.jfr
|----2020_03_12_08_05_12.jfr
|----2020_03_12_08_05_55.jfr
|----2020_03_12_08_06_08.jfr
|----2020_03_12_08_06_08.part

每個 .jfr 文件的大小, 就是 Data Chunk 的大小,這個大小如何配置,會在後面的 jcmd 啓動並配置 JFR 中提到。
dumponexit=true 代表在程序退出的時候,強制dump一次將數據存入 filename 配置的輸出文件。只有用戶手動 dump, 或者是 dumponexit 觸發的 dump, 用戶才能正常看到 .jfr 文件。輸出這個文件其實很快, 就是將內存中所有 beffer 以及臨時文件夾 中的 .jfr文件的內容,輸出到用戶指定的 .jfr 文件中。一般內存中的 buffer 很小,是MB級別的,這個是可以配置的,注意不要配置很大,否則可能會內存不足,最重要的是可能會使老年代增大導致FullGC。

2.JFR相關的內存佔用到底有多大?主要是兩部分,一部分是 global buffer,另一部分是 thread local buffer。 global buffer 總大小由上面提到的 memorysize 自動計算得出,總大小就是 memorysize。所以, JFR 相關的佔用內存大小爲: thread 數量 * thread buffer 大小 + memory size

通過 jcmd 命令啓用

jcmd 命令相關的參數與 JVM 參數涉及的配置參數,其實是一樣的,我們來看。

  1. jcmd <pid> JFR.start。啓動 JFR 記錄,參數和-XX:StartFlightRecording一模一樣,請參考上面的表格。但是注意這裏不再是逗號分割,而是空格
    示例:
jcmd 21 JFR.start name=profile_online maxage=1d maxsize=1g

這個就代表啓動一個名稱爲 profile_online, 最多保留一天,最大保留 1G 的本地文件記錄

  1. jcmd <pid> JFR.stop. 停止 JFR 記錄,需要傳入名稱,例如如果要停止上面打開的,則執行:
jcmd 21 JFR.stop name=profile_online

參數:

參數 默認值 描述
name 指定要停止的 JFR 記錄名稱
copy_to_file 停止時同時複製到文件,指定文件輸出位置
  1. jcmd <pid> JFR.check,查看當前正在執行的 JFR 記錄。

示例:

jcmd 21 JFR.check

輸出:

21:
Recording 1: name=profile_online maxsize=1.0GB maxage=1d (running)

參數:

參數 默認值 描述
name 指定要查看的 JFR 記錄名稱
verbose false 是否查看每種 Event 採集詳細配置
  1. jcmd <pid> JFR.configure,如果不傳入參數,則是查看當前配置,傳入參數就是修改配置。配置與-XX:FlightRecorderOptions的一模一樣。請參考上面的表格
    示例:
./jcmd 21 JFR.configure

輸出:

Repository path: /tmp/2020_03_18_08_41_44_21

Stack depth: 64
Global buffer count: 20
Global buffer size: 512.0 kB
Thread buffer size: 8.0 kB
Memory size: 10.0 MB
Max chunk size: 12.0 MB
Sample threads: true

示例:

./jcmd 21 JFR.configure stackdepth=65

輸出:

21:
Stack depth: 65
  1. jcmd <pid> JFR.dump

參數:

參數 默認值 描述
name 指定要查看的 JFR 記錄名稱
filename 指定文件輸出位置
maxage 0 dump最多的時間範圍的文件,可以通過單位配置,沒有單位就是秒,默認是0,就是不限制
maxsize 0 dump最大文件大小,支持單位配置, 不帶單位是字節,m或者M代表MB,g或者G代表GB。設置爲0代表不限制大小
begin dump開始位置, 可以這麼配置:09:00, 21:35:00, 2018-06-03T18:12:56.827Z, 2018-06-03T20:13:46.832, -10m, -3h, or -1d
end : dump結束位置,可以這麼配置: 09:00, 21:35:00, 2018-06-03T18:12:56.827Z, 2018-06-03T20:13:46.832, -10m, -3h, or -1d (STRING, no default value)
path-to-gc-roots false 是否記錄GC根節點到活動對象的路徑,一般不記錄,dump 的時候打開這個肯定會觸發一次 fullGC,對線上應用有影響。最好參考之前對於 JFR 啓動記錄參數的這個參數的描述,考慮是否有必要
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章