一文詳解如何分析python程序cpu占用率問題
問題
某天我看了一下目前在跑的一個 python 程序的 CPU 利用率,發(fā)現(xiàn)跑到了 100%

這個程序本身執(zhí)行的任務(wù)很多,開啟了很多的線程,跑到 100% 也可以說的過去。但是作為程序員的好奇心就被勾引起來了
java 可以通過 “top -Hp + jstack + 代碼” 精準(zhǔn)定位到 cpu 高利用率的原因。那么在 python 中,通過什么方式可以實現(xiàn)相同的目的?
一、方案調(diào)研
經(jīng)過簡單的調(diào)研,python 和 java 排查的思路不太一樣,有下面三種工具可以使用:cProfile、py-spy、yappi
| cProfile | py-spy | yappi | |
|---|---|---|---|
| 使用便捷性 | 無需安裝,使用簡單(直接導(dǎo)入標(biāo)準(zhǔn)庫) | 需額外安裝,命令行操作,可能需要管理員權(quán)限 | 需額外安裝,需在代碼中初始化/啟動/停止 |
| 功能特性 | 統(tǒng)計函數(shù)調(diào)用次數(shù)、執(zhí)行時間等基礎(chǔ)信息,不支持多線程/異步代碼 | 支持多線程/異步分析,可實時分析運行中的進(jìn)程,生成火焰圖 | 支持多線程/協(xié)程分析,可自定義時鐘類型(CPU時間或墻鐘時間) |
| 性能影響 | 對性能有一定影響(大型程序更明顯) | 非侵入式,對目標(biāo)進(jìn)程性能影響極小 | 性能影響較小,但大規(guī)模程序可能有開銷 |
| 分析結(jié)果展示 | 文本輸出(復(fù)雜場景難解讀) | 火焰圖可視化,直觀定位瓶頸 | 詳細(xì)統(tǒng)計數(shù)據(jù)(需學(xué)習(xí)成本),支持多種輸出格式 |
| 適用場景 | 開發(fā)階段初步性能分析(定位耗時函數(shù)) | 生產(chǎn)環(huán)境實時分析(快速定位CPU高負(fù)載問題) | 復(fù)雜多線程/異步程序深度分析 |
二、每個工具實際使用的效果
2.1 cProfile
基本使用方式是修改程序的啟動入口
import cProfile
def main():
... # 程序入口
if __name__ == "__main__":
cProfile.run('main()', sort='cumtime')
然后控制臺啟動程序,運行足夠長的時間后,終止程序(ctrl + c)時就會輸出對應(yīng)的函數(shù)執(zhí)行統(tǒng)計,如下圖:

輸出表格各列含義如下
- ncalls:函數(shù)被調(diào)用的次數(shù)。
- tottime:函數(shù)自身執(zhí)行所耗費的總時間,不包含調(diào)用其他函數(shù)的時間。
- percall:每次調(diào)用函數(shù)所花費的平均時間,即 tottime / ncalls。
- cumtime:函數(shù)執(zhí)行的累積時間,包含調(diào)用其他函數(shù)的時間。
- percall:函數(shù)每次調(diào)用的累積平均時間,即 cumtime / ncalls。
- filename:lineno(function):函數(shù)所在的文件名、行號以及函數(shù)名。
整體上看相對來說不夠直觀,所以我的策略是,將結(jié)果拷貝進(jìn) excel 表格,分別按照 tottime 和 cumtime 排序,然后將結(jié)果取前 20 行放入大語言模型進(jìn)行分析。提示詞如下
你是一位資深的 python 專家和系統(tǒng)運維專家
你的任務(wù)是使用 cProfile 工具排查 python 程序 cpu 利用率過高的問題,并提出對應(yīng)的優(yōu)化和解決建議
cProfile 的輸出如下
按照 cumtime 排序
---
ncalls tottime pe rcall cumtime pe rcall filename:lineno(function)
91313 0.516 0 56.86 0.001 conv.py:53(forward_fuse)
27123 0.685 0 35.403 0.001 block.py:346(forward)
10435/10350 0.086 0 34.571 0 03 {method 'extend' of 'list' objects}
19892 0.086 0 32.822 0.002 block.py:239(<genexpr>)
97642 0.352 0 32.226 0 conv.py:553(forward)
3616 0.097 0 26.887 0.007 block.py:236(forward)
97642 0.259 0 21.341 0 conv.py:536(_conv_forward)
3616 0.106 0 20.761 0.006 block.py:459(forward)
97642 10.577 0 17.058 0 {built-in method torch.conv2d}
2372/2090 0.051 0 15.157 0.007 pooled_db.py:360(cache)
3671/3328 0.115 0 14.816 0.004 serving.py:259(write)
14464 0.043 0 13.573 0.001 block.py:462(<genexpr>)
7713/7070 0.046 0 12.46 0.002 socket.py:500(close)
3669/2219 0.1 0 12.376 0.006 selectors.py:451(select)
3798/3596 0.047 0 12.338 0.003 connections.py:431(_force_close)
2372/2091 0.042 0 12.221 0.006 steady_db.py:326(_reset)
904 0.107 0 11.436 0.013 head.py:317(forward)
87697 0.187 0 11.194 0 activation.py:431(forward)
2372/2091 0.024 0 9.936 0.005 steady_db.py:436(rollback)
3671/3585 0.056 0 9.425 0.003 server.py:493(send_response)
...(省略)
---
按照 tottime 如下
---
ncalls tottime pe rcall cumtime pe rcall filename:lineno(function)
97642 10.577 0 17.058 0 {built-in method torch.conv2d}
1492/89 10.445 0.007 0.751 0.008 {method 'read' of 'cv2.VideoCapture' objects}
3660/317 9.277 0.003 2.85 0.009 {method 'poll' of 'select.poll' objects}
741/0 6.681 0.009 0 {built-in method time.sleep}
3669/2219 3.78 0.001 7.225 0.003 {method 'poll' of 'select.epoll' objects}
1522/185 2.446 0.002 0.4 0.002 {flip}
6328 1.948 0 3.359 0.001 {built-in method torch.einsum}
345354/12 1.613 0 1.368 0.114 module.py:1740(_call_impl)
903/1 1.581 0.002 0.003 0.003 {resize}
87697 1.573 0 4.584 0 {built-in method torch._C._nn.silu_}
16399/10112 1.556 0 3.308 0 00 {method 'recv_into' of '_socket.socket' objects}
23770/139 1.544 0 0.19 0.001 {method 'sendall' of '_socket.socket' objects}
345353/12 1.184 0 1.368 0.114 module.py:1732(_wrapped_call_impl)
3616 1.117 0 1.921 0.001 {built-in method torch._C._nn.linear}
439192 1.1 0 1.347 0 client.py:75(__setitem__)
9352/153 1.043 0 0.016 0 {method 'recv' of '_socket.socket' objects}
53147/50648 0.798 0 2.345 0 00 {method 'settimeout' of '_socket.socket' objects}
516359 0.783 0 0.783 0 module.py:1918(__getattr__)
18081 0.722 0 1.736 0 {built-in method torch.cat}
3616 0.689 0 7.775 0.002 block.py:426(forward)
...(省略)
---
你的分析結(jié)果應(yīng)該包含每個問題的影響程度,以及你分析的依據(jù)(即從 cProfile 哪個數(shù)據(jù)得到的這個結(jié)論)
具體輸出就不展示了,大語言模型每次都可以分析出問題的原因之一在于模型推理存在大量的 cpu 使用(未經(jīng)過工程上的優(yōu)化)。但是一些其他問題,輸出不太穩(wěn)定,例如 connections.py:431(_force_close) 有時能捕捉到是因為頻繁地關(guān)閉數(shù)據(jù)庫連接問題,但很多時候有捕捉不到這個問題。
主要的問題在于,cProfile 輸出的每一行,都是以函數(shù)為維度的,函數(shù)之間的關(guān)聯(lián)無法很好的捕捉到,例如 method 'extend' of 'list' objects 有點模糊。
2.2 py-spy
安裝
pip install py-spy
使用(需要 sudo 權(quán)限),輸出火焰圖
# -o 輸出文件名 # -d 采樣多久 py-spy record -o profile.svg -d 60 -p <pid>
svg 文件可以直接使用瀏覽器打開,從下圖可以看出,火焰圖可以很直觀地定位到一些瓶頸問題

