使用 dtrace 跟踪 Python 应用

1,352 阅读5分钟
原文链接: ipfans.github.io

今年在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直接安装:

接下来用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运行状态信息,包括:

首先运行一个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