cprofile 直接分析 uvicorn 无效,因其多 worker 和异步调度导致 profile 覆盖不到真实业务代码;需强制单同步 worker、手动包裹请求逻辑或抽离同步函数,聚焦 cumtime 与调用链,配合 pstats 离线分析。

cProfile 能定位 Web 后端服务的性能瓶颈,但必须绕开 ASGI 服务器(如 Uvicorn)的多进程/异步调度干扰,否则看到的大多是 select、epoll_wait 或线程锁耗时,而不是你业务代码的真实瓶颈。
为什么直接跑 python -m cProfile -m uvicorn app:app 基本没用
Uvicorn 默认启用多 worker(--workers N),而 cProfile 只能 profile 当前 Python 进程 —— 你启动的主进程只是管理器,真正处理请求的是子进程,它们完全逃逸在 profile 范围外。更麻烦的是,async/await 切换、event loop 调度、IO 等待这些不计入 tottime,但恰恰是 Web 服务慢的主因。
- 看到大量
threading.Lock.acquire或select.select耗时?那是并发模型本身,不是你的 bug -
json.loads的tottime很低,但cumtime占比高?说明它被高频调用,比如在循环里反复解析同一份响应体 - 所有函数
cumtime都很短,总耗时却很长?大概率卡在数据库查询、HTTP 外部调用或磁盘 IO 上 —— 这些在cProfile里几乎不计时
只 profile 单 worker + 同步模式下的真实请求路径
先关闭干扰项,把问题收敛到可分析的范围:强制 Uvicorn 用 1 个同步 worker,并用 httpx 或 requests 模拟单次请求,再用 cProfile 包裹整个请求处理链。
- 改写启动方式:
uvicorn app:app --workers 1 --loop sync --http httptools(禁用 asyncio,避免协程调度干扰) - 在 FastAPI/Starlette 的路由函数里,用
cProfile.Profile()手动启停:prof.enable()放在函数开头,prof.disable()放在 return 前 - 或者更干净:把核心逻辑抽成独立同步函数(如
def handle_user_request(user_id: int) -> dict:),然后单独 profile 它,彻底剥离 Web 框架胶水代码
重点关注 cumtime 和调用者关系,别信 tottime
Web 服务慢,90% 不是因为某个函数内部计算重,而是因为它被反复调用、或它触发了未缓存的 IO。所以排序必须用 SortKey.CUMULATIVE,再用 pstats 查谁在调用它。
- 运行后执行:
python -m pstats profile.out,然后输入sort cumulative→top 15 - 发现
get_user_profilecumtime占比 42%?接着输callers get_user_profile,看是不是被router.py:87的某个 for 循环调用了 200 次 - 如果
cumtime高但ncalls是 1,立刻检查它内部是否包含未加索引的数据库查询、重复的open()文件读取,或没用lru_cache的纯函数
生产环境慎用,优先导出文件离线分析
全量开启 cProfile 会让 Uvicorn 吞吐下降 5–10 倍,且生成的 .prof 文件可能达百 MB。线上只能临时开,且必须限定 scope。
- 不要用
-o直接写磁盘到高 IO 路径(如/tmp),改用内存文件系统或本地 SSD 路径 - 加条件开关:只对特定 query 参数(如
?profile=1)或特定用户 ID 触发 profile,避免全量采样 - profile 文件必须用
pstats离线加载分析,别在生产机上跑print_stats()—— 字符串拼接本身就会吃 CPU
最常被忽略的一点:cProfile 对 async 函数无效,哪怕你 profile 的是顶层 async def,它也只统计到 coroutine object 创建那一下。真要分析异步瓶颈,得换 py-spy 或用 trio/anyio 自带的 instrumentation —— 但那是另一个问题了。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











