1. 结论先行(你应该怎么用)
推荐排查顺序(不重启)
- 确认现象:CPU / RSS / 线程数是否一起持续上涨
- 抓线程现场:
py-spy dump连续抓 2–3 次做对比 - 看实时热点:
py-spy top观察热点函数/线程 - 保留证据:
py-spy record录制 flame graph(尽量挑峰值时段) - 反查根因:结合初始化入口、依赖版本、上游 issue/changelog
我在线上排查时基本按这个顺序走:先用 dump 把“线程在干嘛”定性,再用 top/record 把热点路径定量。这样不容易被某个瞬时 CPU 抖动带偏。
三条最常用命令
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 安装
pip install py-spy
4.2 找到目标 PID
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)
| 模式 | 命令 | 适用场景 | 产出 |
|---|---|---|---|
top | py-spy top --pid <PID> | 实时看热点 | 终端实时函数热点 |
dump | py-spy dump --pid <PID> | 抓某一刻所有线程栈 | 文本栈快照 |
record | py-spy record -o profile.svg --pid <PID> | 录制一段时间离线分析 | flame graph / speedscope |
5.1 top:实时热点
sudo py-spy top --pid 12345
# 如怀疑 C 扩展:
sudo py-spy top --pid 12345 --native
我用 top 的习惯是:不要只盯“第一名函数”,而是看它是否持续霸榜、以及线程数是否在持续变化(有些问题是线程不断新建,热点函数会跟着变)。
5.2 dump:线程现场
sudo py-spy dump --pid 12345
# 需要局部变量时(谨慎):
sudo py-spy dump --pid 12345 --locals
经验上,dump 里如果出现“几十/上百个线程堆在同一个 telemetry/队列/网络发送栈上”,大概率就不是业务代码慢,而是后台系统性问题(重复初始化、线程未回收、SDK 内部 worker 失控)。
5.3 record:录制 flame graph / speedscope
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 / 线程数)
ps -o pid,ppid,nlwp,rss,%cpu,%mem,cmd -p 12345
top -Hp 12345
我一般先看三件事是否同向变化:
nlwp(线程数)是否持续上升- RSS 是否随时间缓慢“爬坡”(而不是锯齿状可回落)
- CPU 是否跟着线程数/请求量一起上去
一些“会让我警觉”的信号(不一定是硬阈值,但值得立刻介入):
- 线程数在 10–30 分钟内持续上涨且不回落
- RSS 呈线性增长(每小时稳定涨一截)
常见判断:
nlwp(线程数)持续上升 → 优先怀疑线程泄漏/重复初始化
6.2 连续抓 dump 做对比
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 验证热点是否与可疑线程一致
sudo py-spy top --pid 12345
如果 top 的热点和 dump 的可疑栈能对上,那定位速度会快很多:你已经从“系统出问题了”推进到“某条调用链在持续消耗 CPU/线程”。
6.4 高峰期 record 保留证据
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 实战思路(可直接复用)
watch观察nlwp是否持续上涨
watch -n 2 "ps -o pid,nlwp,rss,%cpu,%mem,cmd -p 12345"
- 多次
dump对比,确认相似线程是否不断出现 top看热点是否落在 posthog/telemetry/队列/网络发送等路径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 修复建议(优先级从高到低)
- 优先升级 mem0 到包含修复的版本(至少对照 1.0.11)
- 若不需要 telemetry:确保在 import 前设置环境变量
export MEM0_TELEMETRY=false
- 避免请求级反复调用
Memory.from_config():改为复用长生命周期实例 - 修复后做对比验收:线程数曲线、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
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
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:
