Skip to content

ADR 0007 API Performance Telemetry

Haisu Zuo edited this page Oct 10, 2026 · 1 revision

ADR-0007: 超管接口性能统计(采集、聚合落库与每日慢接口邮件)

状态:已接受(随 GH-186 交付) 日期:2026-10-08 作者:dev agent(Haisu 委托) 关联任务:GH-186(需求讨论:Discussion #180)


变更记录

版本 日期 变更内容
v1 2026-10-08 Initial draft(GH-186 交付件)

1. Context

超管目前无法按 HTTP 接口查看请求量、耗时与历史趋势,难以判断哪些接口变慢、忙闲分布以及退化是否持续。GH-186 确认了产品要求:全量 HTTP 路由采集、小时/日聚合、15 分钟批量落库、720 小时/180 天保留、超管原生页面、持续变慢检测与每天最多一封邮件提醒。

约束:

  • 首版只覆盖一个 Web 进程的全部 HTTP 路由;不为统计新增 worker、实例或外部服务(不装 Prometheus/Grafana/ClickHouse/SigNoz)。
  • 接受异常退出丢失最近未落库的诊断数据;不为统计引入逐请求持久日志、无损队列或补偿系统。
  • 不收集任何正文、query、header、token、用户/租户/message ID、SQL 参数或聊天/媒体内容;不保存逐请求样本。
  • 统计与业务共用同一个 PostgreSQL(同故障域);请求路径上不得新增 DB I/O。
  • 首版不做历史基线算法,用"固定阈值 + 持续时间"发现退化;不做 SQL/trace 下钻。

2. Decision

2.1 采集层:纯 ASGI 中间件 + 自研有界内存聚合器(未引入 prometheus_client)

工单首选写明"优先 prometheus_client"。实施后未引入该依赖,改用约几百行的自研聚合器,理由如实记录:

  1. 本票的落库语义是小时/日分桶快照(count/sum/min/max/状态计数/直方图,按小时轮换、按 15 分钟增量合并),而 prometheus_client 的原语是进程生命周期累计值(Counter/Histogram 无精确 min/max),每 15 分钟都要"上次快照 vs 当前值"求差,min/max 仍要自研,等于两套状态。
  2. 明确不安装 Prometheus/Grafana,exposition 格式没有任何消费方;引入依赖只增加维护面。
  3. AGENTS.md 禁止未经批准引入大依赖;工单写的是"优先"而非强制,属工程默认裁量。
  4. 工单要求"每个 app 实例拥有独立采集状态,测试及 app factory 不共用全局 registry"——自研结构从构造上就是 per-app 实例,避免 prometheus_client 默认全局 registry 的误用风险。

采集在纯 ASGI 层完成(非 BaseHTTPMiddleware,避免其已知 SSE/后台任务问题):

  • 接口身份 = HTTP 方法 + 路由模板(Starlette 匹配后写入 scope["route"],响应阶段读取);未匹配合并为 unmatched;非标准方法合并为 OTHER。标签全部来自路由表,永不使用用户输入。
  • 注册路由清单与观测合并:页面同时展示从未调用过的路由("无样本")与已下线路由的历史(DB 中仍有行)。
  • 计时:time.monotonic();普通响应记录到最后一个 body chunk 发出;响应之后执行的后台任务不计入。异常、取消/断连单独分类(exception/cancelled),不伪装成功;取消不计入耗时分布(口径见页面说明)。
  • 流式(SSE):以响应 Content-Type: text/event-stream 识别。记录"响应开始耗时"(到响应头/首 chunk,页面明确标注是响应开始、不是首 token——首版无路由级首业务事件埋点)与"流持续时间"两个独立口径;流持续时间不参与普通接口排名;HTTP 200 后的流错误单独计数。
  • 排除面:health 探针、/docs|/redoc|/openapi.json、/web/static 静态资源、本模块自身的页面与 API 默认标记为 infra/self,从业务排名排除(仍可查询)。WebSocket 与后台任务不冒充 HTTP 接口(中间件只处理 type == "http")。

2.2 聚合与持久化

  • 新增三张表:api_performance_endpoint_hourly(小时行)、api_performance_endpoint_daily(日行,独立持久化)、api_performance_flush_batches(批次去重)。另有 api_performance_alert_state 持久化每日邮件额度。
  • 行内容:request_count、success_count、error_count、cancelled_count、有界状态分类计数、耗时 sum/min/max(微秒整数)、成功耗时 sum/min/max、直方图(16 个有限桶 + 溢出,版本号)、流式指标、stats_version、更新时间。唯一键 (method, route, bucket);小时表按 bucket 索引,查询范围有界。
  • 分桶在观测时确定(Asia/Shanghai 自然小时/日,按完成时间归桶);跨小时/午夜的待写批次保留原桶,不按 flush 时间归桶。
  • 请求路径只写内存;每 15 分钟一个后台任务把"已轮换的完整桶 + 当前桶自上次快照以来的增量"批量落库。落库失败时内存增量保留、指数退避重试;进程崩溃丢最近 ≤15 分钟(DB 长期故障可产生更长缺口,页面如实显示)。
  • 批次原子去重:稳定批次 id(实例 id + 序列号);同一事务先 INSERT 批次行(唯一键冲突 = 已应用 → 整体回滚跳过),再应用增量。提交结果未知时复用同一批次 id 重试,最多一次生效,不会重复加总。
  • 加权平均 = sum/count;日 p95 从日行自身的合并直方图估算(不平均小时均值/小时 p95);min/max 在区间内维护;最高有限桶溢出时 p95 上界如实标注("≥ 阈值")。直方图版本不同的桶不直接相加(页面显示不可估)。
  • 保留清理独立于请求流量,随 flush 循环低频分批执行(有界 DELETE),只删本模块数据;迟到重试不复活已过期桶(落库侧按保留边界拒收过期增量);批次去重记录保留 48 小时,冻结批次的重试期限为 24 小时——重试窗口始终被去重保留覆盖,超期的批次放弃并如实计数丢失。
  • 当前小时/天展示"已落库值 + 最多约 15 分钟刷新延迟";近期异常状态由内存短窗口独立提供,不等落库。
  • 状态口径保守:区分"确认无流量 / 未启用 / 无样本 / 采集或持久化故障 / 可能缺口";无法证明完整时不假称完整。

2.3 短窗口检测与邮件

  • 每接口维护内存分钟窗口(最近 15 分钟、每分钟一桶、有界);检测循环每 60 秒执行,普通与流式分开判断,阈值与最低样本量可配置。默认(工程默认,非 SLO 承诺):成功响应最近 5 分钟样本 ≥20 且 p95 > 1000ms,连续约 5 分钟成立才算持续异常;流式用"响应开始耗时"单独阈值。
  • 重启后窗口重新积累,样本不足时页面显示"样本不足",不发信;"未达最低样本"永不显示为"健康"。
  • 复用 app/email.py 既有事务邮件边界(provider 配置、单次尝试、不自动重试);新增一个公开发送函数,不新增邮件服务。
  • 当日额度在发送前持久化:api_performance_alert_state 按(实例, 北京自然日)唯一;插入意图失败 = 不发送。发送异步后台执行,内容固定(慢接口列表、窗口、样本量、耗时/阈值、超管入口),不含业务内容。
  • provider 返回 bool 无法区分"拒绝/未知":首版保守策略 = 每天一次发送尝试、未知不自动重发;页面如实显示 未配置/待发送/已接受/失败或未知,不伪造精度。

2.4 页面与 API

  • 沿用平台控制台 HTML 壳 + __TOKEN__ 模板 + 独立 JS 风格;侧栏"基础设施"组新增"接口性能"入口。
  • API 全部挂在 require_platform_admin 之后(页面用 optional 变体重定向登录),匿名与普通租户管理员均不可读;查询参数全部有界校验(分页 ≤100、时间范围钳制),响应不含缓存指令之外的用户数据。
  • 接口列表支持方法/模板/分类筛选,按请求数/平均/p95/错误率排序;最近 180 天日趋势;最近 720 小时内任选一天看小时趋势;单接口与全站加权汇总分开提供;第 31–180 天明确说明小时明细已过期;零观测的 min/max/avg/p95 显示为空而非 0。

3. Consequences

收益:超管可回答"哪些接口慢、何时忙、退化是否持续";观测开销为每请求一次有界内存更新(纳秒级锁内算术),无逐请求 I/O。

代价与已接受的限制(必须显式报告):

  • 单进程范围:多实例部署各存各的统计(行内含实例可区分语义?——否,聚合行按接口×桶合并,多实例写同一行会竞争合并;首版明确只支持单 Web 进程,多实例属于未来扩展)。
  • 崩溃/部署丢最近 ≤15 分钟诊断数据;DB 长期故障产生更长缺口;无补偿。
  • p95 是直方图估算值(桶宽决定误差),最高桶溢出时只有下界。
  • 邮件一天最多一封但Provider 接受 ≠ 收件箱送达;未知结果不重试。
  • 统计与业务同库,极端情况下统计写入与业务争抢同一故障域(清理与落库都有界、低频)。
  • 无逐请求追踪、无 SQL 慢因下钻、无基础设施监控。

未来扩展:多实例按 instance_id 分行或预聚合、Prometheus exposition、历史基线、trace 下钻均可在不改接口身份语义的前提下叠加。

4. Alternatives Considered

  1. prometheus_client / prometheus-fastapi-instrumentator:见 2.1。instrumentator 还需逐项验证异常与 SSE 口径不重复计数,与本票的自定义分桶落库语义不匹配。
  2. 逐请求写 DB / 写日志文件:违背"请求路径无 DB I/O"与"无逐请求持久日志"的已确认要求。
  3. 独立指标库(ClickHouse/VictoriaMetrics):新增服务与部署面,明确越界。
  4. BaseHTTPMiddleware:对 SSE/流式与后台任务有已知的包装语义问题,选纯 ASGI。
  5. 按 flush 时间归桶:实现简单但会把跨午夜请求算错天,违背已确认口径。

5. References

  • GH-186、Discussion #180
  • docs/adr/0002-module-boundaries-and-composition-root.md(模块边界)
  • app/email.py(事务邮件边界,GH-148)
  • backend/tests/test_architecture_boundary.py(可执行的边界真源)

Clone this wiki locally