SOP:proxy 内存态落盘持久化(thinking_stages + cache_probe 快照)¶
主机:Ubuntu 192.168.31.250 | 工程:
/home/rain/Documents/services/token_proxy性质:C 类变更(改本地服务代码 + 一处全局规则),单机、无停机窗口要求 日期:2026-10-08
1. 背景¶
代理有两处内存态,重启即丢:
| 内存态 | 丢失的后果 | 实测证据(2026-10-08) |
|---|---|---|
ProxyHandler.thinking_stages(会话思考阶段) |
重启后该会话的下一步按当次请求重算阶段,可能降级 → 注入的强度/请求形态变化 → 上游前缀从中段断裂 | 38 次重启中 31 次有「重启后第一步」,其中 9 次大 miss 合计 1 848 558 未命中 tok ≈ ¥1.85;间隔 ≤90 s 的 4 次(≈¥0.83)可直接归因于重启 |
CacheProbe._snaps(前缀缓存快照) |
重启后该会话的首次空闲保护失效,长空闲后的首步全额重建 | 2026-10-08 01:34:22:间隔 3.1 h、无探测日志、hit=0、重建 104 310 tok |
顺带纠正一条旧归因:此前把大 miss 归给「思考强度翻转」,实测不成立——245 次阶段翻转里
降级仅 7 次且 7/7 全发生在重启后,而最大的三次(42.4 万 / 35.1 万 / 24.9 万)强度根本没变
(coding:high → unknown:high)。翻转只是"刚重启过"的伴随标志。
2. 目标¶
重启后:
- 同一会话的思考阶段不降级(恢复最后已知阶段);
- 同一会话若空闲 ≥
cache_probe.idle_seconds,仍能触发探测(快照已恢复); - 以上两点不给请求热路径增加 I/O(快照只在退出时落盘)。
3. 前置条件¶
- Python 3.12(
/home/rain/Documents/services/token_proxy/venv); - 能
systemctl --user restart token-proxy的普通用户权限; - 落盘目录可写:
logs/state/(可用环境变量TOKEN_PROXY_STATE_DIR覆盖,离线验证用)。
4. 影响范围¶
- 单机(本机代理进程),不涉及其他主机;
- 不需停服:重启为用户级服务,中断约 1~2 s;
- 影响面:Trae 侧正在进行的流式回答会被打断一次(这就是要攒批重启的原因);
- 在跑业务:仅是本机代理转发,无持久化业务数据。
5. 分步操作¶
步骤 1:加落盘基础设施(proxy.py)¶
在 import yaml 之后新增状态目录常量与原子写函数(_atomic_write_json 失败静默——落盘尽力而为,不得影响业务):
cd /home/rain/Documents/services/token_proxy
grep -n 'STATE_DIR\|_atomic_write_json' proxy.py | head -5
预期:STATE_DIR = Path(os.environ.get("TOKEN_PROXY_STATE_DIR") or ... / "logs" / "state")。
坑:状态目录必须可用环境变量覆盖,否则离线验证会写到线上 logs/state/。
步骤 2:CacheProbe 落盘与恢复¶
grep -n 'def _snap_for_state\|def save_state\|def load_state' proxy.py
预期三处都在。要点:
- _snap_for_state 只存截断后的前缀(探测本来也只用这一段),把单会话体积压到 ~100 KB 量级;
- chars / prompt_tokens 保留原会话的值,恢复后 _truncate_messages 算出的预算与原先一致、
前缀不会被二次截短;
- load_state 丢弃超 SNAP_TTL_SECONDS(24 h)或结构不符的条目 —— 坏状态不得拖垮启动。
步骤 3:thinking_stages 落盘与恢复¶
grep -n 'def _load_thinking_stages\|def _save_thinking_stages' proxy.py
坑(会死锁):thinking_stage_lock 是 threading.Lock,不可重入。
_resolve_thinking_stage 必须在释放锁之后才调 _save_thinking_stages(),否则同一线程二次加锁即死锁。
步骤 4:退出时落盘(SIGTERM)¶
grep -n 'def _install_state_dump_on_term\|signal.signal' proxy.py
systemctl restart 发的是 SIGTERM,故在 main() 里安装处理器:先落盘、再退出。
快照不走请求热路径(只在退出时写),避免每次请求都产生 ~100 KB 写盘。
步骤 5:重启使改动生效¶
systemctl --user restart token-proxy
sleep 1
curl -s http://127.0.0.1:8788/healthz # 预期 {"status":"ok",...}
journalctl --user -u token-proxy -n 5 --no-pager | grep '代理启动'
预期启动日志末段含 状态恢复=阶段N/快照M。
注意:本步会打断当前正在进行的流式回答 —— 同一批改动只做这一次重启。
6. 验证¶
6.1 离线验证(上线前必做,不碰线上)¶
cd /home/rain/Documents/services/token_proxy
./venv/bin/python cache-probe-20261008/offline_verify.py
预期 13 项全通过,其中本次新增 4 项:
| 用例 | 断言 |
|---|---|
| U10 重启后阶段不降级 | 落盘后模拟重启:coding 之后来 collecting → 仍返回 coding |
| U10r 反例:无落盘必降级 | 删掉落盘文件后同样序列 → 返回 collecting(证明 U10 的判据可被证伪) |
| U11 重启后仍能探测 | 建快照 → SIGTERM 落盘 → 重启 → 空闲后再发请求,探测被触发 |
| U11r 反例:无落盘则不探测 | 清掉落盘后重启 → 同情形不探测 |
坑(本次踩到):① 假上游在 U11 之前就 server.shutdown() 了 → 请求挂到超时;解决=把
shutdown() 挪到 U11 之后。② 落盘状态跨轮残留 → U1「首次请求不探测」误报;解决=main() 开头清状态目录。
6.2 线上生效验证 ✅ 已完成(2026-10-08 14:04)¶
cd /home/rain/Documents/services/token_proxy
grep -a -o '状态恢复=[^|]*' logs/proxy.log | tail -3
实测结果 —— 判据闭环
| 启动时刻 | 状态恢复 |
说明 |
|---|---|---|
| 2026-10-08 12:26:37 | 阶段0/快照0 |
首次:旧进程无落盘能力、无可恢复 → 属预期 |
| 2026-10-08 14:04:23 | 阶段4/快照4 |
第二次:非零 → 持久化真的起作用 ✔ |
失败判据均未命中:logs/state/ 两个 .json 存在;日志无 退出前落盘失败。
7. 回滚¶
- 备份位置:
state-persist-20261008/backup/,逐条登记见backup/README.md。 - 回滚步骤:
cd /home/rain/Documents/services/token_proxy
cp -p state-persist-20261008/backup/proxy.py.bak.20261008-122032 proxy.py
rm -rf logs/state
systemctl --user restart token-proxy
curl -s http://127.0.0.1:8788/healthz
- 回滚后验证:启动日志不再出现
状态恢复=;logs/state/不再生成。 - 全局规则那一行单独回滚:恢复
~/.trae-cn/user_rules/backup/rule-agent-efficiency.md.bak.20261008-122032。
8. 风险说明¶
| 风险 | 缓解 |
|---|---|
| 落盘文件膨胀 | 只存截断后的前缀(~100 KB/会话);仅退出时写,不进热路径;max_convs 上限 64 |
| 快照文件损坏 / 半写 | _atomic_write_json 先写临时文件再 replace;load_state 对非 dict、非 list、坏 JSON 一律丢弃 |
| 退出时落盘失败拖住进程 | 处理器内 try/except 兜住,失败只记 退出前落盘失败,照常退出 |
| 锁死锁 | _save_thinking_stages() 一律在 thinking_stage_lock 之外调用 |
| SIGKILL / 崩溃丢失状态 | 接受:退化为原行为(与改动前一致),无新增故障模式 |
| 状态文件写进线上目录 | 离线验证通过 TOKEN_PROXY_STATE_DIR 隔离到 /tmp/cache_probe_test/state |
来源:
/home/rain/Documents/services/token_proxy/state-persist-20261008/SOP.md| 最后更新:2026-10-08 14:09(由build/sync.py同步,请勿在站点侧修改)