你是否曾在排查一个 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 网络应用的执行:

  1. 我们选择一个使用 requests 库发送 HTTP 请求的 Python 应用作为样本,用 Live Record 工具录制它的运行过程,捕获完整的执行细节。

  2. 然后用 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 调用栈揭示了一些重要信息:

  1. 从栈底部(#32-#30)可以看出,这是在一个 Python 线程中执行的代码
  2. 在 #2-#0 可以看到系统正在执行 getaddrinfoconnect 函数,表明程序正在解析主机名并尝试建立连接
  3. 从 #3 可以看出这是从 Python 的 socket 模块发起的调用

不过,这个 C 调用栈主要展示了底层实现细节,对理解 Python 应用层面的逻辑帮助有限。我们无法直接看出是哪个 Python 函数或模块发起了这个网络请求。接下来需要借助 OpenResty XRay 提供的工具来分析 Python 代码路径。

步骤四:分析 Python 调用栈

C 调用栈虽然详细,但对 Python 开发者来说不够直观。为了获取更有意义的 Python 层面调用信息,我们需要使用 OpenResty XRay 提供的专用工具:

  1. 首先加载 OpenResty XRay 提供的 Python 调用栈分析工具:
5% 181,380> source python-udb.y.py

这是 OpenResty XRay 为 UDB 环境专门开发的 Python 扩展,能解析 Python 内部结构并提取完整的 Python 调用栈。

  1. 接下来,跳转到 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 代码的执行上下文。

  1. 现在使用 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 调用栈直观得多,让我们能清楚地看到从业务代码到底层网络操作的完整调用路径。

  1. 接下来,继续执行程序直到触发 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 开发者提供了前所未有的洞察力,让我们能够:

  1. 全程追踪执行路径:从高层 Python 代码到底层系统调用,无遗漏地捕捉每一步
  2. 精准定位关键节点:在网络请求的各个阶段(连接建立、数据发送、响应接收)设置断点并分析
  3. 跨语言无缝调试:同时查看 Python 代码和底层 C 扩展的执行情况
  4. 自由回溯分析:在执行历史中自由穿梭,不受传统调试器的限制

如果你也在为网络问题、性能瓶颈、复杂调用栈苦恼,不妨试试“时间旅行调试”的新方式。让 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. 公司的 博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了 英文版 原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!