- docs/design:四张表字段设计 · 交互设计(方向层 + 细节层)· API 契约 · 同步引擎设计 - docs/upstream:sk-key 接口实证(430 行)· 接口范围(9 个接口) - README(全景入口)· AGENTS.md(AI 协作者指引) - 废弃旧稿已删除(不随仓库留存)
113 lines
9.6 KiB
Markdown
113 lines
9.6 KiB
Markdown
# 同步引擎设计
|
||
|
||
> 采集侧的引擎设计:调度、每轮动作、失败与状态机、可观测、部署初始化。它是「数据流动」的实现依据——**写入者只有它**(`api_keys` / `usage_logs` / `qpu_history` 三张表的写入时机见各表设计);查看面完全只读。
|
||
|
||
| 项目 | 内容 |
|
||
|------|------|
|
||
| 状态 | 定稿(2026-09-13:17 条议题全部落定——见 §2 结论索引与 §3 决策记录) |
|
||
| 关联 | `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`)
|