Skip to content

perf: 重构 usage / billing 查询架构,避免在线扫明细表 #8

Description

@is7Qin

背景

从性能与日志审计报告,以及后续架构讨论来看,当前 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

查询规则

建议建立硬规则:

  1. dashboard / billing / summary / breakdown 默认禁止大范围扫描 usage_logs
  2. usage_logs 只允许用于短期明细、单请求排障、受限时间窗口查询。
  3. 所有在线统计接口必须走 rollup / materialized view / Redis counter / cache。
  4. 明细列表默认 keyset pagination,不默认执行全表 COUNT(*)
  5. 重查询必须有时间窗口上限、超时、并发限制。
  6. 账单成本计算需要保留价格快照 / 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 开始:

  1. usage_logs.idcreated_at + id 作为 watermark。
  2. 周期性读取新增明细,按维度聚合 upsert 到 hourly/daily rollup。
  3. 聚合任务幂等,支持重放指定时间窗口。
  4. 修正/补账通过重算指定 bucket 实现。
  5. 高流量场景后续再升级到队列/流式 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 不是账单架构;缓存只能保护在线路径,不能替代数据模型改造。

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    architectureArchitecture and system design changesenhancementNew feature or requestperformancePerformance, scalability, and database load improvements

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions