Back to Journal
02 / Entry· 6 min read

Python 线上进程不重启排障:使用 py-spy 定位 CPU / 内存异常与 mem0 PostHog 线程暴增问题

线上 Python 进程跑一段时间后出现 CPU 飙升 / RSS 增长 / 线程数暴增,且 不能停机或重启。我一般会先用 py-spy 做非侵入式采样,把“线程/热点到底卡在哪”快速具象化,再决定要不要上更重的内存分析工具。

🔊 系统朗读

1. 结论先行(你应该怎么用)

推荐排查顺序(不重启)

  1. 确认现象:CPU / RSS / 线程数是否一起持续上涨
  2. 抓线程现场py-spy dump 连续抓 2–3 次做对比
  3. 看实时热点py-spy top 观察热点函数/线程
  4. 保留证据py-spy record 录制 flame graph(尽量挑峰值时段)
  5. 反查根因:结合初始化入口、依赖版本、上游 issue/changelog

我在线上排查时基本按这个顺序走:先用 dump 把“线程在干嘛”定性,再用 top/record 把热点路径定量。这样不容易被某个瞬时 CPU 抖动带偏。

三条最常用命令

Bash
sudo py-spy dump   --pid <PID> > dump-1.txt
sudo py-spy top    --pid <PID>
sudo py-spy record --pid <PID> -o profile.svg -r 100

我通常会先跑 dump:它最“稳”,对现场干扰也相对可控;确认线程堆积方向之后再开 top 盯一会儿。

2. py-spy 能做什么/不能做什么

2.1 擅长

  • 定位 CPU 热点:当前 CPU 主要耗在哪些函数
  • 观察线程行为:哪些线程在持续执行/阻塞、调用栈是否高度相似
  • 在线取证:不改代码、不重启即可 attach 到目标进程

我喜欢它的点是:线上出事你往往没有“优雅复现”的条件,py-spy 能让我在不停机的前提下把问题缩到一个调用链上。

2.2 不擅长

  • py-spy 不是专门的内存剖析器,不能直接给出对象级别的内存分配明细

如果现象是“CPU + 内存 + 线程数一起异常”,我一般会先用 py-spy:先把“线程/热点来自哪里”搞清楚;确认是线程/后台 worker/telemetry 类问题后,往往都能直接定位到根因。如果确认更像对象泄漏,再上 memray/tracemalloc 做第二阶段。

3. 为什么它适合线上

������������������

对比项py-spy传统内嵌 profiler
是否需要改代码通常需要
是否需要重启通常需要
线上风险相对高
适合 CPU 热点
适合线程现场一般
直接做内存对象分析部分工具可

我在生产环境更倾向于选“外部 attach 的工具”,原因很现实:线上你最缺的是试错空间,越少改动、越少重启,就越安全。

4. 安装与前置条件

4.1 安装

Bash
pip install py-spy

4.2 找到目标 PID

Bash
ps -ef | grep python
# 或
pgrep -af python

4.3 权限注意

  • Linux/macOS attach 通常需要 sudo
  • Docker/K8s 关注 SYS_PTRACE

我踩过的坑:容器里 py-spy 明明装了,但就是 attach 不上,最后发现是 Pod 没开 SYS_PTRACE(以及某些环境有更严格的 seccomp/apparmor)。排查前先把权限链路跑通,能省很多时间。

参考:

5. 三种常用模式(top / dump / record)

模式命令适用场景产出
toppy-spy top --pid <PID>实时看热点终端实时函数热点
dumppy-spy dump --pid <PID>抓某一刻所有线程栈文本栈快照
recordpy-spy record -o profile.svg --pid <PID>录制一段时间离线分析flame graph / speedscope

5.1 top:实时热点

Bash
sudo py-spy top --pid 12345
# 如怀疑 C 扩展:
sudo py-spy top --pid 12345 --native

我用 top 的习惯是:不要只盯“第一名函数”,而是看它是否持续霸榜、以及线程数是否在持续变化(有些问题是线程不断新建,热点函数会跟着变)。

5.2 dump:线程现场

Bash
sudo py-spy dump --pid 12345
# 需要局部变量时(谨慎):
sudo py-spy dump --pid 12345 --locals

经验上,dump 里如果出现“几十/上百个线程堆在同一个 telemetry/队列/网络发送栈上”,大概率就不是业务代码慢,而是后台系统性问题(重复初始化、线程未回收、SDK 内部 worker 失控)。

5.3 record:录制 flame graph / speedscope

Bash
sudo py-spy record -o profile.svg --pid 12345 -r 100
# 录 speedscope:
sudo py-spy record --format speedscope -o profile.json --pid 12345

可选参数:

  • --native:包含 C/C++ 扩展栈
  • --subprocesses:包含子进程
  • -r 200:提高采样率

我一般会等到“指标开始抬头”的时间窗口再录 record,否则容易录到健康期的画像,浪费一次线上取证机会。

6. 推荐排障流程(不关闭服务)

6.1 先确认现象(CPU / RSS / 线程数)

Bash
ps -o pid,ppid,nlwp,rss,%cpu,%mem,cmd -p 12345
top -Hp 12345

我一般先看三件事是否同向变化

  • nlwp(线程数)是否持续上升
  • RSS 是否随时间缓慢“爬坡”(而不是锯齿状可回落)
  • CPU 是否跟着线程数/请求量一起上去

一些“会让我警觉”的信号(不一定是硬阈值,但值得立刻介入):

  • 线程数在 10–30 分钟内持续上涨且不回落
  • RSS 呈线性增长(每小时稳定涨一截)

常见判断:

  • nlwp(线程数)持续上升 → 优先怀疑线程泄漏/重复初始化

6.2 连续抓 dump 做对比

Bash
sudo py-spy dump --pid 12345 > pyspy-dump-1.txt
sleep 30
sudo py-spy dump --pid 12345 > pyspy-dump-2.txt

重点看:

  • 是否大量线程栈高度相似
  • 是否集中在 telemetry/analytics/HTTP 上报/重试/队列等待等路径

我的一个小技巧:抓 2–3 份 dump 的目的不是“看得更全”,而是确认同类线程是不是在不断新增。如果第二份比第一份多了一堆相似栈,基本就能坐实“泄漏/失控”。

6.3 用 top 验证热点是否与可疑线程一致

Bash
sudo py-spy top --pid 12345

如果 top 的热点和 dump 的可疑栈能对上,那定位速度会快很多:你已经从“系统出问题了”推进到“某条调用链在持续消耗 CPU/线程”。

6.4 高峰期 record 保留证据

Bash
sudo py-spy record -o profile.svg --pid 12345 -r 100

观察 flame graph:

  • 最宽(最热)的调用链是谁
  • 是否出现持续创建线程、事件上报、等待网络 I/O 等模式

我很建议把 profile.svg 当成“证据材料”保存下来:后续无论是团队复盘、回归验证,还是给上游提 issue,都非常有用。

7. 案例:mem0 + PostHog 线程暴增 / 内存增长

7.1 现象与线索

  • 每次调用 Memory.from_config() 后内存持续增加
  • 新线程不断创建且不退出
  • 与 PostHog telemetry 模块相关

Issue 线索:

我看这类现象第一反应不是“内存泄漏”,而是先问自己:是不是某个 SDK 在后台偷偷起了线程/worker,而且被重复初始化了。因为线程泄漏往往同时带来 RSS 增长与 CPU 抖动。

7.2 为什么 py-spy 在该类问题里很有效

  • 问题是“运行一段时间后逐步恶化”,且通常不允许随意重启
  • 关键是定位“线程暴增来自哪里”而不是单次请求耗时

这也是我喜欢用 py-spy 的原因:它能让我在现场快速回答“线程到底卡在谁身上”,而不是靠日志猜。

7.3 实战思路(可直接复用)

  1. watch 观察 nlwp 是否持续上涨
Bash
watch -n 2 "ps -o pid,nlwp,rss,%cpu,%mem,cmd -p 12345"

  1. 多次 dump 对比,确认相似线程是否不断出现
  2. top 看热点是否落在 posthog/telemetry/队列/网络发送等路径
  3. record 输出 flame graph 作为证据与前后对比基线

我踩过一个坑:只看一次 dump 很容易误判(比如当时正好有个瞬时尖峰)。多抓几次对比,能把“偶发”与“持续增长”分开。

8. 根因判断与修复建议

8.1 结合 changelog 的验证点

根据 Mem0 Changelog()中修复记录:

  • v1.0.5:修复 telemetry 禁用时 PostHog client 仍初始化
  • v1.0.6:修复 MEM0_TELEMETRY 禁用时仍执行初始化
  • v1.0.11:进一步修复 PostHog 导致的线程与内存泄漏

我做版本核对时会特别注意:同一个方向的修复如果跨了多个版本出现,往往说明问题不止一个触发点(例如“禁用开关不生效”之外还有“重复初始化”)。

8.2 修复建议(优先级从高到低)

  1. 优先升级 mem0 到包含修复的版本(至少对照 1.0.11)
  2. 若不需要 telemetry:确保在 import 前设置环境变量
Bash
export MEM0_TELEMETRY=false

  1. 避免请求级反复调用 Memory.from_config():改为复用长生命周期实例
  2. 修复后做对比验收:线程数曲线、RSS 曲线、dump/flame graph 前后对照

我个人不太建议的做法(踩过坑之后基本不再这么干):

  • 在线上“临时改一下代码/热修复”去关某个 SDK(可控性差、回滚困难)
  • 只看 CPU 不看线程数:很多线程泄漏问题 CPU 反而是后期才明显

9. 使用注意与工具组合

  • py-spy 给的是“采样热点”,不是完整执行日志
  • 如果需要对象级内存泄漏分析,可组合:tracemalloc / memray / objgraph
  • attach 权限:Linux 常见需要 sudo;容器需 SYS_PTRACE
  • 若非常关注暂停影响,可研究 --nonblocking(可能带来采样误差)

线上我会特别注意两点:

  • 权限与安全:确保操作可审计、可回滚,避免把排障变成“引入新风险”
  • 影响面:优先选择短时、低侵入的取证(dump/短 record),不要长时间高采样率占用 CPU

10. 可直接复用的命令清单

10.1 观察线程 / 内存 / CPU

Bash
ps -o pid,ppid,nlwp,rss,%cpu,%mem,cmd -p <PID>
top -Hp <PID>
watch -n 2 "ps -o pid,nlwp,rss,%cpu,%mem,cmd -p <PID>"

10.2 py-spy

Bash
sudo py-spy top    --pid <PID>
sudo py-spy dump   --pid <PID>
sudo py-spy dump   --pid <PID> --locals
sudo py-spy record --pid <PID> -o profile.svg
sudo py-spy record --pid <PID> -o profile.svg --native --subprocesses -r 200
sudo py-spy record --pid <PID> --format speedscope -o profile.json

11. 排障 Checklist

11.1 现场确认

  • CPU 是否持续升高
  • RSS 是否持续升高
  • 线程数是否持续升高(nlwp
  • 是否只能在线排查、不能重启

我一般会把“线程数是否持续升高”放在优先级很靠前:它能把一大类问题(线程泄漏/重复初始化/SDK worker 失控)直接筛出来。

11.2 py-spy 取证

  • 已执行 py-spy top
  • 已抓取至少 2–3 份 py-spy dump
  • 已录制 record 生成 flame graph / speedscope
  • 已确认热点/线程是否集中在某个第三方库

11.3 根因与修复验证

  • 是否存在重复初始化入口(如 Memory.from_config()
  • 是否核对依赖版本与上游 changelog
  • 升级或改造后线程数是否稳定
  • 长时间运行后 RSS 是否不再线性上涨

我做验收时会刻意拉长时间窗口(soak test/长稳压测)。这类问题“短时间看不出来”太常见了。

12. 参考资料

  • py-spy:
  • mem0 issue #3376:
  • mem0 issue #3729:
  • Mem0 Changelog:

分享
← 返回博客列表
🎁 有邀请福利哦,点击查看
🎁