跳转至

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. 目标

重启后:

  1. 同一会话的思考阶段不降级(恢复最后已知阶段);
  2. 同一会话若空闲 ≥ cache_probe.idle_seconds,仍能触发探测(快照已恢复);
  3. 以上两点不给请求热路径增加 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 同步,请勿在站点侧修改)