18 个版本
0.3.14 | 2022年9月7日 |
---|---|
0.3.12 | 2022年5月17日 |
0.3.11 | 2021年11月13日 |
0.3.8 | 2021年7月31日 |
0.1.8 | 2018年9月22日 |
#74 in 性能分析
927 每月下载量
用于 3 个 crate (2 直接使用)
1MB
26K SLoC
py-spy: Python 程序的采样分析器
py-spy 是一个 Python 程序的采样分析器。它允许您在不重启程序或修改代码的任何方式下可视化您的 Python 程序花费时间的地方。py-spy 具有极低的开销:它是用 Rust 编写的,以速度著称,并且不与被分析的 Python 程序运行在同一个进程中。这意味着 py-spy 在生产 Python 代码上使用是安全的。
py-spy 在 Linux、OSX、Windows 和 FreeBSD 上运行,支持分析所有最新的 CPython 解释器版本(版本 2.3-2.7 和 3.3-3.10)。
安装
可以从 PyPI 使用预构建的二进制 wheel 安装
pip install py-spy
您还可以从 GitHub 发布页面 下载预构建的二进制文件。
如果您是 Rust 用户,也可以使用以下命令安装 py-spy:cargo install py-spy
。
在 macOS 上,py-spy 在 Homebrew 中,可以使用以下命令安装:brew install py-spy
。
在 Arch Linux 上,py-spy 在 AUR 中,可以使用以下命令安装:yay -S py-spy
。
在 Alpine Linux 上,py-spy 在测试仓库中,可以使用以下命令安装:apk add py-spy --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ --allow-untrusted
。
用法
py-spy 从命令行工作,接受您想要采样的程序的 PID 或您想要运行的 Python 程序的命令行。py-spy 有三个子命令 record
、top
和 dump
record
py-spy 支持使用 record
命令将配置文件记录到文件中。例如,您可以通过以下操作生成您 Python 进程的 火焰图:
py-spy record -o profile.svg --pid 12345
# OR
py-spy record -o profile.svg -- python myprogram.py
这将生成一个类似以下交互式 SVG 文件:
您可以使用 --format
参数将文件格式更改为生成 speedscope 配置文件或原始数据。有关其他选项的信息,包括更改采样率、仅包括持有 GIL 的线程、分析原生 C 扩展、显示线程 ID、分析子进程等,请参阅 py-spy record --help
。
top
Top 显示了您的 Python 程序中耗时最多的函数的实时视图,类似于 Unix 的 top 命令。运行 py-spy 与以下命令:
py-spy top --pid 12345
# OR
py-spy top -- python myprogram.py
将显示您 Python 程序的高层次实时视图
dump
py-spy 还可以使用 dump
命令显示每个 Python 线程当前的调用栈
py-spy dump --pid 12345
这将输出每个线程的调用栈和一些其他基本进程信息到控制台
这在您只需要一个调用栈来找出您的 Python 程序卡住在哪里的情况很有用。此命令还可以通过设置 --locals
标志来打印出与每个堆栈帧关联的局部变量。
常见问题解答
为什么我们需要另一个 Python 分析器?
该项目旨在让您分析和调试任何运行的 Python 程序,即使该程序正在处理生产流量。
尽管有许多其他 Python 分析项目,但几乎所有的这些项目都需要以某种方式修改被分析程序。通常,分析代码会在目标 Python 进程内部运行,这将减慢程序运行并改变其操作方式。这意味着通常不安全地在生产服务中调试这些问题,因为这些分析器通常会对性能产生明显的影响。
py-spy 如何工作?
py-spy 通过在 Linux 上使用 process_vm_readv 系统调用、在 OSX 上使用 vm_read 调用或在 Windows 上使用 ReadProcessMemory 调用直接读取 Python 程序的内存来工作。
确定 Python 程序的调用栈是通过查看全局 PyInterpreterState 变量来获取解释器中运行的所有 Python 线程,然后迭代每个线程中的每个 PyFrameObject 来获取调用栈。由于 Python ABI 在不同版本之间有所变化,我们使用 rust 的 bindgen 为我们关心的每个 Python 解释器类生成不同的 rust 结构,并使用这些生成的结构来了解 Python 程序中的内存布局。
由于地址空间布局随机化,获取Python解释器的内存地址可能有些棘手。如果目标Python解释器包含符号,则可以通过解引用interp_head
或_PyRuntime
变量来轻松地找出解释器的内存地址,具体取决于Python版本。然而,许多Python版本在Windows上发布时带有去除符号的二进制文件,或者没有相应的PDB符号文件。在这些情况下,我们会扫描BSS部分以查找可能指向有效PyInterpreterState的地址,并检查该地址的布局是否符合我们的预期。
py-spy可以分析本地扩展吗?
是的!py-spy支持分析用C/C++或Cython等语言编写的本地Python扩展,在x86_64 Linux和Windows上。您可以通过在命令行中传递--native
来启用此模式。为了获得最佳结果,您应该用符号编译您的Python扩展。对于Cython程序来说,也值得注意的一点是,py-spy需要生成的C或C++文件来返回原始.pyx文件的行号。请阅读博客文章以获取更多信息。
我如何分析子进程?
通过将--subprocesses
标志传递给记录视图或顶部视图,py-spy还将包括任何是目标程序的子进程的Python进程的输出。这对于分析使用多进程或gunicorn工作池的应用程序非常有用。py-spy将监视新进程的创建,并自动将其附加到进程中,并将它们的样本包括在输出中。记录视图将包括每个程序的PID和cmdline,子进程将作为其父进程的子进程出现。
何时需要以sudo运行?
py-spy通过从不同的Python进程读取内存来工作,这可能因操作系统和系统设置而受到安全限制。在许多情况下,以root用户(使用sudo或类似方式)运行可以绕过这些安全限制。OSX始终需要以root身份运行,但在Linux上则取决于您如何启动py-spy以及系统安全设置。
在Linux上,默认配置是在附加到非子进程的进程时要求root权限。对于py-spy来说,这意味着您可以通过让py-spy创建进程来无需root权限进行分析(py-spy record -- python myprogram.py
),但通过指定PID来附加到现有进程通常需要root权限(sudo py-spy record --pid 123456
)。您可以通过设置ptrace_scope sysctl变量来在Linux上取消此限制。
如何检测线程是否空闲?
py-spy试图仅包含活动运行代码的线程的堆栈跟踪,并排除处于休眠状态或其他空闲状态的线程。当可能时,py-spy会尝试从操作系统获取这些线程活动信息:在Linux上通过读取/proc/PID/stat
,在OSX上通过使用mach thread_basic_info调用,以及在Windows上通过查看当前SysCall是否已知为空闲。
尽管这种方法有一些局限性,可能会导致空闲线程仍然被标记为活跃。首先,我们必须在暂停程序之前获取这个线程活动信息,因为从暂停的程序获取这些信息将导致它始终返回这是空闲的。这意味着存在潜在的竞争条件,即我们获取线程活动后,当获取堆栈跟踪时,线程处于不同的状态。在FreeBSD和Linux上的i686/ARM处理器上,查询操作系统以获取线程活动尚未实现。在Windows上,由于I/O而阻塞的调用也尚未被标记为空闲,例如从stdin读取输入。最后,在某些Linux调用中,我们使用的ptrace attach可能会导致空闲线程瞬间唤醒,导致从procfs读取时出现误报。因此,我们还有一个启发式回退,将已知的某些Python调用标记为空闲。
您可以通过设置--idle
标志来禁用此功能,这将包括py-spy认为的空闲帧。
GIL检测是如何工作的?
我们通过查看指向Python 3.6及以前版本中_PyThreadState_Current
符号的线程id值来获取GIL活动,并通过确定Python 3.7及以后版本中_PyRuntime
结构的等效值来获取GIL活动。这些符号可能不包括在您的Python发行版中,这将导致解析哪个线程持有GIL失败。当前的GIL使用情况也在top
视图中以%GIL的形式显示。
传递--gil
标志将仅包括持有全局解释器锁(Global Interpreter Lock)的线程的跟踪。在某些情况下,这可能更准确地反映您的Python程序如何花费其时间,尽管您应该意识到这可能会错过在仍然活跃时释放GIL的扩展中的活动。
为什么我在OSX上对/usr/bin/python进行性能分析时遇到问题?
OSX有一个名为系统完整性保护的功能,该功能阻止即使是root用户也无法从位于/usr/bin中的任何二进制文件读取内存。不幸的是,这包括随OSX一起提供的Python解释器。
处理此问题有几个不同的方法
- 您可以安装不同的Python发行版。内置的Python将在未来的OSX中被移除,您可能还希望迁移到Python 2之外的版本。
- 您可以使用virtualenv在SIP不适用的环境中运行系统Python。
- 您可以通过禁用系统完整性保护。
如何在Docker中运行py-spy?
在Docker容器内运行py-spy通常也会出现权限拒绝错误,即使以root身份运行。
此错误是由于Docker限制了我们所使用的process_vm_readv系统调用。可以通过在启动Docker容器时设置--cap-add SYS_PTRACE
来覆盖此限制。
或者,您可以编辑docker-compose yaml文件
your_service:
cap_add:
- SYS_PTRACE
请注意,您需要重新启动Docker容器才能使此设置生效。
您还可以使用主机OS中的py-spy来分析运行在Docker容器内的正在运行的过程。
如何在Kubernetes中运行py-spy?
py-spy需要SYS_PTRACE
来读取进程内存。Kubernetes默认情况下会删除此功能,从而导致错误
Permission Denied: Try running again with elevated permissions by going 'sudo env "PATH=$PATH" !!'
处理这个问题推荐的方法是编辑规范并添加该功能。对于部署,通过以下方式实现:将以下内容添加到 Deployment.spec.template.spec.containers
securityContext:
capabilities:
add:
- SYS_PTRACE
更多详细信息请参考这里: https://kubernetes.ac.cn/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container 注意,这将删除现有的Pod并重新创建。
如何在Alpine Linux上安装py-spy?
Alpine的python默认不支持manylinux
wheels: pypa/pip#3969 (comment)。您可以通过以下步骤使用pip在Alpine上安装py-spy来覆盖此行为:
echo 'manylinux1_compatible = True' > /usr/local/lib/python3.7/site-packages/_manylinux.py
或者您可以从GitHub发行页面下载musl二进制文件。
如何避免暂停Python程序?
通过设置--nonblocking
选项,py-spy不会暂停您正在分析的Python目标。虽然使用py-spy从进程中进行采样通常性能影响极低,但设置此选项将完全避免中断您的运行Python程序。
设置此选项后,py-spy将读取Python进程的运行时解释器状态。由于我们用于从内存读取的调用不是原子的,并且我们需要发出多个调用以获取堆栈跟踪,这意味着在采样时偶尔会出错。这可能导致采样时的错误率增加,或者部分堆栈帧包含在输出中。
py-spy支持32位Windows吗?与PyPy集成?与Python2的USC2版本一起工作?
目前还不支持。
如果您希望在py-spy中看到某些功能,请为相应的问题点赞或创建一个新的问题,描述缺少的功能。
如何强制在管道到分页器时输出彩色信息?
py-spy遵循CLICOLOR规范,因此将CLICOLOR_FORCE=1
设置到您的环境中,py-spy将即使在管道到分页器时也会打印彩色输出。
致谢
py-spy深受Julia Evans在rbspy上的出色工作的启发。特别是,生成火焰图和speedscope文件的代码直接来自rbspy,该项目使用了从rbspy分离出来的read-process-memory和proc-maps crate。
许可证
py-spy在MIT许可证下发布,有关完整文本,请参阅LICENSE文件。
依赖关系
~16–28MB
~442K SLoC