Python 程序調(diào)試分析大殺器合集

問題背景
在之前的?debug 系列文章[1]?里,我們曾經(jīng)介紹過一個 Python 進程 hang 住的問題的排查。最近正好又碰到了類似的問題,來簡單調(diào)研總結(jié)一下處理這類問題的排查處理方法,以及各種 Python 調(diào)試,性能分析中可以使用的強大工具。
思路
對于程序 hang 住,但 CPU 等使用率較低的情況,最常用的做法是獲取程序當(dāng)前的 callstack,查看是否在“等待”什么東西。理想情況下,我們希望能直接得到 Python 程序當(dāng)前在執(zhí)行哪一行代碼,定位根源。
對于程序跑得慢,且 CPU 使用率相對正?;虮容^高,說明程序還是在“干活”的。這時候一般會采用各種 profiling 工具,去多次“采樣” callstack,查看程序在各行代碼上的所耗費時間的百分比,以此來定位性能瓶頸。
可見獲取 callstack 是一個共性需求,我們接下來會以此展開,來看看各種處理方法。
常規(guī)方法
先介紹一下不需要額外庫的排查方法,也是很多文章中會提到的常見做法。
如果可以改代碼
如果這個 hang 住的程序是我們自己控制的,比如自己開發(fā)的項目,線下的測試環(huán)境可以修改重啟等,那么我們可以通過植入“信號 handler”的方法來獲取到程序的 callstack,進而定位到底 hang 在哪一行代碼執(zhí)行上。具體代碼如下:
import?code,?traceback,?signal
def?debug(sig,?frame):
????"""Interrupt?running?process,?and?provide?a?python?prompt?for
????interactive?debugging."""
????d={'_frame':frame}?????????#?Allow?access?to?frame?object.
????d.update(frame.f_globals)??#?Unless?shadowed?by?global
????d.update(frame.f_locals)
????i?=?code.InteractiveConsole(d)
????message??=?"Signal?received?:?entering?python?shell.\nTraceback:\n"
????message?+=?''.join(traceback.format_stack(frame))
????i.interact(message)
def?listen():
????signal.signal(signal.SIGUSR1,?debug)??#?Register?handler
然后只要在程序開始執(zhí)行前的代碼里先調(diào)用一下listen(),后面當(dāng)程序卡住時,可以通過另一個 Python 進程來發(fā)送信號:
os.kill(pid,?signal.SIGUSR1)
然后就能得到當(dāng)時的 callstack 了。不過這個方法只能在 Linux/Unix 系的操作系統(tǒng)上使用。
如果不能改代碼
很多情況下,我們并不能修改相應(yīng)的 Python 程序,比如是在線上運行的程序,不能隨意關(guān)掉;或者并不是自己開發(fā)的項目(雖然 Python 庫一般來說都是代碼開放的)。這時候我們可以使用一些 debug 通用進程的方式來進行排查,例如:
可以用 pstack,gdb來打印 native callstack。用 strace來做進程調(diào)用的追蹤。使用 perf等動態(tài)追蹤工具來進行 profiling。
同樣的,這些方法也基本只能在 Linux 系統(tǒng)上使用,且拿到的信息大多數(shù)都是系統(tǒng)調(diào)用,不容易直接映射到對于的 Python 代碼上。
利用專有工具
Java 中自帶的 JDK 里有很多強大的開發(fā)者工具,可以幫助我們來對 Java 程序做各類排查診斷,另外也衍生出了包括?arthas[2]?這樣優(yōu)秀的第三方工具??上?Python 原生并沒有如此完善的工具體系,但好在類似的第三方工具還是有不少的,我們這就來嘗試一下。
示例代碼
首先我們先來“人造”一個非常簡單的程序 hang 住的代碼:
import?time
def?func1():
????print('hello')
????func2()
????print('bye')
def?func2():
????print('enter?func2')
????time.sleep(100)
????print('leave?func2')
def?main():
????func1()
if?__name__?==?'__main__':
????main()
執(zhí)行這個程序,程序會在sleep處 hang 住,然后可以用ps命令獲取到相應(yīng)的 pid。后續(xù)我們希望各類工具都能直接使用這個 pid,而不需要對程序本身進行修改或重啟操作,就能獲取到相關(guān)的 callstack 或更豐富的 profiling 信息。
大家如果對性能 profiling 的排查感興趣,也可以自己構(gòu)造一個消耗較高 CPU 資源的程序(例如啟動 2 個子進程來計算圓周率)來進行后面的各種實驗。
系統(tǒng)工具
我們先用gdb來看下獲取的 callstack 長啥樣,執(zhí)行:
gdb?attach?
然后通過bt命令打印出 callstack:

可見 native 的 callstack 包含的內(nèi)容比較“細節(jié)”,雖然大致可以看出程序是 hang 在了 sleep 操作上,但 Python 內(nèi)部代碼的調(diào)用路徑基本是不可見的。
pystack-debugger
找這個項目的出發(fā)點是想看看能否在gdb里支持 Python 調(diào)用 frame 的信息獲取。其它網(wǎng)站有看到過一些 gdb 的宏的介紹,不過安裝配置比較麻煩,還是這個工具用起來最簡單直接。
項目地址:pystack[3]
安裝:
pip?install?pystack-debugger
執(zhí)行:
pystack?
返回信息:
??pystack?3570
Dumping?Threads....
??File?"hang.py",?line?17,?in?
????main()
??File?"hang.py",?line?14,?in?main
????func1()
??File?"hang.py",?line?5,?in?func1
????func2()
??File?"hang.py",?line?10,?in?func2
????time.sleep(100)
??File?"" ,?line?1,?in?
Wow,效果很好,直接就找到了我們 hang 在了 sleep 這一行上。
hypno
項目地址:hypno[4]
安裝同樣用 pip:
pip?install?hypno
這個庫的目標(biāo)不太一樣,主要是為了 inject 到正在運行的 Python 進程里跑一些代碼,所以靈活性比單純打 callstack 更高,比如可以打印一些變量之類。我們用如下的命令來實現(xiàn)獲取 callstack 的功能:
hypno??"import?traceback;?traceback.print_stack()"
可以看到原來的進程中打印出了我們所需要的信息:
??python?hang.py
hello
enter?func2
??File?"hang.py",?line?17,?in?
????main()
??File?"hang.py",?line?14,?in?main
????func1()
??File?"hang.py",?line?5,?in?func1
????func2()
??File?"hang.py",?line?10,?in?func2
????time.sleep(100)
??File?"" ,?line?1,?in?
leave?func2
bye
這里也能直接定位到進程當(dāng)時運行到的位置,而且“入侵”完成后還不影響原先程序的運行,會一直跑完正常退出。我琢磨著這個工具應(yīng)該還可以做不少其它的騷操作……
py-spy
在搜尋 profiling 相關(guān)工具時看到了這個項目,感覺非常強大。
項目地址:py-spy[5]
安裝:
pip?install?py-spy
獲取 callstack 可以使用 dump 命令:
??py-spy?dump?--pid?5103
Process?5103:?python?hang.py
Python?v3.8.8?(/home/zijie0/miniconda3/envs/playground/bin/python3.8)
Thread?5103?(idle)
????func2?(hang.py:10)
????func1?(hang.py:5)
????main?(hang.py:14)
?????(hang.py:17)
此外py-spy還支持top形式的實時 profiling,以及生成火焰圖(操作稍麻煩)的功能,除了程序 hang 之外也能做性能優(yōu)化的監(jiān)控工具。不過易用性上來說比下面要介紹的austin還是稍微差了些。

在我的電腦上看這個 sample 百分比的記錄好像也有些問題。
austin
本以為py-spy已經(jīng)足夠強大了,沒想到又找到了這個無敵的austin!
項目地址:austin[6],austin-tui[7]
安裝:
conda?install?-c?conda-forge?austin-tui
這是一個集大成的 profiling 工具,不光可以看 callstack,還可以看對應(yīng)的采樣(近似理解為程序時間開銷)百分占比信息,以及實時更新的 flamegraph 等,竟然還是跨平臺的!官網(wǎng)上還有更多高級特性,感覺已經(jīng)可以跟 JDK 里的 visualvm 來媲美了!這個例子里的 callstack 比較簡單,生成出來長這樣:

官網(wǎng)上給的樹狀圖和 flamegraph 對于復(fù)雜程序的性能問題排查會很有幫助:


作者還很貼心的提供了 cheatsheet 來幫助你更好的使用這個工具,以后可以在項目里多嘗試一下了。

值得一提的是在調(diào)研跟austin類似的工具框架時,還找到了這個?pyroscope[8],把 profiling 做成了個平臺(也支持 Python),還成立了家公司進入了 YC 的孵化器,未來說不定也是個不錯的細分市場呀。
其它項目
搜了一圈下來發(fā)現(xiàn) Python 這方面的工具遠比我想象中的要豐富,還有很多相對冷門或者有些時間沒有維護的項目,這里也列舉出來供大家參考:
pyinstrument[9] pystuck[10] PyFlame[11] pyrasite[12] pydbattach[13]
大家在平時工作中有找到過什么好用的 Python 開發(fā)者工具嗎?也歡迎在評論區(qū)留言討論 :)
感謝收看,我們下期再見。
參考資料
debug 系列文章:?https://zhuanlan.zhihu.com/p/385962965
[2]arthas:?https://github.com/alibaba/arthas
[3]pystack:?https://github.com/wooparadog/pystack
[4]hypno:?https://github.com/kmaork/hypno
[5]py-spy:?https://github.com/benfred/py-spy
[6]austin:?https://github.com/P403n1x87/austin
[7]austin-tui:?https://github.com/p403n1x87/austin-tui
[8]pyroscope:?https://github.com/pyroscope-io/pyroscope
[9]pyinstrument:?https://github.com/joerick/pyinstrument
[10]pystuck:?https://github.com/alonho/pystuck
[11]PyFlame:?https://github.com/uber-archive/pyflame
[12]pyrasite:?https://github.com/lmacken/pyrasite
[13]pydbattach:?https://github.com/albertz/pydbattach

還不過癮?試試它們
▲微軟發(fā)布 Python 的 JIT 編譯器:Pyjion
▲Python 函數(shù)為什么會默認(rèn)返回 None?
