今年在 Pycon China 上,来自饿了么的郭浩川分享了 利用 systemtap 进行 Python 执行情况分析 的内容。分享利用 systemtap 在线上环境中实时监控 gevent patch 的 green thread 程序的执行状况。
dtrace 和 systemtap 均支持在 Linux 上进行分析,在 macOS 系统上则只有 dtrace 使用。在 Python3.5 和之前版本中,需要使用手工 Patch 的方式进行埋点监控。在 Python 3.6 以上中 dtrace 和 systemtap 埋点支持功能可以通过编译参数 –with-dtrace 开启。
从 dtrace 开始
dtrace 是一个低开销的成本动态跟踪工具,可以通过埋点 probs 方式监控各项程序运行状态。dtrace 最初内置在 Solaris 系统中,因此我们可以借助 Solaris 系统的相关文档了解 dtrace 的基本操作。DTrace 用户指南 是 Oracle 提供的基于 Solaris 系统的 dtrace 操作手册,操作基本与其它系统相同,推荐在最初开始阶段阅读该使用手册。
在 macOS 上,已经很多系统底层功能和 framework 中已经集成了 dtrace 的功能。
比如说我们需要监控 read 这个 syscall 的入口,可以通过下面这个命令实现:
➜ ~ sudo dtrace -n syscall::read:entry
dtrace: description 'syscall::read:entry' matched 1 probe
CPU ID FUNCTION:NAME
0 156 read:entry
0 156 read:entry
0 156 read:entry
...
其中 -n
参数表示打印特定的 probs 内容的调用。现在这样仅仅显示了调用,但是调用的信息还是不详细的。这个时候就需要使用 dtrace 的脚本获取更多的信息。
dtrace 的脚本
dtrace 的名字暗示了自己的脚本,dtrace 使用了 D 语言作为脚本语言(WTF)。这个时候就需要学习一下基础的 D 语言内容。dtrace 中 D 语言的使用,可以在上面提到的 dtrace 用户指南中的对应章节。
我们继续上一节中的例子,检测调用 read syscall 的参数内容。在进行操作之前,我们需要了解一下 read 的参数:
size_t read(int fildes, void *buf, size_t nbyte);
我们想知道调用来自于哪些进程,读取了多少字节。首先,我们需要创建一个叫 syscall.d
文件。
syscall::read:entry
{printf ("%s called read, asking for %d bytes\n", execname, arg2);
}
然后通过命令行执行:
➜ ~ sudo dtrace -s syscall.d
dtrace: script 'syscall.d' matched 1 probe
CPU ID FUNCTION:NAME
0 156 read:entry steam_osx called read, asking for 128 bytes
0 156 read:entry steam_osx called read, asking for 128 bytes
0 156 read:entry steam_osx called read, asking for 128 bytes
0 156 read:entry steam_osx called read, asking for 128 bytes
0 156 read:entry iTerm2 called read, asking for 32 bytes
0 156 read:entry iTerm2 called read, asking for 1024 bytes
...
从 dtrace 到 Python 跟踪
在 Pycon 上的分享上,提供了一种 CPython 虚拟机代码 Patch 方式进行跟踪的方案,这种方案需要系统支持,比如 REHL 系列系统默认支持,假如你是用了 Ubuntu 系统,除了重新编译 CPython 以外没有其他办法。不过除此之外,还有一种更简单的方式,就是使用 Python USDT
。但是值得注意的是 Python USDT
是一种用户态修饰器监控方法,这种方式与 patch CPython 的方式相比差距较大,毕竟 USDT 无法监控一些系统底层的尤其是涉及到比如 gevent 导致的上下文切换时的混乱。同样的,systemtap 也有一个专门的库:python-systemtap。
不过还是从 USDT 开始。USDT 可以通过 pip 直接安装:
pip install usdt
接下来用 ipython 演示一下 usdt 的基础使用:
➜ ~ ipython
Python 2.7.12 (default, Jul 4 2016, 11:33:35)
Type "copyright", "credits" or "license" for more information.
IPython 5.0.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]: import os
In [2]: os.getpid()
Out[2]: 7777
In [3]: from usdt.tracer import fbt
In [4]: @fbt
...: def example(v):
...: pass
...:
In [5]: example("hello")
我们使用监听进程的方式监控来自于 usdt 的探针:
➜ ~ sudo dtrace -l -p 7777 -m fbt
ID PROVIDER MODULE FUNCTION NAME
1206 python-fbt7777 fbt example entry
1207 python-fbt7777 fbt example return
其中 -p
用于指定进程,-m
指定对应的模块。如果不进行模块过滤的话,你很有可能被很多相关的调试信息淹没。
在新版的 Python 3.6 以上版本中通过开启 --with-dtrace
开关的方式可以获取详细的 Python 运行状态信息,包括:
- 函数调用与返回
- GC 开始与结束
- 执行代码行数
首先运行一个 Python 3.6
➜ ~ ipython
Python 3.6.0b1 (default, Sep 15 2016, 10:16:39)
Type "copyright", "credits" or "license" for more information.
IPython 5.1.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]: import os
In [2]: os.getpid()
Out[2]: 33164
In [3]:
接下来通过 dtrace 来获取所有的 Python 探针输出:
➜ ~ sudo dtrace -l -m python3.6
ID PROVIDER MODULE FUNCTION NAME
35775 python33164 python3.6 _PyEval_EvalFrameDefault function-entry
35776 python33164 python3.6 _PyEval_EvalFrameDefault function-return
35777 python33164 python3.6 collect gc-done
35778 python33164 python3.6 collect gc-start
35779 python33164 python3.6 _PyEval_EvalFrameDefault line
如果想要实现一个类似 pycon 中监控程序执行具体内容的 dtrace 脚本,请参考官方文档 Instrumenting CPython with DTrace and SystemTap 中的现成脚本即可。
总结
dtrace 提供了一种方便的、低干扰的 Python 内部执行探查方式。通过这种方式可以方便的了解目前 Python 的具体执行状况。同时,通过脚本,可以快速获取和定位相关想要了解的具体内容。当然,你也可以像郭浩川分享的那样,做一个比较炫酷的烈焰图。XD