在呼叫棧層面除錯 Java 檔案 I/O,就是把每一次 native 的 write() 系統呼叫回追到觸發它的 Java 業務方法 —— 這是 IDE 斷點觸及不到、coredump 也無法儲存的資訊。本文用時間旅行回放演示如何做到:錄製執行中的 Java 程序,在 write() 上打斷點,從 JNI 層向上重建完整的 Java 呼叫棧,直至你的 Files.write 呼叫方。

IDE 斷點和 coredump 在 Java 檔案 I/O 排查中的短板

大多數 Java 開發者最先拿起的兩類工具,都無法回答"是哪個業務方法觸發了這次檔案寫入":

  • IDE 斷點止步於 JVM 邊界。 它們可以在 Java 方法上暫停,但無法在 libc 中的底層 write() 系統呼叫處打斷點 —— 而這裡才是檔案寫入真正離開程式的地方。
  • Native 呼叫棧止步於 JNI 層。 掛上 C 層偵錯程式執行 bt,能追到 Java_sun_nio_ch_FileDispatcherImpl_write0 就斷了 —— 無法看到是哪個 Java 方法觸發了它。
  • coredump 只是單一時刻的快照。 coredump 抓取的是某一瞬間的狀態,無法還原走到那一刻的執行路徑,也就沒法回退檢視之前的每次 write 或當時的 Java 呼叫棧。

時間旅行回放同時填上這兩個缺口:它記錄整個執行過程,讓你在時間線上前後自由導航;同時允許在 native 系統呼叫上打斷點,並在任何一次斷點處重建完整的 Java 呼叫棧。

從 native write() 系統呼叫重建完整的 Java 呼叫棧

這套重建能力由兩個部分協同完成:

  1. UDB 記錄 JVM 程序的整個執行過程,因此你可以在任何系統呼叫處暫停 —— 包括 libc 中的 write() —— 並沿時間線向前或向後單步。

  2. OpenResty XRay 提供了一個 Y 語言指令碼 java-udb.y.py,在 UDB 中載入後會新增一個 java_bt 命令。java_bt 會在任意斷點上列印完整的 Java 呼叫棧 —— 從 C:__libc_write 一路向上穿過 JNI 層直到業務方法 —— 把原生 bt 只能看到的 C 層回溯變成一條端到端的完整路徑。

後文將在真實 Java 程序上演示錄製、打斷點、重建這一整套流程。

用時間旅行回放一步步除錯 Java 檔案 I/O

下面 5 步將錄製執行中的 Java 程序、在 native 的 write() 系統呼叫上打斷點,並重建每一次 write 背後的 Java 呼叫棧:

步驟一:錄製應用執行軌跡

首先使用 UDB 的 Live Record 工具錄製 Java 應用的執行過程:

  1. 使用 Live Record 工具錄製一個正在執行的 Java 應用樣本。

  2. 使用 UDB 工具載入錄製樣本,並設定除錯環境:

udb -ex "set pagination off" -ex "set python print-stack full" java.rec

步驟二:定位檔案操作斷點

在 UDB 環境中,設定斷點捕獲檔案寫入操作:

start 1> break write
start 1> c
Continuing.
[Switching to Thread 3923049.3923068]

Thread 20 "Thread-0" hit Breakpoint 1.768, 0x00007ffff7cfdb40 in write () from /tmp/undodb.3976686.1747987405.935444.61ae9aec99c4629b/debuggee-1-bus1z2h5/symbol-files/lib64/libc.so.6

步驟三:分析底層 C 呼叫棧

使用 bt 命令檢視當前的 C 層呼叫棧:

4% 7,955> bt
#0  0x00007ffff7cfdb40 in write () from /tmp/undodb.3976686.1747987405.935444.61ae9aec99c4629b/debuggee-1-bus1z2h5/symbol-files/lib64/libc.so.6
#1  0x00007ffff7e32c5f in Java_sun_nio_ch_FileDispatcherImpl_write0 (env=0x7ffff0181290, clazz=<optimized out>, fdo=<optimized out>, address=140733730265536, len=81)
    at src/java.base/unix/native/libnio/ch/FileDispatcherImpl.c:118
#2  0x00007fffe0baacf6 in ?? ()
#3  0x000000062a39ed70 in ?? ()

