343 lines
15 KiB
Markdown
343 lines
15 KiB
Markdown
# 统计分析模块
|
||
|
||
## 1. 定位
|
||
|
||
统计分析模块是 `cpa-ext` 的数据处理层。它把已经持久化的请求、执行、用量、账单和账本事实,转换为可查询、可重建、适合 UI 展示的统计结果。
|
||
|
||
它回答以下问题:
|
||
|
||
- 一段时间内消费了多少钱、发生了多少请求;
|
||
- 金额、请求、Token 和性能随时间如何变化;
|
||
- 哪些 Key、用户、模型和上游账户产生了消费;
|
||
- 成功率、错误、TTFT、Latency 和缓存情况如何;
|
||
- 原始事实是否已被聚合、统计是否滞后、是否存在无法核算的数据。
|
||
|
||
它不负责:
|
||
|
||
- 从 CPA 回调取得事实;
|
||
- 决定请求是否放行;
|
||
- 选择上游账户;
|
||
- 计算一笔请求应该扣多少钱;
|
||
- 修改账单、余额或账本。
|
||
|
||
最重要的边界是:**统计模块只能汇总计费模块已经确定的金额,不能根据 Token 和当前价格重新计算历史费用。**
|
||
|
||
## 2. 输入、处理与输出
|
||
|
||
```text
|
||
Request / Execution / Usage / Billing / Ledger / Identity facts
|
||
│
|
||
▼
|
||
增量读取 → 维度规范化 → 时间分桶 → 聚合计算
|
||
│
|
||
▼
|
||
Rollup rows + independent checkpoints
|
||
│
|
||
▼
|
||
Query services → permission projection → UI/API DTO
|
||
```
|
||
|
||
### 2.1 输入事实
|
||
|
||
| 输入 | 用途 | 权威来源 |
|
||
| --- | --- | --- |
|
||
| `RequestRecord` | 请求数量、终态、入口、下游身份、请求时间 | 采集与 Core |
|
||
| `ExecutionRecord` | 重试、上游账户、实际模型、性能、失败 | 采集与 Core |
|
||
| `UsageRecord` | Token 分项、数据质量、generate 状态 | 采集模块 |
|
||
| `BillingRecord` | 请求对应的已确认金额、价格快照引用、核算状态 | 计费模块 |
|
||
| `LedgerEntry` | 实际扣款、补记、退款、充值和管理员调整 | 计费模块 |
|
||
| `IdentityRecord` | Key、账户、Auth、Provider 的稳定展示维度 | 数据/目录模块 |
|
||
| `QuotaSnapshot` | 上游配额与订阅的时间点分析 | 采集模块 |
|
||
|
||
所有输入必须已经落入持久化层。进程内通知只负责唤醒处理器,不能作为唯一事实来源。
|
||
|
||
### 2.2 处理结果
|
||
|
||
统计处理产生两类数据:
|
||
|
||
- `AggregateRecord`:小时、日、活动度、延迟等可重建聚合;
|
||
- 查询投影:把事实、聚合、目录和权限组合成 API/UI 需要的只读结果。
|
||
|
||
聚合数据不是账本真相。它可以删除、重建和升级;任何重建结果都必须与原始事实和账本一致。
|
||
|
||
## 3. 统一统计口径
|
||
|
||
### 3.1 请求、执行和账单不能混为一层
|
||
|
||
- 用户请求数按 `RequestRecord` 计数;
|
||
- 上游尝试数按 `ExecutionRecord` 计数;
|
||
- 上游失败率可以按 Execution 计算;
|
||
- 用户看到的请求结果按 Request 最终终态计算;
|
||
- 金额按属于该 Request 的有效 LedgerEntry 净额汇总。
|
||
|
||
一次请求发生三次重试时,用户请求数是 1,上游执行数是 3;如果三个执行都产生了**可可靠关联**的上游消费,管理员审计必须能看到三次成本事实。当前 CPA 无 RequestID 的 attempt usage 无法安全归属时,执行仍计数,但金额覆盖率通过 `unmeasured` 单独表达。
|
||
|
||
### 3.2 金额口径
|
||
|
||
金额统计统一使用 `int64 amount_micros`:
|
||
|
||
```text
|
||
请求最终消费 = Σ usage-linked `spend_delta_micros`
|
||
```
|
||
|
||
其中 charge/late settlement 为正、Usage refund 为负;账户充值和普通余额 adjustment 的 spend delta 为 0。余额则单独由 `balance_delta_micros` 推导:扣费为负、充值/退款为正。二者都以金额表示,但不能混为同一个统计口径。
|
||
|
||
禁止:
|
||
|
||
- 使用 `float64` 累计余额;
|
||
- 查询时按当前价格重算历史 Usage;
|
||
- 因价格目录更新而改变已经结算的趋势;
|
||
- 把未定价或未取得 Usage 自动视为确认的免费请求。
|
||
|
||
用户视图只返回金额;管理员视图可以同时返回 Token、单价、倍率、核算质量和价格版本。
|
||
|
||
### 3.3 取消、失败和迟到结算
|
||
|
||
请求终态与金额是两个正交维度:
|
||
|
||
- `outcome=canceled/failed` 但存在可靠 Usage:金额进入正常统计;
|
||
- `outcome=canceled/failed` 且无可靠 Usage:当前金额为 0,同时计入 `unmeasured_count`;
|
||
- 迟到 Usage 产生 `late_settlement`:用差额账本更新原请求所属的业务时间桶;
|
||
- 账本的 `booked_at` 同时保留,用于管理员审计“何时发现并补扣”。
|
||
|
||
因此查询可以同时提供:
|
||
|
||
- `occurred_amount`:按原请求时间归属的消费趋势;
|
||
- `booked_amount`:按实际入账时间归属的账本变化。
|
||
|
||
用户消费趋势默认使用 `occurred_amount`;账本审计默认按 `booked_at` 排序。
|
||
|
||
### 3.4 时间和区间
|
||
|
||
- 事实时间保存 UTC;
|
||
- 所有查询使用半开区间 `[start, end)`;
|
||
- 小时桶以配置时区的钟面整点为边界;
|
||
- 日桶以配置时区的本地自然日为边界;
|
||
- DST 日期允许出现 23 或 25 小时,不能固定假设一天等于 24 小时;
|
||
- 修改统计时区需要显式重建依赖本地边界的聚合。
|
||
|
||
## 4. 目标聚合集合
|
||
|
||
### 4.1 `money_overview_hourly` / `money_overview_daily`
|
||
|
||
这是用户和管理员金额页面的核心聚合。
|
||
|
||
维度按实际查询需求控制:
|
||
|
||
- downstream account / credential;
|
||
- model / model alias;
|
||
- provider / upstream auth;
|
||
- effective service tier;
|
||
- outcome;
|
||
- endpoint。
|
||
|
||
指标:
|
||
|
||
- request count、success/failure/canceled/rejected count;
|
||
- charge、late-settled、Usage refund/correction 的 spend delta micros;
|
||
- final charged amount micros;
|
||
- measured/partial/unmeasured count;
|
||
- input/output/cache/reasoning Token(仅管理员)。
|
||
|
||
不要把所有维度机械组合到一张极宽聚合表。MVP 先覆盖 UI 已经确定的筛选;低频组合可以查日级聚合或受限事实明细。
|
||
|
||
账户充值和非 Usage 人工余额调整只进入 balance/ledger 投影,不具有 model/Auth/endpoint,不能强行归属到本表这些请求维度。
|
||
|
||
### 4.2 `request_health`
|
||
|
||
按时间桶和必要维度保存:
|
||
|
||
- 用户请求数与上游执行数;
|
||
- 成功、失败、取消、拒绝;
|
||
- HTTP/标准错误类别;
|
||
- RPM;
|
||
- 取消率、无核算率、价格不可用率。
|
||
|
||
RPM 是所选窗口请求数除以窗口分钟数。空窗口返回 0;部分窗口必须使用真实覆盖分钟数,不能按完整日除。
|
||
|
||
### 4.3 `usage_activity`
|
||
|
||
参考 `cpa-usage-keeper` 的 Activity 设计,提供:
|
||
|
||
- 日内细粒度活动;
|
||
- 7 天和 30 天活动;
|
||
- 长期自然日活动;
|
||
- 成功/失败以及 Token 分项。
|
||
|
||
第一版可以简化为小时和日两级;但边界函数必须只有一套实现,并同时被聚合、查询和测试使用。
|
||
|
||
### 4.4 `latency_stats`
|
||
|
||
只把满足条件的真实生成请求作为性能样本:
|
||
|
||
- 成功执行;
|
||
- `generate=true`;
|
||
- TTFT、总延迟为有效正值。
|
||
|
||
保存:
|
||
|
||
- sample count;
|
||
- TTFT/Latency 可合并分位 sketch;
|
||
- P50/P95/P99;
|
||
- 精确最大值;
|
||
- 有界、稳定、按 EventID 去重的真实散点样本。
|
||
|
||
不要只保存平均值,也不要为图表永久保存无限散点。
|
||
|
||
### 4.5 `identity_stats`
|
||
|
||
按下游账户、Key、模型、上游 Auth 和 Provider 提供:
|
||
|
||
- 请求与执行数量;
|
||
- 金额;
|
||
- 最近使用时间;
|
||
- 成功率和取消率;
|
||
- 管理员 Token 与性能摘要。
|
||
|
||
目录名称变化只改变展示解析,不应重写历史事实中的稳定 ID。
|
||
|
||
## 5. 增量处理与 checkpoint
|
||
|
||
所有领域事务向统一 `projection_events` 追加变化。该表包含 `event_seq INTEGER PRIMARY KEY AUTOINCREMENT`、`event_id UNIQUE`、kind、fact ID/revision、occurred/booked time、可选 supersedes ID 和确定性 delta payload。外部 UUID/EventID 用于业务幂等,`event_seq` 用于稳定分页和 checkpoint;不能拿时间戳、随机 UUID 或各事实表互不相关的自增 ID 充当全局聚合游标。
|
||
|
||
状态修订必须发布逆向/正向差量。例如 unmeasured 在迟到 Usage 后变为 measured 时,同一 projection event 表达 `unmeasured_count=-1`、`measured_count=+1` 和新增 `spend_delta_micros`,不能只新增 measured 行让请求被计两次。领域事实与 projection event 必须在同一 SQLite 事务提交。
|
||
|
||
每种聚合拥有独立 checkpoint,例如:
|
||
|
||
```text
|
||
money_overview
|
||
request_health
|
||
activity
|
||
latency
|
||
identity
|
||
```
|
||
|
||
处理规则:
|
||
|
||
1. 新事实事务提交后,只发送非阻塞唤醒和最大 `event_seq`;
|
||
2. runner 冻结本轮目标上界,按固定页大小读取事实;
|
||
3. 纯函数在内存中生成确定性的增量行;
|
||
4. 聚合 upsert 与 checkpoint 推进处于同一短事务;
|
||
5. 单类失败只阻止自己的 checkpoint,不冻结其他聚合;
|
||
6. 启动时从数据库最大事实序号和各 checkpoint 自动追平;
|
||
7. SQLite 有前台事实等待写入时,聚合主动让出唯一 writer。
|
||
|
||
同一输入页重复执行必须得到相同结果。推荐使用 `(aggregate_kind, source_event_seq)` inbox/应用记录或等价事务约束,确保崩溃发生在 upsert 与推进之间时仍可安全重试。
|
||
|
||
动态库内后台 goroutine 在目标 CPA 的双 Go runtime 下尚未证明安全。runtime soak gate 通过前,runner 由安全的 host callback/管理查询按时间和页数预算增量驱动,或运行在 sidecar;统计算法与 checkpoint 不依赖“常驻 goroutine 一定存在”。
|
||
|
||
## 6. 实时查询
|
||
|
||
实时视图由两部分组成:
|
||
|
||
```text
|
||
已聚合到 checkpoint 的稳定数据
|
||
+ checkpoint 之后的近期事实右边界补偿
|
||
```
|
||
|
||
近期缓存只是性能优化:
|
||
|
||
- 缓存丢失时回退到 SQLite;
|
||
- 缓存项必须来自已提交事务;
|
||
- 查询以 checkpoint/event_seq 切开两段,不能重复累计;
|
||
- 页面不可见时停止轮询;
|
||
- 实时窗口默认 5~15 分钟,刷新间隔由 UI 控制。
|
||
|
||
每个 widget 返回自身 projector 的 `as_of_seq` 和 lag。一个组合响应若要求强一致,使用所有相关 checkpoint 的最小值作为共同 `as_of_seq`,并分别从该位置做右边界补偿;不能拿 money checkpoint 切 latency/identity 数据。
|
||
|
||
## 7. 查询服务
|
||
|
||
统计模块向 Core/管理 API 提供只读服务,不直接处理 HTTP:
|
||
|
||
```go
|
||
type StatisticsService interface {
|
||
Overview(ctx context.Context, scope QueryScope, filter OverviewFilter) (Overview, error)
|
||
Realtime(ctx context.Context, scope QueryScope, filter RealtimeFilter) (Realtime, error)
|
||
Analysis(ctx context.Context, scope QueryScope, filter AnalysisFilter) (Analysis, error)
|
||
Activity(ctx context.Context, scope QueryScope, filter ActivityFilter) (Activity, error)
|
||
Events(ctx context.Context, scope QueryScope, filter EventFilter) (EventPage, error)
|
||
Diagnostics(ctx context.Context) (AggregationDiagnostics, error)
|
||
}
|
||
```
|
||
|
||
`QueryScope` 必须由 Core 根据已认证主体构造:
|
||
|
||
- 用户 scope 只能查询自己的 account/credential,且 DTO 只含金额;
|
||
- 管理员 scope 才能指定任意 Key/Auth/model 并查看 Token、价格与核算质量;
|
||
- Repository 不能信任浏览器传来的 account ID;
|
||
- 分页、时间上限、导出数量和查询复杂度必须由服务端限制。
|
||
|
||
交付阶段固定为:
|
||
|
||
- MVP:money hourly/daily(合并基本 request outcome/quality count)、用户金额摘要/趋势/最近请求、管理员 Overview、请求事件分页和一个统一 money checkpoint;
|
||
- 第二阶段:独立 request health、Activity、latency sketch/散点、identity rollup、Key/模型/Auth 构成、realtime cache;
|
||
- 数据量达到实测阈值后:冷归档、影子表重建、排名和高维分析。
|
||
|
||
所有阶段都保留完整底层事实;延后页面能力不等于丢弃数据。
|
||
|
||
## 8. 重建、修正和归档
|
||
|
||
- 所有聚合必须支持从 0 全量重建;
|
||
- 也可按聚合类型或时间范围重建;
|
||
- 重建写入影子表/新版本后原子切换,避免页面看到半成品;
|
||
- 聚合 schema/算法版本必须记录;
|
||
- 事实修正只能追加 revision/correction,不能原地篡改已入账历史;
|
||
- MVP 不物理归档 Request/Execution/Usage 最小事实;账本引用关系先保持完整;
|
||
- 后续若引入冷归档,所有依赖 checkpoint 必须追过归档上界,统一 view 仍能读取重建来源,不能只保留聚合后删除账本依据。
|
||
|
||
价格变化不触发历史金额重建。只有数据修复或追加账本会改变金额聚合。
|
||
|
||
## 9. 数据质量与诊断
|
||
|
||
统计服务必须公开:
|
||
|
||
- 各 checkpoint 当前值、目标值和 lag;
|
||
- 最后成功/错误时间与脱敏错误;
|
||
- measured、partial、unmeasured、inconsistent 数量;
|
||
- 未定价、迟到结算和冲正数量/金额;
|
||
- 近期缓存是否降级;
|
||
- 聚合版本与是否正在重建。
|
||
|
||
用户视图不展示内部 Token 或价格错误细节;用户只看到本次金额是否“待核算/已补记”。管理员可以查看完整质量原因。
|
||
|
||
## 10. 如何参考现有项目
|
||
|
||
`cpa-usage-keeper` 是本模块的主要实现参考:
|
||
|
||
| 主题 | 源码位置 |
|
||
| --- | --- |
|
||
| Overview 小时/日聚合纯函数 | `cpa-usage-keeper/internal/overview/aggregate.go` |
|
||
| Activity 多粒度边界与聚合 | `internal/activity/grain.go`、`aggregate.go` |
|
||
| 延迟 sketch 与稳定抽样 | `internal/latency/aggregate.go`、`sketch.go`、`sample.go` |
|
||
| 独立 checkpoint 和公平 runner | `internal/poller/usage_aggregation_runner.go` |
|
||
| 聚合实体 | `internal/entities/usage_*_stat.go`、`usage_aggregation_checkpoint.go` |
|
||
| 查询与右边界补偿 | `internal/repository/usage_overview_stats.go`、`usage_recent_event_cache.go` |
|
||
| Analysis 与事件查询 | `internal/repository/usage_analysis_projection.go`、`usage.go` |
|
||
| 服务层投影 | `internal/service/usage.go` |
|
||
| API 输出 | `internal/api/usage_overview.go`、`usage_analysis.go`、`usage_events.go` |
|
||
|
||
应该吸收:确定性聚合、独立 checkpoint、批量追赶、实时补偿、分位 sketch、有界样本、查询服务分层和丰富的测试。
|
||
|
||
不能直接照搬:
|
||
|
||
- 查询时依据当前价格重算历史费用;
|
||
- 把 Token 作为普通用户的核心展示单位;
|
||
- 所有分析功能一次性进入 MVP;
|
||
- 为每个 UI 组合增加写放大很高的索引或聚合维度。
|
||
|
||
`cpa-plugin-key-billing` 只用来核对账单输出字段和简单累计口径,不作为统计架构参考。其状态内累计和 30 天日志不能替代事实表、聚合表和 checkpoint。
|
||
|
||
## 11. 验收标准
|
||
|
||
- 同一事实集全量构建与任意批次增量构建结果一致;
|
||
- 重复通知、重复执行、崩溃恢复不会重复累计;
|
||
- MVP 统一 money checkpoint、后续各聚合 checkpoint 可以按阶段独立失败和恢复;
|
||
- 请求数、执行数和账单数不会混淆;
|
||
- 金额直接来自不可变账本,价格更新不会改变历史;
|
||
- 取消/失败的可靠 Usage 被计入金额,无 Usage 被计入 `unmeasured`;
|
||
- 迟到结算按原请求时间修正消费趋势,同时保留真实入账时间;
|
||
- 用户查询只能看到自己的金额投影;
|
||
- 管理员 Overview、Events、账本和余额在相同过滤范围内可核对;
|
||
- DST、空窗口、部分窗口、大范围查询和归档边界有测试;
|
||
- 百万级事实容量下,前台写入不会被聚合长事务持续阻塞。
|