代码改变世界

使用 cProfile 分析和定位 Python 应用性能瓶颈

2024-12-13 08:00  曾左  阅读(321)  评论(0编辑  收藏  举报

一、背景需求

性能压测时,发现某接口存在性能瓶颈,期望借助工具定位该瓶颈,最好能定位至具体慢方法。

二、cProfile 简介

cProfile 是 Python 标准库中的一个模块,用于对 Python 程序进行性能分析,它能输出每个函数的调用次数、执行耗时等详细信息,可帮助开发者识别程序中运行缓慢的方法,以便进行性能优化,适合作为上述需求的解决方案。

此外,Python 还内置了使用纯 Python 实现的 profile 模块,与 cProfile 功能一样,只不过 cProfile 是用 C 语言编写,性能更高、开销更小,适合在性能敏感的环境(如线上生产环境)中使用。profile 是纯 Python 实现的模块,性能开销相对较大,但因其用 Python 编写,易于理解和修改,适合学习时使用。

三、使用方法

cProfile 支持三种使用方法,一是硬编码于代码中;二是在 Python 应用启动时加载 cProfile 模块;三是通过 IDE(PyCharm)运行。开发环境建议采用方法三,因其简单易用且结果图表丰富;生产环境建议采用方法二,此方法对代码无侵入性。

1. 硬编码于代码中

示例代码:

import cProfile

def my_function():
    # Some code to profile
    pass

profiler = cProfile.Profile()
profiler.enable()
my_function()
profiler.disable()
profiler.print_stats()

执行结果:

2 function calls in 0.000 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    1    0.000    0.000    0.000    0.000 test.py:3(my_function)
    1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

结果字段说明:

ncalls:函数调用的次数。

tottime:在该函数中花费的总时间,不包括调用子函数的时间。

percall:tottime 除以 ncalls。

cumtime:该函数及其所有子函数中花费的总时间。

percall:cumtime 除以原始调用次数。

filename:lineno(function):函数所在的文件名、行号和函数名。

2. 在 Python 应用启动时加载 cProfile 模块

示例代码:

python -m cProfile my_script.py # 方法一、将结果输出至控制台
python -m cProfile -o output.prof my_script.py # 方法二、将结果保存到指定的prof文件

可使用 snakeviz 插件(安装方法为pip install snakeviz)分析 prof 文件。执行snakeviz output.prof后,会将结果挂载到 web 容器中,支持通过 URL(如http://127.0.0.1:8080/snakeviz/xxxoutput.prof)访问。

3. 通过 IDE(PyCharm)运行

使用方法:

通过菜单 [run] > [profile 'app'](其中 app 为应用名称,下同)启动应用。待应用执行完毕且停止后,相关面板会输出相应的调用统计与调用链。

调用统计:

img

表头“Name”表示被调用的模块或函数;“Call Count”表示被调用次数;“Time (ms)”表示耗时及百分比,时间单位为毫秒。

点击表头列名可对该列进行排序。

在调用统计中,选择“name”列的单元格,右键选中“Navigate to Source”或“Show on Call Graph”,可打开其源码或对应的调用链及位置。

调用链:

img

此外,通过菜单 [run] > [Concurrency Diagram 'app'] 启动程序,可查看到线程和异步协程(Asyncio)的调用情况,如下图所示:

img

四、相关配置项

1. cProfile

[root@test bin]# python3 -m cProfile -h
Usage: cProfile.py [-o output_file_path] [-s sort] [-m module | scriptfile] [arg] ...

Options:
  -h, --help            show this help message and exit
  -o OUTFILE, --outfile=OUTFILE
                        Save stats to <outfile> #  将分析结果输出到指定的文件中。
  -s SORT, --sort=SORT  Sort order when printing to stdout, based on pstats.Stats class # 指定输出结果的排序方式。可以根据不同的字段进行排序,如 time, cumulative, calls 等。
  -m                    Profile a library module # 分析一个模块,而不是一个脚本文件 

2. snakeviz

[root@test bin]# snakeviz --help
usage: snakeviz [-h] [-v] [-H ADDR] [-p PORT] [-b BROWSER_PATH] [-s] filename

Start SnakeViz to view a Python profile.

positional arguments:
  filename              Python profile to view

options:
  -h, --help            show this help message and exit
  -v, --version         show program \`s version number and exit
  -H ADDR, --hostname ADDR hostname to bind to (default: 127.0.0.1) # 用于指定绑定的主机名,默认值为 127.0.0.1,即本地主机。
  -p PORT, --port PORT  port to bind to; if this port is already in use a free port will be selected automatically (default: 8080) # 用于指定绑定的端口。如果指定的端口已被占用,程序将自动选择一个空闲端口。默认值为 8080。
  -b BROWSER_PATH, --browser BROWSER_PATH  name of webbrowser to launch as described in the documentation of Python\'s webbrowser module: https://docs.python.org/3/library/webbrowser.html # 按照 Python 的 webbrowser 模块的文档描述,指定要启动的浏览器名称。用户可以通过指定浏览器的路径来控制使用哪个浏览器打开应用。
  -s, --server          start SnakeViz in server-only mode--no attempt will be made to open a browser # 仅在服务器模式下启动 SnakeViz,不会尝试打开服务器中的浏览器。对于非图形化或不带浏览器的服务器非常有用。

五、生产环境使用示例

生产环境系统版本为 CentOS 7.9.2009 (Core),内核为 5.15.81,使用 4 核 4G 的容器运行DB-GPT controller 子系统。

1. 使用步骤

(1)执行脚本:/usr/local/bin/python3.10 -m cProfile -o out.prof /usr/local/bin/dbgpt start controller &,在 Python 应用启动时加载 cProfile 模块。

(2)执行相关接口压测。

(3)正常停止应用,生成性能分析结果文件(out.prof)。注意:性能分析结果只能是在程序正常停止后才能输出。常规两种做法:其一、后台守护进程,可以使用 kill -2 {应用 PID};其二、前台进程,通过 Ctrl + C 退出。

(4)使用snakeviz -H 0.0.0.0 -s out.prof分析结果文件(其中 -s 仅在服务端模式下运行,不会尝试打开服务器浏览器,一般情况下服务器不自带浏览器;-H 0.0.0.0,支持监听网卡所有接口),执行成功后,会输出可访问的 URL 地址,通过外部或本地浏览器打开。

2. 结果分析

使用外部或本地浏览器访问 snakeviz 生成的 URL 地址。结果如下:

结果说明:

(1)结果包含两部分,即上图和下表。上图展示选中方法及其子方法的调用关系、耗时及占比;下表展示所有方法及其总调用次数(ncalls)、方法本身总耗时(tottime)、方法本身平均耗时(percall)、方法及其子方法总耗时(cumtime)、方法及其子方法平均耗时(percall)以及方法所在文件位置及其行列号。

使用说明:

(1)表格任意列支持升降序操作,选中任意行,页面上方的图形会自动展示该方法及其子方法的调用关系、耗时及占比。

(2)单击图形中的任意子模块,可查看子模块所在方法及其子方法的调用关系、耗时及占比。

分析建议:

(1)选择 cumtime 列降序,选择入口代码,逐步细看,分析瓶颈点。

(2)使用 Sunburst 图样展示,更易体现各方法耗时占比。

3. 评估加载 cProfile 对性能的影响

我们用 Jmeter 对未加载以及加载 cProfile 模块的 Python 应用性能进行评估,以判断生产环境加载 cProfile 对性能的影响程度。结果如下:

配置 Jmeter 压测线程数 CPU 使用率 吞吐量 平均响应时间
CASE1 某应用未加载 cProfile 20 接近单核 100% 527 36ms
CASE2 某应用加载 cProfile 后 20 接近单核 100% 395 49ms

从上表可知,加载 cProfile 后,应用吞吐量下降 25%,平均响应时间增加 13ms,对性能有一定影响。

五、遇到问题

1. kill -15 {应用 PID} 无法生成性能分析结果文件

由于 cProfile 仅支持监听中断(SIGINT)信号,导致 kill 15 发送 SIGTERM 信号时,无法生成性能分析结果文件。

解决办法:使用 kill -2 {应用 PID}。

六、使用总结

(1)cProfile 可以生成详细的性能分布和调用链,非常适合作为分析和定位 Python 应用性能瓶颈的工具。

(2)由于生成性能分析结果文件需要停止应用,且对性能损耗较大(吞吐量降低 25%),所以一般情况下不建议在生产环境直接使用。不过可以使用流量复制,将生成环境的流量复制到测试或预生产环境,这样既能定位实际性能瓶颈,又不影响线上业务。