從 C 呼叫棧可以看到系統底層的 write 系統呼叫被觸發,但這僅顯示了 JNI 層面的資訊,無法直接看到 Java 業務程式碼的完整呼叫路徑。

步驟四:分析 Java 完整呼叫棧

  1. 載入 OpenResty XRay 提供的 Java 呼叫棧分析工具:
4% 7,955> source java-udb.y.py
  1. 使用 java_bt 命令獲取完整的 Java 呼叫棧:
4% 7,955> java_bt
Start tracing...
C:__libc_write
sun.nio.ch.FileDispatcherImpl:write0(Native Method)
@FileDispatcherImpl.java:62
sun.nio.ch.FileDispatcherImpl:write
@IOUtil.java:114
sun.nio.ch.IOUtil:writeFromNativeBuffer
@IOUtil.java:75
sun.nio.ch.IOUtil:write
@IOUtil.java:67
sun.nio.ch.IOUtil:write
@FileChannelImpl.java:288
sun.nio.ch.FileChannelImpl:write
@Channels.java:89
java.nio.channels.Channels:writeFully
@Channels.java:158
java.nio.channels.Channels$1:write
@StreamEncoder.java:223
sun.nio.cs.StreamEncoder:writeBytes
@StreamEncoder.java:325
sun.nio.cs.StreamEncoder:implClose
@StreamEncoder.java:165
sun.nio.cs.StreamEncoder:close
@OutputStreamWriter.java:252
java.io.OutputStreamWriter:close
@BufferedWriter.java:263
java.io.BufferedWriter:close
@Files.java:3579
java.nio.file.Files:write
@Processor.java:42
FileWriterTask:run
@Thread.java:840
java.lang.Thread:run

透過這個完整的 Java 呼叫棧,我們可以清晰地看到從業務程式碼 FileWriterTask:run 方法開始,經過 Java 標準庫的 Files.write 方法,一直到底層系統呼叫的完整路徑。這對於理解應用的檔案操作行為、定位效能瓶頸或排查檔案操作相關問題具有關鍵價值。一旦識別出觸發呼叫的業務方法,OpenResty XRay 還可以實時抓取傳給這個方法的引數值,無需 Java agent、也無需位元組碼插樁。

跨時間對比檔案寫入呼叫棧

UDB 的時間旅行除錯能力是其最強大的特性之一。透過這一功能,我們可以在錄製的執行軌跡中自由地前進或回溯,精確定位到不同的檔案操作時間點,並全面分析每次寫入操作的完整上下文和呼叫棧。這種能力在排查複雜的檔案操作問題時尤為關鍵。

為了驗證這一點,我們可以繼續執行程式,捕獲下一個檔案寫入操作:

8% 16,337> c
Continuing.

Thread 20 "Thread-0" hit Breakpoint 1.768, 0x00007ffff7cfdb40 in write () from /tmp/undodb.3976686.1747987405.935444.61ae9aec99c4629b/debuggee-1-bus1z2h5/symbol-files/lib64/libc.so.6

8% 16,337> java_bt
Start tracing...
C:__libc_write
sun.nio.ch.FileDispatcherImpl:write0(Native Method)
@FileDispatcherImpl.java:62
sun.nio.ch.FileDispatcherImpl:write
@IOUtil.java:114
sun.nio.ch.IOUtil:writeFromNativeBuffer
@IOUtil.java:75
sun.nio.ch.IOUtil:write
@IOUtil.java:67
sun.nio.ch.IOUtil:write
@FileChannelImpl.java:288
sun.nio.ch.FileChannelImpl:write
@Channels.java:89
java.nio.channels.Channels:writeFully
@Channels.java:158
java.nio.channels.Channels$1:write
@StreamEncoder.java:223
sun.nio.cs.StreamEncoder:writeBytes
@StreamEncoder.java:325
sun.nio.cs.StreamEncoder:implClose
@StreamEncoder.java:165
sun.nio.cs.StreamEncoder:close
@OutputStreamWriter.java:252
java.io.OutputStreamWriter:close
@BufferedWriter.java:263
java.io.BufferedWriter:close
@Files.java:3579
java.nio.file.Files:write
@Processor.java:203
CoreWriterTask:run
@Thread.java:840
java.lang.Thread:run

