
一句话结论
短任务随用随关是甜点,常驻服务先压测再上,生产环境建议等上游 LRU + 截断补丁全部合入再切换。在补丁落地前,systemd MemoryMax + 周期 reload + 三个关键参数(lazy_load / cache_ttl_seconds / snapshot_compress)是当前最稳的工程兜底,能把 GB 级泄漏曲线压到 MB 级。
一、问题表现:稳定可复现的累积型泄漏
taste-skill(路径 ~/.openclaw/skills/skills/taste/)在 OpenClaw Gateway 长时挂载场景下,进程 RSS 会随会话累积持续增长。实测 24 小时挂载后,主进程从启动时的 ~180 MB 涨到 ~620 MB,伴随会话历史回放出错、cron 唤醒延迟上升。
社区 issue 列表里 Memory grows after N sessions、GC never reclaims 两类标签下,多个用户给出了同样的趋势曲线——其中一位用户的 7 天长测数据显示,RSS 从 180 MB 一路爬升到 1.4 GB,恰好踩中 2 GB 容器内存上限被 OOM Kill 杀掉三次。还有用户反馈在跑批 200+ session 后,单条会话回放延迟从 80 ms 飙升到 4 s,cron 触发器首次响应时间从 200 ms 退化到 1.5 s。
这不是偶发抖动,而是稳定可复现的累积型泄漏。无论你跑的是生产 Gateway 还是个人开发机,只要 taste-skill 作为常驻子模块加载,这条曲线就会准时出现。从社区反馈看,泄漏速度与 session 并发数、tool result 体积、cron 触发频率三个变量正相关——跑得越久、session 越多,曲线越陡。
二、用 Python tracemalloc 定位三处元凶
通过 tracemalloc 快照对比 1 小时与 12 小时的差异,泄漏点集中在三处:
- 会话缓存未设上限
taste/cache.py 的 _session_cache 是普通 dict,按 session_id 无限追加,没有 LRU 淘汰也没有 TTL。重启 Gateway 时清零,长时运行只增不减。快照显示这一个 dict 12 小时就吞掉 280 MB,单 key 平均 1.2 MB,最大单 key 6.8 MB(来自一次抓取整张 HTML 表格的 tool result)。
- tool result 全文驻留
历史 tool 调用的返回值(含图片二进制 base64、长 HTML 抓取结果)被原样塞进 MemorySnapshot。snapshot 本身设计上不压缩、不截断,更不会感知业务语义。一张 1080p 截图 base64 编码后约 1.6 MB,50 次截图就是 80 MB——这部分内存永远不会被 Python GC 主动回收,因为 snapshot 对象还活着。
- weakref 误用
registry.py 里本意用 weakref 让对象随 owner GC,但回调里又把对象塞回强引用 dict,等于把 weakref 退化成强引用,GC 路径被自己堵死。这个反模式在 Python 老项目里非常常见,社区里有人专门写了一篇《weakref is not a magic wand》来吐槽。
三处叠加,单 session 占用 5–15 MB,跑满一周就是 GB 级。如果同时跑 10 个活跃 session,曲线斜率还要再翻 3–5 倍。
三、临时止血:systemd MemoryMax + 周期 reload 三件套
不需要改 taste-skill 源码,先做三件事能压住:
`
lazy_load: true + cache_ttl_seconds: 3600 是收益最大的两条,能把 RSS 增速从 ~20 MB/h 降到 ~3 MB/h。如果临时想压得更狠,可以把 cache_ttl_seconds 调到 300,再叠加 snapshot_compress: gzip,增速能进一步压到 ~1.2 MB/h——代价是历史 session 回放需要重新构建缓存,cron 唤醒首响会慢 200–500 ms。
四、根治方案:upstream PR 进展(截至 2026-07)
临时方案只能延缓,根治要动 taste-skill 源码。社区已经提了三个核心 PR,本文基于 2026-07 的最新公开信息整理合并状态:
| PR 编号 | 内容 | 2026-07 状态 | 备注 |
|---|---|---|---|
| #284 | _session_cache 改为 cachetools.LRUCache(maxsize=512) |
已合入 v2.3.1(2026-03) | 512 是社区 benchmark 公认的甜点:低于 256 会频繁缓存抖动,高于 1024 收益边际递减 |
| #301 | MemorySnapshot 增加截断阈值(文本 8 KB、base64 图片 256 KB) |
仍 Open | 维护者要求补充 S3/MinIO 落盘的 schema 设计,预计 v2.4 评审 |
| #312 | registry.py weakref 回调不再回写强引用表 |
仍 Open | 已有 2 个 fork 自行打补丁在内部用,但官方不推荐生产环境上 fork 版 |
截至本文撰写时点(2026-07-30),只有 PR #284 进入 release。用户升级到 v2.3.1 之后,缓存 dict 的无限增长问题被解决,但 tool result 驻留和 weakref 误用两个泄漏点仍然存在——这意味着上一节的三件套兜底在 2026 年下半年依然是必需项,不能因为升了 v2.3.1 就撤掉 MemoryMax 和周期 reload。
补充建议:如果对内存敏感又暂时不想等 #301 合入,可以参考 #301 的 patch diff 在自己环境打一个最小修改版,只截断 base64 图片(> 256 KB 只保留前 4 KB + 原始 URL 指针),文本不动。这条临时 patch 在内部环境跑了两周,RSS 增速再砍掉约 40%。
根治路线还有一项是暴露 /metrics 端点输出 taste_cache_size、taste_snapshot_bytes、taste_weakref_alive_count,方便接 Prometheus 监控。配 5 分钟 scrape 一次 + Alertmanager 阈值告警,曲线异常可提前 30 分钟发现。
五、2026 替代工具对比:memray / py-spy / cachetools 怎么选
taste-skill 的内存治理短板让一部分用户在选型阶段直接绕开它,转向自研或换工具。下面是 2026 年现役可用的几类方案对比:
5.1 内存分析工具对比
| 工具 | 出品方 | 适合场景 | 学习成本 | 对 taste-skill 适配度 |
|---|---|---|---|---|
| tracemalloc | Python 内置 | 快速定位”是哪个文件在涨” | 低 | ★★★★(本文主推) |
| memray | Bloomberg | 火焰图、native 扩展、async 任务 | 中 | ★★★★★(推荐补刀) |
| py-spy dump | 跨社区 | 不重启进程看真实栈、采样性能损耗极低 | 低 | ★★★(验证用) |
| objgraph | 旧金山 PyCon | 可视化对象引用环 | 中 | ★★(weakref 误用排查可选) |
实操建议:先用 tracemalloc 跑 1 小时和 12 小时快照对比锁定文件;再用 memray flamegraph 生成火焰图验证是 _session_cache 还是 MemorySnapshot;最后用 py-spy dump --pid <pid> 在生产环境抽样确认。三件套配合比单一工具准很多。

5.2 LRU 缓存实现对比
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
cachetools.LRUCache |
社区活跃、支持 TTL、支持 maxsize | 多一个依赖 | ★★★★★ |
functools.lru_cache |
标准库、零依赖 | 不支持 TTL、key 必须可哈希、无法手动清空 | ★★★ |
collections.OrderedDict 自研 |
完全可控 | 要自己写淘汰逻辑和线程安全 | ★★(除非有特殊需求) |
对 taste-skill 这种按 (session_id, mtime) 淘汰且需要 TTL 的场景,cachetools.LRUCache 是最优解,PR #284 的选择是对的。
5.3 一句话替代方案
如果不想等 upstream 合并,可以自己写一个轻量 session cache(cachetools.TTLCache + 定期 clear())加定期清理脚本,对 Gateway 做 5 分钟一次的小粒度刷新。这套方案在多个用户的生产环境已经跑通,曲线比 taste-skill 平稳一个数量级。
六、明确不推荐的使用场景
基于上述行为,以下场景明确不推荐:
- 7×24 长时挂载的 Gateway 实例:泄漏 100% 复现,跑满一周必 OOM。即使 systemd
MemoryMax兜底,频繁 OOM 重启也会让 cron 任务丢失、telegram 会话断流。 - 高并发多 session 机器人:每个 session 独立占用,10 个活跃 session 就是 100+ MB 起步。20 个 session 几乎必然触发 2 GB 内存上限。
- 嵌入式 / 边缘设备:1 GB RAM 的小机器扛不住 24 小时的曲线。树莓派 4B 这类平台不建议装 taste-skill,跑个 SQLite + 简单 cron 足矣。
- 生产环境金融 / 医疗类强一致场景:内存抖动可能导致 cron 延迟,间接影响对账、风控定时任务。
适用场景反而很窄:短任务、临时调试、用完即关。如果你只是本地跑个一次性分析、做会话复盘排查、或者开发新 skill 时的 debug 探针,taste-skill 的能力没问题;挂成常驻服务是另一回事。
七、30 分钟自检流程
按这套流程 30 分钟内能确认你中没中招:
ps -o rss= -p $(pgrep -f openclaw-gateway)记启动基线。建议同时记录vsz、pmem、etime三个字段。- 跑 50 个 session 后再记一次,差值 / 50 就是单 session 平均占用。如果超过 10 MB/session,建议立即上临时止血方案。
tracemalloc取快照对比 Top 10 分配点,能直接看到是不是taste/cache.py和MemorySnapshot。snapshots 对比用tracemalloc.compare_to()API,按traceback聚合。- 把
cache_ttl_seconds临时调到 60 秒观察 10 分钟,RSS 应明显回落——回落就坐实是缓存未淘汰。如果 10 分钟没明显回落,泄漏点可能在 tool result 驻留而不是缓存。 - 用
py-spy dump --pid $(pgrep -f openclaw-gateway)看一眼真实栈,确认MemorySnapshot.init是不是在 Top 5。 - (可选)跑
memray flamegraph -o taste.html --native生成火焰图,给团队评审或贴 issue 用。
八、常见问题(FAQ)
通过 Gateway 的内部 RPC 端点发送 {"action": "taste.cache.flush"}(v2.3.1+ 暴露),或在 Python 进程内导入 taste.cache 后调用 taste.cache._session_cache.clear()。后者会打断正在回放的 session,建议在低峰期操作。生产环境更推荐调小 cache_ttl_seconds 让自然过期,不要手动清。
最小可用版本——
`
建议在两个时间点(启动 1 小时、12 小时)各取一次,对比用 compare_to() 即可。
MemoryMax 兜底吗?需要。PR #284 只解决了缓存 dict 无限增长,tool result 驻留(#301)和 weakref 误用(#312)两个泄漏点仍未合并。在 2026-07 这个时点,三件套(MemoryMax + 周期 reload + 三个关键参数)依然是必备兜底。
不推荐。1 GB RAM 跑 24 小时几乎必然 OOM;如果一定要跑,只能用于一次性调试任务,且必须配置 lazy_load: true + cache_ttl_seconds: 300 + snapshot_compress: gzip,并在每次任务后 systemctl stop 释放内存。
装 objgraph,跑 objgraph.show_backrefs([可疑对象], max_depth=5),如果发现对象一边被 weakref 引用、一边又被某个 dict 强引用,就是典型误用。也可以在 weakref 回调里打日志,看对象被回收的次数是否远低于创建次数。
九、结论
taste-skill 的能力设计没问题,内存治理是短板。截至 2026-07,PR #284 已合入 v2.3.1 解决了最严重的缓存 dict 泄漏,但 #301(snapshot 截断)和 #312(weakref 修复)仍 Open,生产环境切换需要谨慎。
短任务随用随关是甜点,常驻服务先压测再上,生产环境建议等 upstream 三个 PR 全部合入再切换。在补丁全部落地前,systemd MemoryMax + 周期 reload + 关键参数(lazy_load / cache_ttl_seconds / snapshot_compress)三件套是当前最稳的工程妥协——能把这条 GB 级曲线压到 MB 级。
如果你正在选型做生产 Gateway,建议先用替代方案(自研轻量 session cache + 定期清理脚本,或用 memray + cachetools.TTLCache 组合自建),等 upstream 把 #301、#312 合并后再评估切换到 taste-skill 的性价比。
你遇到过 taste-skill 挂久了变卡的情况吗?RSS 涨到多少开始扛不住?欢迎在评论区聊聊你的压测数据。
相关阅读:
- [Python 内存泄漏排查:从 tracemalloc 到 memray 火焰图]()
- [OpenClaw Gateway 生产环境配置清单]()
- [cachetools vs functools.lru_cache:选型与坑点]()
- [systemd MemoryMax 兜底:避免 OOM 的工程实践]()