真正有效的办法是结合进程内指标和系统级观测交叉验证:用ps查wchan判断io阻塞,py-spy火焰图定位cpu或io热点,/proc/pid/status看线程内核态等待点,strace补全网络文件调用并观察耗时,同时注意python日志等隐式io瓶颈。

怎么看 Python 进程到底卡在 CPU 还是 IO 上
直接看 top 或 htop 里的 %CPU 和 %WAIT(或 %IOWAIT)不靠谱——Python 解释器线程调度、GIL、系统级采样延迟会让这两个值严重失真。真正有效的办法是结合进程内指标和系统级观测交叉验证。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 用
ps -o pid,ppid,pcpu,pmem,wchan=COMM -p <pid></pid>查wchan:如果显示ep_poll、pipe_wait、wait_event_interruptible等,基本是阻塞在 IO;若显示do_syscall_64或为空,更可能是 CPU 密集 - 用
py-spy record -p <pid> -o profile.svg</pid>(需安装py-spy)生成火焰图:顶部宽而平的栈集中在builtins.sum、numpy.dot、循环体里?CPU 瓶颈;大量栈停在socket.recv、os.read、time.sleep?IO 瓶颈 - 对多线程程序,
cat /proc/<pid>/stack</pid>能看到每个线程当前内核态等待点,比用户态堆栈更准——注意要 root 权限
asyncio 程序里为什么 asyncio.sleep(0) 不能缓解“假 IO 瓶颈”
这不是 IO 瓶颈,是协程没让出控制权导致事件循环饿死。常见于误把同步阻塞调用(如 requests.get、time.sleep、json.loads 大字符串)塞进 async def 函数里。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 用
asyncio.create_task()包裹耗时同步调用时,必须配合loop.run_in_executor,不能只加await asyncio.sleep(0) - 检查是否用了非异步库:比如用
aiohttp发请求但里面混了pandas.read_csv——后者会锁死整个 event loop - 用
asyncio.current_task().get_coro()+ 日志打点,确认协程是否真在等待,还是卡在某行同步代码上
threading.Thread 开很多反而更慢?GIL 不是只锁解释器吗
GIL 锁的是字节码执行权,不是整个进程。但频繁线程切换、争抢 GIL、内存分配竞争(尤其是 list.append、dict[key]=val 这类操作)会让多线程在 CPU 密集场景下比单线程还慢 20%~50%。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 用
perf record -e sched:sched_switch -p <pid></pid>抓上下文切换频次:每秒超 10k 次基本就是 GIL 争抢严重 - CPU 密集任务优先用
multiprocessing,但注意Process启动开销和 IPC 成本——小任务别拆 - IO 密集任务用线程可以,但线程数别盲目设成
os.cpu_count() * 4;实际按并发连接数或文件句柄上限设(比如min(32, ulimit -n // 2))
为什么 strace -p <pid> -e trace=recvfrom,sendto,openat,read,write</pid> 看不到明显 IO 延迟
因为很多 IO 已被缓冲:read() 返回快不代表磁盘快,可能只是从 page cache 读;write() 返回快也不代表落盘,只是进了内核 write buffer。真正的瓶颈可能在刷盘(fsync)、锁文件(flock)、或 DNS 解析(getaddrinfo 不在默认 trace 列表里)。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 加
-e trace=%network,%file覆盖更全,尤其补上connect、getaddrinfo、fsync - 观察
strace输出里系统调用耗时(单位微秒):连续多个recvfrom耗时 > 10ms,且返回值为 0(对端关闭),说明网络层已断连但应用没及时处理 - 配合
iostat -x 1看%util和await:若磁盘%util接近 100% 但await很低,瓶颈在队列深度或并发策略,不是磁盘本身
最常被忽略的是:Python 的 print()、logging 默认同步刷到终端或文件,高频日志本身就会变成 IO 瓶颈——关掉 debug 日志、用 logging.handlers.QueueHandler、或者把 sys.stdout 换成带缓冲的 io.BufferedWriter,效果可能比换框架还明显。










