背景
从性能与日志审计报告,以及后续架构讨论来看,当前 dashboard / usage / billing 慢查询的根因不是“少一个缓存库”,而是数据模型和查询模型错位:
usage_logs 作为请求明细事实表,却直接承担在线 dashboard、账单、用户/API Key/组/模型 breakdown 聚合。
- 现有预聚合表覆盖面偏窄,很多带过滤条件的查询会回落到
usage_logs。
- 在线接口缺少查询成本边界:大范围
SUM/COUNT/GROUP BY、大表 COUNT(*)、offset pagination 等路径仍存在。
- Postgres 被同时当成 OLTP 明细库、报表库和部分日志平台使用,数据量增长后必然卡顿。
云厂商账单系统通常不会在用户打开账单页面时实时扫描原始 usage event 表,而是持续生成可查询的 billing / usage view:明细事实流进入 metering pipeline,再产生小时/日/月 rollup、line item、hot counter 和导出视图。在线页面查的是 rollup / materialized view / 列式分析库,而不是原始明细大表。
核心判断
这个问题本质上是:
表设计、查询设计、数据生命周期设计问题;统一缓存层只是在线保护层,不是根因修复。
缓存 issue(#7)仍然有价值,但它应该服务于新的查询架构:
bad:
cache miss -> scan usage_logs 20M rows
good:
cache miss -> query rollup table / materialized view / Redis hot counter
目标架构
建议把 usage / billing 查询链路拆成分层模型。
L0:明细事实表
usage_logs 定位为 append-only 明细事实表:
- 用于审计、排障、短期明细查询、异步聚合来源。
- 不直接服务 dashboard / billing / summary 的大范围聚合。
- 必须分区,按时间生命周期保留。
- 明细列表默认 keyset pagination,避免大表
COUNT(*) 和大 offset。
L1:小时/分钟 rollup
用于近实时 dashboard 和趋势:
usage_rollup_global_hourly
usage_rollup_user_hourly
usage_rollup_api_key_hourly
usage_rollup_group_hourly
usage_rollup_model_hourly
usage_rollup_account_hourly
usage_rollup_endpoint_hourly
字段至少包含:
bucket_start
- 对应维度 ID / key
request_count
input_tokens
output_tokens
cache_creation_tokens
cache_read_tokens
total_tokens
total_cost
actual_cost
error_count
L2:日 rollup
用于账单、用户列表、排名、长期趋势:
usage_rollup_user_daily
usage_rollup_api_key_daily
usage_rollup_group_daily
usage_rollup_model_daily
usage_rollup_account_daily
usage_rollup_endpoint_daily
典型查询应变为:
SELECT day, SUM(total_cost), SUM(input_tokens), SUM(output_tokens)
FROM usage_rollup_user_daily
WHERE user_id = $1
AND day >= $2
AND day < $3
GROUP BY day
ORDER BY day;
而不是:
SELECT date_trunc('day', created_at), SUM(total_cost)
FROM usage_logs
WHERE user_id = $1
AND created_at >= $2
GROUP BY 1;
L3:Redis hot counter
用于实时感更强但可最终一致的展示,例如:
- 今日用量
- 当前小时用量
- 最近 5 分钟请求量
- 热门模型/endpoint 粗略排行
示例 key:
usage:today:user:{id}
usage:hour:model:{model}
usage:minute:global
Redis hot counter 不是最终账单来源,只作为热展示和在线减压。
L4:统一缓存层
issue #7 中规划的统一缓存层用于缓存 rollup 查询结果和 API 响应,并提供:
- TTL / jitter
- singleflight 防击穿
- stale-while-revalidate
- namespace / version
- 主动失效
- metrics
查询规则
建议建立硬规则:
- dashboard / billing / summary / breakdown 默认禁止大范围扫描
usage_logs。
usage_logs 只允许用于短期明细、单请求排障、受限时间窗口查询。
- 所有在线统计接口必须走 rollup / materialized view / Redis counter / cache。
- 明细列表默认 keyset pagination,不默认执行全表
COUNT(*)。
- 重查询必须有时间窗口上限、超时、并发限制。
- 账单成本计算需要保留价格快照 / pricing version,避免历史成本被当前价格变更影响。
第一阶段建议范围
优先改造最重且用户感知最强的路径:
/api/v1/usage/dashboard/stats
/api/v1/usage/dashboard/trend
/api/v1/usage/dashboard/models
/api/v1/usage/dashboard/api-keys-usage
/api/v1/admin/dashboard/users-usage
/api/v1/admin/groups/usage-summary
/api/v1/dashboard/billing/usage
/api/v1/dashboard/billing/subscription
- 用户/API Key/账号/组/模型/endpoint breakdown
第一阶段不要求一次性覆盖所有维度,但需要建立可扩展模式:
- 先选 2-3 个热点接口落地 rollup 表 + 聚合 worker + 查询迁移。
- 保留旧路径作为短期 fallback,但加开关、超时和日志告警。
- 用统一缓存层缓存新 rollup 查询结果。
聚合链路建议
可以从简单可靠的增量 worker 开始:
- 以
usage_logs.id 或 created_at + id 作为 watermark。
- 周期性读取新增明细,按维度聚合 upsert 到 hourly/daily rollup。
- 聚合任务幂等,支持重放指定时间窗口。
- 修正/补账通过重算指定 bucket 实现。
- 高流量场景后续再升级到队列/流式 metering pipeline。
验收标准
- 有明确的 usage / billing rollup 表设计文档或 migration 草案。
- 至少一个 dashboard/billing 热点接口不再直接扫
usage_logs,改查 rollup。
- 新增聚合 worker,支持 watermark、幂等 upsert、重算指定时间窗口。
- 明细查询路径使用 keyset pagination 或明确限制 count/offset 行为。
- 对仍回落
usage_logs 的路径有日志告警和保护:时间窗口上限、超时、并发限制。
- 单元/集成测试覆盖:rollup upsert、重复聚合幂等、重算窗口、查询结果与明细聚合一致。
- 文档说明:哪些接口已迁移,哪些仍是 legacy fallback,下一阶段迁移计划。
风险与注意事项
- 不要设计一个“全维度超级 rollup 表”导致组合爆炸;应按实际查询模式拆多张 rollup 表。
- 账单正确性优先于实时性;当日数据可以标注为 estimated / delayed。
- Redis hot counter 不能作为最终账单事实来源。
- 价格、套餐、倍率、折扣等需要记录快照或 version,否则历史账单不可重放。
- rollup 迁移期间需要对比新旧查询结果,允许小范围灰度。
- 缓存 TTL 不是账单架构;缓存只能保护在线路径,不能替代数据模型改造。
关联
背景
从性能与日志审计报告,以及后续架构讨论来看,当前 dashboard / usage / billing 慢查询的根因不是“少一个缓存库”,而是数据模型和查询模型错位:
usage_logs作为请求明细事实表,却直接承担在线 dashboard、账单、用户/API Key/组/模型 breakdown 聚合。usage_logs。SUM/COUNT/GROUP BY、大表COUNT(*)、offset pagination 等路径仍存在。云厂商账单系统通常不会在用户打开账单页面时实时扫描原始 usage event 表,而是持续生成可查询的 billing / usage view:明细事实流进入 metering pipeline,再产生小时/日/月 rollup、line item、hot counter 和导出视图。在线页面查的是 rollup / materialized view / 列式分析库,而不是原始明细大表。
核心判断
这个问题本质上是:
缓存 issue(#7)仍然有价值,但它应该服务于新的查询架构:
目标架构
建议把 usage / billing 查询链路拆成分层模型。
L0:明细事实表
usage_logs定位为 append-only 明细事实表:COUNT(*)和大 offset。L1:小时/分钟 rollup
用于近实时 dashboard 和趋势:
usage_rollup_global_hourlyusage_rollup_user_hourlyusage_rollup_api_key_hourlyusage_rollup_group_hourlyusage_rollup_model_hourlyusage_rollup_account_hourlyusage_rollup_endpoint_hourly字段至少包含:
bucket_startrequest_countinput_tokensoutput_tokenscache_creation_tokenscache_read_tokenstotal_tokenstotal_costactual_costerror_countL2:日 rollup
用于账单、用户列表、排名、长期趋势:
usage_rollup_user_dailyusage_rollup_api_key_dailyusage_rollup_group_dailyusage_rollup_model_dailyusage_rollup_account_dailyusage_rollup_endpoint_daily典型查询应变为:
而不是:
L3:Redis hot counter
用于实时感更强但可最终一致的展示,例如:
示例 key:
Redis hot counter 不是最终账单来源,只作为热展示和在线减压。
L4:统一缓存层
issue #7 中规划的统一缓存层用于缓存 rollup 查询结果和 API 响应,并提供:
查询规则
建议建立硬规则:
usage_logs。usage_logs只允许用于短期明细、单请求排障、受限时间窗口查询。COUNT(*)。第一阶段建议范围
优先改造最重且用户感知最强的路径:
/api/v1/usage/dashboard/stats/api/v1/usage/dashboard/trend/api/v1/usage/dashboard/models/api/v1/usage/dashboard/api-keys-usage/api/v1/admin/dashboard/users-usage/api/v1/admin/groups/usage-summary/api/v1/dashboard/billing/usage/api/v1/dashboard/billing/subscription第一阶段不要求一次性覆盖所有维度,但需要建立可扩展模式:
聚合链路建议
可以从简单可靠的增量 worker 开始:
usage_logs.id或created_at + id作为 watermark。验收标准
usage_logs,改查 rollup。usage_logs的路径有日志告警和保护:时间窗口上限、超时、并发限制。风险与注意事项
关联
usage_logs明细表承担了报表/账单查询职责,rollup 维度不足,在线查询缺少成本边界。