結合 UDB 的時間旅行功能,揭秘 Python 程式碼執行過程
你是否曾在排查一個 HTTP 超時問題時,面對成堆的日誌卻一頭霧水?是否也嘗試過用 Core Dump 追蹤呼叫棧,卻始終找不到問題發生的真實上下文?
這一次,我們用 UDB,一款時間旅行偵錯程式,配合 OpenResty XRay,完整重現了一個 Python 網路請求從發起到響應的全過程,並找到了問題的根源。
UDB 在 Python 除錯中的獨特價值
UDB(Undo Debugger)是一款時間旅行偵錯程式,它為 Python 開發者帶來了全新的除錯體驗。與 pdb、PyCharm 等傳統除錯工具相比,UDB 最大的優勢是能夠完整記錄程式的執行過程,讓開發者可以在執行歷史中自由穿梭。
對於 Python 這樣的解釋型語言,UDB 的價值尤為明顯:
- 完整執行軌跡捕獲:記錄 Python 直譯器的每一步執行,包括底層 C 擴充套件模組的呼叫
- 無限次重現問題:同一個 bug 可以反覆重現和分析,無需重啟程式
- 跨語言無縫除錯:同時觀察 Python 程式碼和底層 C/C++ 擴充套件的執行流程
- 精確效能分析:藉助時間旅行功能,精確定位效能瓶頸
- 靈活擴充套件能力:基於 GDB 構建,完全相容 GDB 的 Python API,可透過 Python 指令碼擴充套件偵錯程式功能
結合 OpenResty XRay 增強除錯能力
OpenResty XRay 是一款出色的動態追蹤工具,它能自動分析正在執行的應用程式,快速找出效能瓶頸、異常行為和安全漏洞,並給出實用的最佳化建議。OpenResty XRay底層由我們自主研發的Y 語言驅動,可以輕鬆支援多種執行環境,如 Stap+、eBPF+、GDB和 ODB。
UDB 允許載入 GDB 後端的 Python 擴充套件,這讓我們能在 UDB 環境中直接使用 XRay 的高階分析工具。將 UDB 的時間旅行除錯與 OpenResty XRay 的深度效能分析結合起來,開發者可以獲得應用程式更全面的執行資訊,從宏觀效能到微觀呼叫棧都能一目瞭然,大大提高問題診斷和解決的效率。
實戰案例:分析 Python 網路請求的呼叫棧
下面透過一個實際案例,來看看如何用 UDB 深入分析 Python 應用中網路請求的執行過程。
步驟一:錄製應用執行軌跡
首先,我們需要使用 UDB 的 Live Record 工具來錄製 Python 網路應用的執行:
我們選擇一個使用
requests
庫傳送 HTTP 請求的 Python 應用作為樣本,用 Live Record 工具錄製它的執行過程,捕獲完整的執行細節。然後用 UDB 工具載入這個錄製樣本:
udb -ex "set pagination off" -ex "set python print-stack full" python.rec
步驟二:設定網路請求關鍵斷點
在 UDB 環境中,我們需要設定斷點來捕捉網路請求的關鍵階段。對於 HTTP 請求,最核心的系統呼叫是 connect
(建立連線)和 recv
(接收資料):
start 1> break connect
Breakpoint 1 at 0x7ffff750f590: file ../sysdeps/unix/sysv/linux/connect.c, line 24.
start 1> break recv
Breakpoint 2 at 0x7ffff750f700: file ../sysdeps/unix/sysv/linux/recv.c, line 24.
start 1> c
Continuing.
[New Thread 1259245.1261346]
[Switching to Thread 1259245.1261346]
Thread 2 "python3" hit Breakpoint 1, __libc_connect (fd=fd@entry=3, addr=addr@entry=..., len=len@entry=16) at ../sysdeps/unix/sysv/linux/connect.c:24
當程式執行到 connect
系統呼叫時,UDB 會暫停執行並顯示當前位置。這說明 Python 程式正在嘗試建立網路連線,這是 HTTP 請求的第一個環節。透過在這些關鍵點設定斷點,我們能夠精確捕捉網路操作的各個階段。
步驟三:分析底層 C 呼叫棧
當斷點觸發後,我們先用 bt
命令檢視當前的 C 層呼叫棧:
5% 181,380> bt
#0 __libc_connect (fd=fd@entry=3, addr=addr@entry=..., len=len@entry=16) at ../sysdeps/unix/sysv/linux/connect.c:24
#1 0x00007ffff74f5b3f in try_connect (family=<optimized out>, addrlen=16, addr=0x7fffe40042a0, source_addrp=0x7fffe8d47848, afp=<synthetic pointer>, fdp=<synthetic pointer>)
at ../sysdeps/posix/getaddrinfo.c:2272
#2 __GI_getaddrinfo (name=<optimized out>, name@entry=0x7fffe8d9a900 "localhost", service=<optimized out>, service@entry=0x7fffe8d99bb8 "8888", hints=<optimized out>, hints@entry=0x7fffe8d48070,
pai=pai@entry=0x7fffe8d48058) at ../sysdeps/posix/getaddrinfo.c:2493
#3 0x00007ffff768e5b2 in socket_getaddrinfo (self=0x7fffe9cd7920, args=<optimized out>, kwargs=<optimized out>) at ./Modules/socketmodule.c:6763
#4 0x00007ffff79c98e8 in cfunction_call (func=0x7fffe9cd7f60, args=0x7fffe8d80400, kwargs=0x0) at Objects/methodobject.c:537
...省略部分輸出...
#29 0x00007ffff7b667f8 in thread_run (boot_raw=0x7fffe4005aa0) at ./Modules/_threadmodule.c:1114
#30 0x00007ffff7aef967 in pythread_wrapper (arg=<optimized out>) at Python/thread_pthread.h:237
#31 0x00007ffff7489d22 in start_thread (arg=<optimized out>) at pthread_create.c:443
#32 0x00007ffff750ed40 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81
這個 C 呼叫棧揭示了一些重要資訊:
- 從棧底部(#32-#30)可以看出,這是在一個 Python 執行緒中執行的程式碼
- 在 #2-#0 可以看到系統正在執行
getaddrinfo
和connect
函式,表明程式正在解析主機名並嘗試建立連線 - 從 #3 可以看出這是從 Python 的 socket 模組發起的呼叫
不過,這個 C 呼叫棧主要展示了底層實現細節,對理解 Python 應用層面的邏輯幫助有限。我們無法直接看出是哪個 Python 函式或模組發起了這個網路請求。接下來需要藉助 OpenResty XRay 提供的工具來分析 Python 程式碼路徑。
步驟四:分析 Python 呼叫棧
C 呼叫棧雖然詳細,但對 Python 開發者來說不夠直觀。為了獲取更有意義的 Python 層面呼叫資訊,我們需要使用 OpenResty XRay 提供的專用工具:
- 首先載入 OpenResty XRay 提供的 Python 呼叫棧分析工具:
5% 181,380> source python-udb.y.py
這是 OpenResty XRay 為 UDB 環境專門開發的 Python 擴充套件,能解析 Python 內部結構並提取完整的 Python 呼叫棧。
- 接下來,跳轉到 Python 直譯器執行程式碼的核心函式
_PyEval_EvalFrameDefault
:
5% 181,380> b _PyEval_EvalFrameDefault
Breakpoint 3 at 0x7ffff790c100: file ./Include/internal/pycore_pystate.h, line 107.
5% 181,406> c
Continuing.
Thread 2 "python3" hit Breakpoint 3, _PyEval_EvalFrameDefault (tstate=0x93cb00, frame=0x7ffff73de000, throwflag=0) at ./Include/internal/pycore_pystate.h:107
這個函式是 Python 直譯器執行位元組碼的核心,在這裡設定斷點可以捕獲 Python 程式碼的執行上下文。
- 現在使用
python_bt
命令獲取完整的 Python 呼叫棧:
5% 182,479> python_bt
C:_PyEval_EvalFrameDefault
@/usr/local/openresty-python3/lib/python3.12/socket.py:106
_intenum_converter
@/usr/local/openresty-python3/lib/python3.12/socket.py:978
getaddrinfo
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/util/connection.py:60
create_connection
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/connection.py:199
_new_conn
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/connection.py:279
connect
@/usr/local/openresty-python3/lib/python3.12/http/client.py:1035
send
@/usr/local/openresty-python3/lib/python3.12/http/client.py:1091
_send_output
@/usr/local/openresty-python3/lib/python3.12/http/client.py:1331
endheaders
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/connection.py:441
request
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/connectionpool.py:495
_make_request
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/connectionpool.py:789
urlopen
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/adapters.py:667
send
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/sessions.py:703
send
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/sessions.py:589
request
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/api.py:59
request
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/api.py:73
get
@/opt/app/processor.py:10
make_request
@/usr/local/openresty-python3/lib/python3.12/threading.py:1012
run
@/usr/local/openresty-python3/lib/python3.12/threading.py:1075
_bootstrap_inner
@/usr/local/openresty-python3/lib/python3.12/threading.py:1032
_bootstrap
這個 Python 呼叫棧清晰地展示了整個請求的呼叫鏈路:
- 最底層是 Python 執行緒的啟動函式
_bootstrap
- 中間經過了
requests
庫的各層呼叫 - 最終在我們的業務程式碼
/opt/app/processor.py
第 10 行的make_request
函式中呼叫了requests.get
方法
這比 C 呼叫棧直觀得多,讓我們能清楚地看到從業務程式碼到底層網路操作的完整呼叫路徑。
- 接下來,繼續執行程式直到觸發
recv
斷點,分析接收響應資料的呼叫棧:
5% 182,479> delete breakpoint 3
5% 182,479> c
Continuing.
Thread 4 "python3" hit Breakpoint 2, __libc_recv (fd=3, buf=buf@entry=0x7fffe400a210, len=len@entry=8192, flags=flags@entry=0) at ../sysdeps/unix/sysv/linux/recv.c:24
5% 185,730> b _PyEval_EvalFrameDefault
Breakpoint 4 at 0x7ffff790c100: file ./Include/internal/pycore_pystate.h, line 107.
5% 185,730> c
Continuing.
Thread 2 "python3" hit Breakpoint 4, _PyEval_EvalFrameDefault (tstate=0x93cb00, frame=0x7ffff73ddea0, throwflag=0) at ./Include/internal/pycore_pystate.h:107
5% 186,005> python_bt
C:_PyEval_EvalFrameDefault
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/connection.py:504
getresponse
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/connectionpool.py:536
_make_request
@/usr/local/openresty-python3/lib/python3.12/site-packages/urllib3/connectionpool.py:789
urlopen
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/adapters.py:667
send
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/sessions.py:703
send
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/sessions.py:589
request
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/api.py:59
request
@/usr/local/openresty-python3/lib/python3.12/site-packages/requests/api.py:73
get
@/opt/app/processor.py:10
make_request
@/usr/local/openresty-python3/lib/python3.12/threading.py:1012
run
@/usr/local/openresty-python3/lib/python3.12/threading.py:1075
_bootstrap_inner
@/usr/local/openresty-python3/lib/python3.12/threading.py:1032
_bootstrap
這個呼叫棧展示了接收和處理 HTTP 響應的過程。表面上,這條呼叫路徑看起來和傳送請求時很像,但細節卻大有不同——關鍵函式出現在 urllib3/connection.py
的第 504 行,正在執行 getresponse
相關的程式碼。
得益於 OpenResty XRay 的全自動分析功能,結合 UDB 的“時間旅行”能力,我們不僅還原了網路請求的全生命週期,還在連線建立、資料傳送、響應接收等每一個環節都精準下探到了程式碼級別。這類深入的分析,傳統除錯工具和 Core Dump 根本做不到。
為甚麼說這是“時間維度”的除錯革新?相比傳統方法,UDB 讓我們擁有了真正動態、完整的上下文資訊:
- Core Dump 只能看到程式崩潰瞬間的靜態狀態
- UDB 可以回放整個請求過程,即使原始程序早已退出
- 能在請求過程中的任意環節下斷點,觀察變數和函式呼叫
- 甚至能跨越 Python 和底層 C 擴充套件,完整追蹤整個系統行為鏈條
舉個例子:面對一個 HTTP 超時問題,Core Dump 通常只能讓你看到“最終哪裡掛了”;但用 UDB,你可以從請求發起開始,一步步穿越執行路徑,精準找出在哪一秒發生了延遲、哪個呼叫拖慢了響應。
總結
透過這個實戰案例,我們展示了 UDB 結合 OpenResty XRay 如何幫助開發者深入分析 Python 網路請求的執行過程。UDB 的時間旅行除錯功能為 Python 開發者提供了前所未有的洞察力,讓我們能夠:
- 全程追蹤執行路徑:從高層 Python 程式碼到底層系統呼叫,無遺漏地捕捉每一步
- 精準定位關鍵節點:在網路請求的各個階段(連線建立、資料傳送、響應接收)設定斷點並分析
- 跨語言無縫除錯:同時檢視 Python 程式碼和底層 C 擴充套件的執行情況
- 自由回溯分析:在執行歷史中自由穿梭,不受傳統偵錯程式的限制
如果你也在為網路問題、效能瓶頸、複雜呼叫棧苦惱,不妨試試“時間旅行除錯”的新方式。讓 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、LuaJIT、GDB、SystemTap、LLVM、Perl 等,並編寫過 60 多個開源軟體庫。
關注我們
如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的 部落格網站 。也歡迎掃碼關注我們的微信公眾號:
翻譯
我們提供了 英文版 原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!