RUNBOOK:上游耗时字段(ttfb_ms / first_chunk_ms / total_ms)¶
读者:看账本、做时间归因的人|实现体:
proxy.py|变更档案:SOP.md
1. 概览¶
| 字段 | 含义 | 起止点 |
|---|---|---|
ttfb_ms |
上游响应头到达耗时(含 TCP/TLS 建连、请求上传、上游排队与首 token 前的思考) | 请求发出前 → getresponse() 返回后 |
first_chunk_ms |
上游首个正文块到达耗时(≈ 首 token 时间) | 同上 → 首个非空 chunk 读出时 |
total_ms |
上游生成结束耗时(最后一个正文块到达) | 同上 → 最后一个非空 chunk 读出时 |
三者恒满足 ttfb_ms ≤ first_chunk_ms ≤ total_ms。
它们是代理侧观测:不含客户端消费速度(取时点在写回客户端之前),也不含最后一块的回写与连接关闭。
缺失即不写键——重启代理前产生的记录、以及上游异常(502)路径,都没有这三个字段。
与工具侧埋点的分工:本字段管「上游生成侧」,logs/tool_timing.jsonl 管「工具执行侧」,
两者相加即可解释一步间隔(步间隔 ≈ 生成 + 工具 + 客户端与代理往返)。
2. 位置¶
| 用途 | 路径 |
|---|---|
| 数据 | ~/Documents/services/token_proxy/logs/usage.jsonl(append-only,绝不可删) |
| 实现 | ~/Documents/services/token_proxy/proxy.py(_forward / _relay / _record) |
| 离线验证 | ~/Documents/services/token_proxy/upstream-timing-20261007/verify_upstream_timing.py |
| 生效方式 | 改完须 systemctl --user restart token-proxy(重启会打断在途流式回答) |
3. 日常速查表¶
# ① 最近一条的三个字段(确认在采集)
tail -n 1 ~/Documents/services/token_proxy/logs/usage.jsonl | python3 -c "import json,sys;d=json.load(sys.stdin);print({k:d.get(k) for k in ('ts','route','ttfb_ms','first_chunk_ms','total_ms')})"
# ② 采集覆盖率(重启后应逐步趋近 100%)
cd ~/Documents/services/token_proxy && python3 -c "
import json
n=h=0
for ln in open('logs/usage.jsonl',encoding='utf-8',errors='replace'):
try: d=json.loads(ln)
except ValueError: continue
n+=1; h+= 1 if isinstance(d.get('ttfb_ms'),(int,float)) else 0
print('总记录 %d | 含耗时字段 %d(%.0f%%)' % (n,h,100*h/n))"
# ③ 按分位看上游耗时(近 2000 条)
cd ~/Documents/services/token_proxy && python3 -c "
import json,statistics as st
from collections import defaultdict
g=defaultdict(list)
for ln in open('logs/usage.jsonl',encoding='utf-8',errors='replace').read().splitlines()[-2000:]:
try: d=json.loads(ln)
except ValueError: continue
for k in ('ttfb_ms','first_chunk_ms','total_ms'):
if isinstance(d.get(k),(int,float)): g[k].append(d[k])
for k in ('ttfb_ms','first_chunk_ms','total_ms'):
v=sorted(g[k]) or [0]
print('%-16s n=%-6d 中位=%8.0fms p90=%9.0fms max=%9.0fms' % (k,len(v),st.median(v),v[int(len(v)*0.9)-1],v[-1]))"
# ④ 生成侧净时长(total − first_chunk)≈ 吐字阶段
cd ~/Documents/services/token_proxy && python3 -c "
import json,statistics as st
v=[]
for ln in open('logs/usage.jsonl',encoding='utf-8',errors='replace').read().splitlines()[-2000:]:
try: d=json.loads(ln)
except ValueError: continue
if isinstance(d.get('total_ms'),(int,float)) and isinstance(d.get('first_chunk_ms'),(int,float)):
v.append(d['total_ms']-d['first_chunk_ms'])
v=sorted(v) or [0]
print('生成净时长(total−first_chunk):n=%d 中位=%.0fms p90=%.0fms max=%.0fms' % (len(v),st.median(v),v[int(len(v)*0.9)-1],v[-1]))"
4. 日常操作¶
字段不想要了 / 想临时关掉:没有开关。最轻的做法是回滚 proxy.py(见 SOP.md 第 7 章)后重启;
若只是想少看,分析时按 ② 的覆盖率过滤即可,不必改代码。
改了计时口径:改完 python3 -m py_compile proxy.py → 跑一次
python3 upstream-timing-20261007/verify_upstream_timing.py → 再重启。离线验证不可省,
它是唯一能在不打断在线回答的情况下发现"字段为 0 / 缺键"的手段。
5. 配置项¶
本字段不引入任何配置项。相关常量只有假上游的验证延迟(在验证脚本里:
HEADER_DELAY=0.30、CHUNK_GAP=0.15、CHUNKS=3),改它们只影响自测,不影响线上采集。
6. 备份与恢复¶
- 实现体备份:
upstream-timing-20261007/backup/home/rain/Documents/services/token_proxy/proxy.py.bak.20261007-073146(约 51 KB,权限同原文件)→ 恢复步骤见SOP.md第 7 章; - 数据
logs/usage.jsonl无自动备份且不可再生,需留档时手工cp。
7. 监控与告警¶
| 指标 | 健康值 | 说明 |
|---|---|---|
| 字段覆盖率 | 重启后趋近 100%(历史段为 0 属正常) | 速查表 ② |
ttfb_ms 中位 |
与模型/时段相关,看趋势不看绝对值 | 突然整体翻倍 → 上游侧排队或网络异常 |
ttfb_ms p90 / max |
无固定阈值 | 长尾常由上游排队造成,与断点无必然关系 |
| 「0 ms」或「负值」 | 应为 0 条 | 出现即说明计时点被改坏(需回滚) |
8. 故障处置¶
场景 1:字段不出现(全部为 None)¶
症状:tail -n 1 的三个字段都是 None。
诊断
systemctl --user show token-proxy -p ActiveEnterTimestamp --value # 看服务启动时间
stat -c '%y' ~/Documents/services/token_proxy/proxy.py # 与上面对比
python3 -m py_compile ~/Documents/services/token_proxy/proxy.py && echo 语法通过
处置:服务启动时间早于 proxy.py 修改时间 → 说明没重启,执行
systemctl --user restart token-proxy(择无在途回答时)。语法不通过 → 按 SOP 第 7 章还原。
验证:再发一次请求,tail -n 1 三字段为数字且有序。
场景 2:字段恒为 0 或出现负值¶
症状:字段存在但等于 0,或 total_ms < first_chunk_ms。
诊断:说明三处改动没成套——常见是「取时点在 wfile.write() 之后」,
或 sent_at 未随重发路径重置。
处置:按 SOP.md 第 5 章步骤 2 逐条核对三个取时点;改完先跑离线验证(会出现同类断言失败),
通过后再重启。
验证:离线验证 15 项全通过;线上字段恢复有序。
场景 3:total_ms 明显大于客户端观测的墙钟¶
症状:记录里的 total_ms 比客户端实际等待时间长很多。
诊断:total_ms 只到「最后一个正文块读出」为止,正常应略小于客户端墙钟
(差值 = 回写 + 连接关闭)。若明显更大,通常是上游 side 的长时间静默(等待生成),
也可能是客户端中断后服务端仍在读——查 logs/proxy.log 是否有
客户端中断连接 告警。
处置:若告警频繁,属客户端行为(用户点停),非本字段缺陷;如需区分,可在分析时
按 total_ms > 客户端墙钟 过滤后人工判读。
验证:连续观察若干条,正常请求应满足 total_ms ≤ 客户端墙钟。
9. 安全与权限¶
- 三个字段是数值,不含任何请求/响应内容,不涉及凭据;
- 不联网、不新增对外暴露面;
logs/usage.jsonl权限沿用现有(664,属主rain)。
10. 容量与性能¶
- 每行 +60 B;按日均 700 步估算 +40 KB/天(约 15 MB/年),无需轮转;
- 每步 3 次
time.monotonic(),纳秒级; - 已验证:真实账本在离线验证期间零字节变化。
11. 附录¶
相关文档¶
- 变更档案:
SOP.md - 工具侧埋点:
../tool-timing-20261007/RUNBOOK.md - 时间侧全景:
../efficiency-sides-20261007/profile_time_sides.py
修订记录¶
| 日期 | 变更 |
|---|---|
| 2026-10-07 | 首次建立(字段上线,离线验证 15 项通过;生效需重启代理) |
来源:
/home/rain/Documents/services/token_proxy/upstream-timing-20261007/RUNBOOK.md| 最后更新:2026-10-07 07:33(由build/sync.py同步,请勿在站点侧修改)