前言
當我們寫完一個程式,對程式運作調試的時候,往往會遇到一些問題,有時候是程式運作過慢,有時候是程式啟動後占用記憶體過高,還有比較少見的一種情況是記憶體一直增加,産生記憶體洩漏問題。那麼其實我們要解決的問題可以從三個方向出發。1. 為什麼程式這麼慢?2. 為什麼程式占用記憶體這麼高?3.遇到記憶體洩露問題如何定位?
對于上述問題,可以查找到很多對應的工具,官方的profile,memory_profiler庫,objgraph庫,graphviz工具第三方的工具等等,但是很多工具都有特定的使用場景,或者工具本身就對程式有侵入性,會影響最終的結果。在這裡我主要介紹自己踩坑使用下來能夠去通用性的解決上述問題的兩個工具:用于調查記憶體洩漏問題的官方包tracemalloc 和 無侵入式的用來調查程式性能瓶頸的開源庫 py-spy。
1.解決 "為什麼程式這麼慢?"
諸如此類的問題,首先可以檢視 python 程式的 cpu 使用率,如果在使用單核單程序模式下,cpu 使用率低下,無法達到100% 左右,可以優先從程式邏輯中查找是否是有磁盤或者網絡io 占用資源過多,排查掉io 的問題後在從代碼方面出發。
一. 使用py-spy 生成火焰圖
repo 位址 https://github.com/benfred/py-spy 。
py-spy是一個非常好用而且簡單的庫,看完他的readme 介紹文檔基本就可以入手使用spy。這個工具一是可以生成profile 火焰圖,二是可以定位到程式中最耗時間的代碼的位置。它的優點在于完全不用修改代碼,相比較其他的一些性能調查工具,py-spy這一點非常棒,當你debug 一個線上正在運作的程式的時候,隻需要提供程序id,py-spy 就可以直接生成火焰圖。
sudo py-spy record -o profile.svg --pid 12345
"12345" 為程式運作的pid,當運作這行指令的時候,py-spy 開始抽樣的程式simlple 并且生成火焰圖,我們可以等待1分鐘左右 ctrl+c 結束,這時候會在運作這行指令的目前目錄下生成 profile.svg 火焰圖, 如下圖:

火焰圖的分析非常簡單直覺,主要是看"平頂",看圖中最下方那個峰頂是平的,那麼程式的性能問題就可以從這裡入手去解決,這裡不詳細介紹火焰圖看法,不明白的同學可以自行百度。
通過生成火焰圖分析程式瓶頸大機率可以找到并解決80%的程式性能問題,但是還有一種問題,如果我的火焰圖沒有平頂,但是程式依舊很慢,該如何定位問題?
二. 沒有平頂情況下,定位程式中耗時最多函數/代碼
如下圖,通過火焰圖并沒有發現程式中的平頂

這時候要用到py-spy 提供的 top 指令
sudo py-spy top --pid 12345
輸入上述指令後,在控制台會顯示程式實時的運作狀态,這裡可以介紹一下圖中4個參數的含義, 然後可以通過按1,2,3,4 四個按鍵,讓程式按照下圖所述排序。
1按%Own排序(目前在該函數中花費的時間的百分比)
2按%Total排序(函數及其子級中目前的時間百分比)
3按OwnTime排序(函數中花費的總時間)
4按TotalTime排序(該函數及其子項花費的總時間)
比較直覺的 使用3 , 可以比較直接的看出程式運作中,所占比消耗時間最多的函數,然後從函數如圖進行分析,如下圖,可以看出 是wrap 裝飾器函數消耗的時間最長,我們用wrapt 這個c寫的裝飾器進行替換後效率有了明顯的提升。

總結 : 使用py-spy 相對于其他一些python性能分析工具,優勢在于使用非常簡單,而且無須對代碼做任何改動,并且可以在保護現場情況下,直接生成火焰圖,還可檢視實時程式運作狀态。
2. 解決 "為什麼記憶體這麼高"
記憶體洩漏問題其實在日常工作中比價難以遇見,我們現在使用 python ,golang ,java 大多數進階語言都已經自帶了一套完成的垃圾回收機制(gc),是以一般遇見記憶體無限增加(記憶體洩漏)問題,比較難以debug。在這裡主要使用的是 python 的官方提供的工具包 tracemalloc 。中文文檔:
https://docs.python.org/zh-cn/3.7/library/tracemalloc.html 通過閱讀文檔,文檔中包的用法已經寫的非常的詳細了,這裡我們主要通過擷取兩個程式運作時候的快照照片就行比對,看哪裡記憶體進行了增長。
import tracemalloc
tracemalloc.start()
# ... start your application ...
# 進行一次記憶體快照片
snapshot1 = tracemalloc.take_snapshot()
# 在兩次快照之間跑你的程式
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print("[ Top 10 differences ]")
for stat in top_stats[:10]:
# 列印出來記憶體增加最多的前十個代碼位址。
print(stat)
通過記憶體的比對,可以準确定位到記憶體洩漏的位址。詳細的還請看官方文檔,但是目前個人感覺tracemalloc 包定位記憶體洩漏問題是最簡潔直接的。