透過對比兩次捕獲的呼叫棧,我們可以清晰地看到:

  1. 第一次寫入操作來自 FileWriterTask:run 方法(位於 Processor.java:42
  2. 第二次寫入操作則來自 CoreWriterTask:run 方法(位於 Processor.java:203

這種分析方法不僅適用於檔案寫入操作,同樣可以應用於檔案讀取、網路通訊等各類 I/O 場景。如果你想從更宏觀的角度看,OpenResty XRay 還能分析 Java 應用的 CPU、off-CPU 和磁碟 I/O 使用,且無需 JVM 安全點。

OpenResty XRay 與 UDB 結合的動態分析能力讓我們能夠全面洞察應用中的檔案操作行為模式,突破了傳統除錯方法僅能捕獲單一時刻狀態的侷限。特別是在程序已經終止或不存在的情況下,相較於傳統 GDB 的 coredump 分析,UDB 的回溯分析能力展現出其獨特的優勢。

coredump 僅能提供程式崩潰瞬間的靜態快照,無法重現崩潰前一段時間的完整執行路徑;而 UDB 透過其錄製功能,即使在原始程序不再執行的情況下,依然賦予 OpenResty XRay 在完整程式執行歷史中自如穿梭的能力,可以精確檢視任意時間點的程式狀態與行為細節,真正實現了時間維度上的全方位除錯。

常見問題

如何看到一次 Java 檔案寫入的完整呼叫棧?

在錄製的 JVM 執行中對 native 的 write() 系統呼叫打斷點(UDB 裡執行 break write),然後執行 java_bt —— 透過 source java-udb.y.py 載入 OpenResty XRay 的 Y 語言指令碼後新增的命令。它會列印完整的 Java 棧:從最底部的 C:__libc_write 到最頂端觸發寫入的業務方法。

時間旅行除錯和 coredump 分析有甚麼區別?

coredump 是崩潰瞬間的單時刻快照 —— 你可以檢視狀態,但無法回放程式是怎麼走到那裡的。時間旅行除錯記錄整個執行過程,你可以前後單步、在任意系統呼叫上暫停,並在過去執行中的任何時刻重建呼叫棧。

為甚麼 C 層 bt 只看到 JNI,看不到 Java 業務方法?

C 層偵錯程式的 bt 命令是在遍歷 C 棧幀。當 Java 程式碼觸發一次檔案寫入時,C 層回溯只能追到 JNI 入口 Java_sun_nio_ch_FileDispatcherImpl_write0 就斷了 —— 因為 JVM 的直譯器幀和 JIT 幀並不是標準的 C 棧幀。你需要一個懂 JVM 的工具來展開它們;OpenResty XRay 的 java_bt 就是做這件事的。

總結

UDB 結合 OpenResty XRay 提供的 Java 呼叫棧分析能力,為開發者提供了前所未有的應用行為洞察力。透過時間旅行除錯和完整呼叫棧分析,開發者可以:

  1. 精確定位檔案操作的來源和上下文
  2. 深入分析 I/O 效能瓶頸
  3. 高效排查檔案操作相關的問題
  4. 全面驗證檔案操作的安全性和正確性

這種深度分析能力對於開發 Java 應用尤為重要,特別是在現在這個微服務成常態的時代,這種深度分析能力簡直就是救命稻草。

希望本文的實戰演示能幫助您更好地理解和體會 UDB 和 OpenResty XRay 的應用場景和強大之處。如果你也在為複雜的除錯問題頭疼,不妨試試這套組合拳。

關於 OpenResty XRay

OpenResty XRay 是一個動態追蹤產品,它可以自動分析執行中的應用,以解決效能問題、行為問題和安全漏洞,並提供可行的建議。在底層實現上,OpenResty XRay 由我們的 Y 語言驅動,可以在不同環境下支援多種不同的執行時,如 Stap+、eBPF+、GDB 和 ODB。

關於作者

章亦春是開源 OpenResty® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。

章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴,是 “邊緣計算 “、” 動態追蹤 “和 “機器程式設計 “的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用 動態追蹤 技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,並編寫過 60 多個開源軟體庫。

關注我們

如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的 部落格網站 。也歡迎掃碼關注我們的微信公眾號:

我們的微信公眾號

翻譯

我們提供了 英文版 原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!