同步引擎设计
采集侧的引擎设计:调度、每轮动作、失败与状态机、可观测、部署初始化。它是「数据流动」的实现依据——写入者只有它(api_keys / usage_logs / qpu_history 三张表的写入时机见各表设计);查看面完全只读。
| 项目 |
内容 |
| 状态 |
已实现(2026-09-15) |
| 关联 |
docs/upstream/NewAPI-接口范围.md(上游调用与失败语义)· 四张表字段设计(写入时机)· docs/design/API契约.md(查看面消费的数据形状)· README.md(既定决策与关键事实) |
| 范围 |
调度与执行、失败处理、可观测、部署初始化 |
| 不含 |
前端交互(交互设计)· HTTP 契约(API 契约,已定稿)· 多实例协调 / 历史回填 / 告警渠道(见 §4) |
1. 前提(已由其他文档锁定,不在此重新讨论)
- 统一 tick 方向已定(2026-09-13 作者确认):一轮 = 遍历全部
active key,逐个查日志 → 去重入库
- 后端单进程(README 决策 7,2026-09-13 修订):API 与同步引擎同进程、单实例;前端静态产物独立部署(后端不托管前端产物);「定时器唯一」的前提就是这一个后端进程
- 三个上游调用、失败互不影响(
api_keys §3.3):日志(写 log_synced_at)· 额度(写 usage_synced_at)· /api/status(QPU 采样)
- 物理约束:
/api/log/token 固定返回最近 1000 条(实测 ≈ 3.2 天);同步停摆超约 3 天即永久丢数据(README 事实 4)
- 失败语义(
docs/upstream/NewAPI-接口范围.md §1):401 = key 不存在 / 显式禁用(唯一计入摘除的);403 = 账号封禁;500 = 数据库错误;过期 / 额度耗尽在宽松档仍可查
- 登录 / 登记不阻塞:登记只做验证 + INSERT,首轮同步由引擎立即异步补一轮(API 契约 §2.2)
- 启用需验证:管理面手动启用 = 上游验证通过(
api_keys §3.6);启用成功清零 consecutive_401s
2. 议题清单(2026-09-13 范围确认;逐条推导,结论逐步回填)
2.1 运行形态与调度
| # |
议题 |
初步倾向 |
状态 |
| 1 |
载体:定时器跑在哪 |
已定(2026-09-13):随 Hono 进程内的定时器(后端单进程);不引入 worker 线程、不拆独立进程——影响评估见 §3 |
已定 |
| 2 |
周期值 |
已定(2026-09-13):5 分钟(固定间隔 tick + 防重入)——见 §3.2 |
已定 |
| 3 |
启动行为 |
已定(2026-09-13):进程启动立即跑一轮,随后进常规节奏——见 §3.2 |
已定 |
| 4 |
单轮内执行顺序 |
已定(2026-09-13):全部 active key 串行遍历——见 §3.2 |
已定 |
2.2 每轮的动作
| # |
议题 |
初步倾向 |
状态 |
| 5 |
日志同步 |
已定(2026-09-13):每 key 拉最近 1000 条 → 整批单事务入库(ON CONFLICT (dedupe_key) DO NOTHING;全成或全不写)→ 写 log_synced_at(取请求发起时刻)——见 §3.3 |
已定 |
| 6 |
额度采集 |
已定(2026-09-13):每 key 取上游字段全组 + usage_synced_at(同口径);与日志各自独立(先日志后额度)——见 §3.3 |
已定 |
| 7 |
QPU 采样 |
已定(2026-09-13):独立于 key 循环,每轮一次读 /api/status;值变化才追加——见 §3.3 |
已定 |
2.3 失败与状态机
| # |
议题 |
初步倾向 |
状态 |
| 8 |
401 摘除阈值 N |
已定(2026-09-13):N = 3——见 §3.4 |
已定 |
| 9 |
失败状态区分 |
已定(2026-09-13):只有 401 计数;403 / 500 / 网络不动计数——见 §3.4 |
已定 |
| 10 |
单轮内重试 |
已定(2026-09-13):不重试,等下一轮——见 §3.4 |
已定 |
| 11 |
解密失败(KEYS_MASTER_KEY 变更) |
已定(2026-09-13):记日志、跳过、不影响其他 key;不计数——见 §3.4 |
已定 |
2.4 可观测与完整性
| # |
议题 |
初步倾向 |
状态 |
| 12 |
错误日志 |
已定(2026-09-13):人类可读一行([sync] 前缀 + key_id / 阶段 / 上游状态 / 耗时);不引日志库、不输出 JSON——见 §3.5 |
已定 |
| 13 |
每轮摘要日志 |
已定(2026-09-13):一行「轮次完成:N key,成功 X,失败 Y,新增 Z 条,耗时 W 秒」——见 §3.5 |
已定 |
| 14 |
「3 天窗口」警示 |
已定(2026-09-13):落后 > 24 小时则每轮打 warning(留 2 天人工余量);不做告警渠道——见 §3.5 |
已定 |
2.5 部署与初始化
| # |
议题 |
初步倾向 |
状态 |
| 15 |
QPU 基线 |
已定(2026-09-13):首次启动且 qpu_history 为空时插入当时取值——见 §3.6 |
已定 |
| 16 |
无 key 时 |
已定(2026-09-13):空转不报错;QPU 采样照跑(独立于 key 循环)——见 §3.6 |
已定 |
| 17 |
时间处理 |
已定(2026-09-13):上游 UTC 秒 → timestamptz;同步时刻取服务端时钟;输出统一 +08:00——见 §3.6 |
已定 |
3. 决策记录(逐条推导后回填)
1. 载体:后端进程内的定时器(2026-09-13 定)
- 定时器跑在 API 进程内(后端单进程、单实例);不引入 worker 线程、不拆独立进程(2026-09-13 作者裁决:现在没必要)
- 影响评估结论:一轮的 CPU 占用只在「JSON 解析 + 入库参数组装」两个小片段(各几十 ms 级、5 分钟级低频);且最重的活(SQL 聚合)在 PostgreSQL 进程里跑——对前端响应时间的影响可忽略
- 升级路径(真需要时再走,现在不做):同进程 Worker 线程(先挪 parse / 组装)→ 独立进程
2. 调度节奏(2026-09-13 定,议题 #2–#4)
- 周期 5 分钟:固定间隔 tick(与上一轮耗时无关);防重入——上一轮未结束时本轮跳过并记一条日志(避免轮次叠加)
- 启动立即跑一轮:进程重启(含部署更新)后不等第一个 tick,尽快补齐窗口
- 单轮内串行:全部
active key 逐个处理——对上游友好、日志按 key 顺序可读;数量增长到影响单轮时长时再评估并发
3. 每轮的动作(2026-09-13 定,议题 #5–#7)
- 日志同步(#5):每 key 拉最近 1000 条(上游不支持参数、无法增量)→ 整批单事务入库(
ON CONFLICT (dedupe_key) DO NOTHING)——全成或全不写:失败则 log_synced_at 不前移「数据截止」,下一轮幂等重来
- 额度采集(#6):每 key 取上游字段全组 +
usage_synced_at;与日志各自独立(先日志、后额度,两个独立 try——谁失败都不影响对方写自己的时间戳)
- QPU 采样(#7):独立于 key 循环,每轮一次读
/api/status;值变化才追加一行(qpu_history §4)
- 时间戳口径(#5/#6 共同):一律取该次请求的发起时刻——上游响应内容的上限是「处理请求那一刻」;请求发起至入库完成之间产生的日志不在本次响应内,不能许诺(与「数据截止取最慢」同一保守精神)
4. 失败与状态机(2026-09-13 定,议题 #8–#11)
- 401 摘除阈值 N = 3:任一调用返回 401 → 本轮
consecutive_401s +1;成功即清零;其他失败不动计数。累计到 3 → status = disabled + disabled_source = auto_401(数据保留、不再轮询;复活走管理面启用 = 上游验证通过 + 计数清零)
- 失败区分(#9):只有 401 是「key 失效」信号(上游实证:宽松档下过期 / 额度耗尽仍可查,唯一 401 = key 不存在 / 显式禁用);403 / 500 / 网络 = 其他故障,不计数
- 不重试(#10):单轮内失败不立即重试,等下一轮(5 分钟后自然重试);3 天窗口余量下延迟无碍
- 解密失败(#11):视为该 key 的故障——记服务器日志、跳过本轮、不影响其他 key;不计数、不摘除(非 401 语义),需人工重新登记
5. 可观测(2026-09-13 定,议题 #12–#14)
- 错误日志(#12):人类可读一行——
[sync] 前缀 + key_id / 阶段(日志·额度·QPU)/ 上游状态或错误 / 耗时;输出 stdout / stderr 由 systemd 收集。不引日志库、不输出 JSON(单人维护,journalctl 直读)
- 每轮摘要(#13):一行——
轮次完成:N key,成功 X,失败 Y,新增日志 Z 条,耗时 W 秒
- 超期警示(#14):
log_synced_at 落后超过 24 小时时每轮打一条 warning——距 3 天极限留 2 天人工介入余量;不做告警渠道(超期的可见性已由 UI 的 data_until 承担)
6. 部署与初始化(2026-09-13 定,议题 #15–#17)
- QPU 基线(#15):首次启动时若
qpu_history 为空,插入当时 /api/status 取值(qpu_history §4)
- 无 key 时(#16):空转不报错;QPU 采样照跑(它独立于 key 循环)
- 时间处理(#17):上游
created_at(UTC Unix 秒)→ timestamptz;同步时刻类时间戳取服务端时钟;对外输出统一 +08:00(API 契约 约定 9)
4. 明确不做
- 多实例协调(单进程部署,无并发实例)
- 历史回填 / 补数(上游无此能力,1000 条窗口之外永久不可得)
- 告警通知渠道(无通知对象与渠道;超期信息进服务器日志与 UI 的
data_until)