- A+
编程派微信号:codingpy
本文作者为叶剑烨,原文链接:http://tech.glowing.com/cn/python-profiling/。文中介绍了 Python 中进行性能调试的三种方法,以及如何定制低开销的性能调试器。
全文大约 2300 字,读完可能需要 6 分钟。
Profiling(性能调试)是我一直很感兴趣的一个话题,之前给大家介绍过Datadog这个工具,今天我们来看看Python语言中有哪些方法来做Profiling。
Poorman’s Profiler
最基础的就是使用time.time()
来计时,这个方法简单有效,也许所有写过Python代码的人都用过。我们可以创建一个decorator使它用起来更方便。
这个方法的优点是简单,额外开效非常低(大部分情况下可以忽略不计)。但缺点也很明显,除了总用时,没有任何其他信息。
cProfile
cProfile
是Python标准库中的一个模块,它可以非常仔细地分析代码执行过程中所有函数调用的用时和次数。cProfile
最简单的用法是用cProfile.run
来执行一段代码,或是用python -m cProfile myscript.py
来执行一个脚本。例如,
以上代码的执行结果如下
完整的结果很长,这里我只截取了一部分。结果中每一行包含一个函数的执行信息,每个字段的含义如下
-
ncalls: 该函数被调用的次数
-
tottime: 该函数的总耗时,子函数的调用时间不计算在内
-
percall: tottime / ncalls
-
cumtime: 该函数加上其所有子函数的总耗时
-
percall: cumtime / ncalls
这里我们看到requests.get
的时间主要是花费在_socket.getaddrinfo
和_socket.socket.recv
这两个子函数上,其中_socket.getaddrinfo
被调用了一次,而_socket.socket.recv
被调用了3次。
虽然cProfile.run
用起来非常直观,但在实际调试时并不灵活,并且最后打印出来的信息过于冗长。我们可以通过写一个decorator让cProfile的使用更方便,但这里我想介绍Python中另一个非常好用的特性,那就是contextmanager。Decorator用来包裹一个函数,而contextmanager则用来包裹一个代码块。例如,我们希望用cProfile来调试一个代码块,可以创建如下的contextmanager
这样就可以很方便地用with
语法对一段代码进行性能调试,以上代码的执行结果如下
因为这里的结果按tottime
排序,并且只显示最费时的5个函数,所以看起来要比之前的结果清晰很多。
Line Profiler
与cProfile相比,Line Profiler的结果更加直观,它可以告诉你一个函数中每一行的耗时。Line
Profiler并不在标准库中,需要用pip来安装
Line Profiler的使用方法与cProfile类似,同样建议为其创建decorator或是contextmanager来方便使用。具体代码可以看这里。
例如,我们用Line Profiler来调试urlparse.parse_qsl
函数,其输出结果如下
Line Profiler的结果虽然直观,但由于它不能显示子函数运行的详细信息,所以实际可以使用的场景比较有限。
定制低开销的Profiler
cProfile虽然是非常不错的性能调试工具,应用场景也很广泛,但它最主要的缺点是有很大的额外开销(overhead)。这使cProfile不适合在生产环境中使用,而且过大的额外开销有时会导致Profiler打开与关闭状态下代码的性能瓶颈不一致。
cProfile高开销来自于它对每一个函数调用进行计时,但很多时候,其实我们只关心某些类库的执行时间。以典型的网络移动应用的服务程序为例,它们的大部分时间花费在I/O上,我们在做性能分析时,关心的是在响应用户请求时,服务器在RPC,Cache和DB这些I/O操作上各花费了多少时间。如果更细致一些,我们也许会关心在一次用户请求中,DB的select/insert/update各被调用了多少次,花费了多少时间。
如果大家熟悉如New Relic之类的APM服务(Application Performance
Monitoring),它们可以将每一个web请求的响应时间分解成RPC/Cache/DB的开销,同时它并不会对服务器造成太多额外的负担。我们也可以自己编写这类的Profiler,需要做的就是那一些特定的I/O函数进行monkey-
patch,将原先的函数替换成带有计时器的函数。比如,我们想对所有的requests.get
函数计时,但又不能更改requests
模块的代码,我们可以这样做
如果你想对requests
模块下所有的公有API计时,我们可以写一个patch_module
辅助函数
那要是我们想对所有redis的访问计时呢?这时我们需要对redis.StrictRedis
这个类下的所有方法进行monkey-
patch,我们可以写一个patch_class
辅助函数
在Github上,我们给出了一个完整的自定义Profiler的例子,它可以patch一个模块或是一个类,对不同的方法归类,profiling结束时打印出各个方法的时间,以及各个类别的总耗时。同时这个Profiler也是线程安全的。由于篇幅有限,这里只给出使用这个Profiler的例子,实现部分请查阅源代码
其执行结果如下
小结
性能调试是一个很大的题目,这里介绍的只是非常初级的知识。在最后一小节给出的例子也是希望大家不要局限于现有的工具,而是能根据自己的需求,打造最合适的工具。欢迎大家交流讨论!