不過遺憾的是,svg 文件大語言模型還無法很好的支持,如果截圖給 AI 分析,又有很多文字省略了。
2.3 yappi
安裝
pip install yappi
使用,對于有執(zhí)行結(jié)束的程序
yappi.set_clock_type("cpu") # cpu or wall
yappi.start()
my_function()
yappi.stop()
stats = yappi.get_func_stats()
# stats.print_all()
stats.save('yappi_stats.callgrind', type='callgrind')
對于無法結(jié)束的程序,例如網(wǎng)絡(luò)接口,可以如下(使用 kill -10 <pid> 來觸發(fā)寫入結(jié)果,實測寫的過程會 hang 住主進(jìn)程,最好再單獨開啟一個寫文件線程!)
import yappi
import signal
def start_all():
app.run(debug=False, host='0.0.0.0', port=5000)
def handle_sigusr1(signum, frame):
print("接收到 SIGUSR1 信號,正在停止 yappi 分析并保存結(jié)果...")
yappi.stop()
stats = yappi.get_func_stats()
stats.save('yappi_stats.callgrind', type='callgrind')
print("yappi 分析結(jié)果已保存為 yappi_stats.callgrind")
if __name__ == '__main__':
yappi.set_clock_type("cpu")
yappi.start()
# 注冊 SIGUSR1 信號處理函數(shù)
signal.signal(signal.SIGUSR1, handle_sigusr1)
start_all()callgrind 是一種特殊格式的文件,windows 可以使用 QCacheGrind 查看(下載地址)

我仔細(xì)看了這里列的熱點代碼,整體上來說和 cProfile 以及 py-spy 相差較多,基本分析不出來什么有用信息。
我也不知道是不是自己使用姿勢有問題,但如果這個工具學(xué)習(xí)成本這么高,說明它實際上也不是一個簡單易用的工具。
總結(jié)
整體使用下來,cProfile 和 py-spy 的結(jié)果是可以相互印證的。但我還是 比較推薦 py-spy,原因如下
- 安裝簡單
- 不需要改代碼,使用方便
- 火焰圖結(jié)果看起來直觀易懂
- 看文檔還可以把完整的執(zhí)行棧 dump 出來,這樣實際可以和 java 的排查思路一致了(上面例子中沒有嘗試,后面有實際問題可以嘗試一下)
當(dāng)然,實際情況下,很可能要多種工具齊上陣,綜合分析才能得到準(zhǔn)確的問題排查結(jié)果。
到此這篇關(guān)于python程序cpu占用率問題的文章就介紹到這了,更多相關(guān)python程序cpu占用率問題內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python將Markdown轉(zhuǎn)換為HTML的主流實現(xiàn)方法對比
在日常的技術(shù)寫作和文檔管理中,Markdown 憑借其簡潔的語法成為眾多開發(fā)者的首選,本文將介紹使用 Python 將 Markdown 轉(zhuǎn)換為 HTML 的常用方法,分別適用于不同的使用場景,大家可以根據(jù)需要進(jìn)行選擇2026-04-04

