使用 dtrace 跟踪 Python 应用

今年在 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

Built with Hugo
主题 StackJimmy 设计