[Bug]: LiteLLM_SpendLogs has no (api_key, startTime) index — budget-window spend reseed seq-scans the table and can saturate the DB (P2028)

这个报错通常出现在 LiteLLM 代理实例在高流量下启动预算窗口计数器(Redis 冷启动、TTL 过期)时,LiteLLM_SpendLogs 表缺少 (api_key, startTime) 复合索引导致顺序扫描排空数据库 CPU,进而触发 Prisma P2028 事务超时。优先排查 Lit

快速结论:这个报错通常出现在 LiteLLM 代理实例在高流量下启动预算窗口计数器(Redis 冷启动、TTL 过期)时,LiteLLM_SpendLogs 表缺少 (api_key, startTime) 复合索引导致顺序扫描排空数据库 CPU,进而触发 Prisma P2028 事务超时。优先排查 LiteLLM_SpendLogs 表上是否存在覆盖索引(covering index),而不是单纯依赖默认的 startTime 单列索引。

适用环境:LiteLLM v1.93.0(litellm-database 镜像)、Postgres 16(AWS RDS,2 vCPU 实测受影响)、3 个代理实例、Redis 启用。Python/CUDA/显卡版本在 Issue 中未提及。

最快修复方案:暂无确认的一步修复方案。维护者在评论中表示“索引不是最终方案,会重构 spend 计算方式并引入 budget windows 表”,但该重构尚未落地。可优先尝试的缓解手段是:手动在 Postgres 上创建覆盖索引(见解决步骤),已在生产环境实测将查询从约 170 ms 降到 65 ms(dominant key 场景),selective key 从约 170 ms 降到亚毫秒级。

注意事项:Prisma 的 schema DSL 不支持 INCLUDE 列(覆盖索引),必须通过 litellm-proxy-extras 中的原生 SQL 迁移来创建,不能直接写进 schema.prisma。另外只有一个普通复合索引 (api_key, startTime) 是不够的——当某个 key 占表数据 ~97% 时,规划器会放弃索引改走顺序扫描,所以必须用覆盖索引把 spend 列也包含进去。此修复是缓解手段,不是 Issue 作者和 LiteLLM 维护者认可的最终架构方案。

问题场景

LiteLLM 代理实例(3 个)在正常流量下处理预算窗口(budget_duration)逻辑时,Redis 中的预算计数器冷启动(新 pod、TTL 过期或低流量期后恢复),触发 SpendCounterReseed.window_from_spend_logs() 在请求路径上执行回填查询。该查询在 LiteLLM_SpendLogs 表上按 api_keystartTime 聚合 spend,由于缺少合适索引,Postgres 对全表做顺序扫描,将 2 vCPU 的 RDS 实例 CPU 打满到 ~94%,导致并发的 spend 更新事务无法在 60 秒超时内启动,最终返回 Prisma P2028 错误。

报错原文

DB read/write call failed: 504: {"is_panic":false,"message":"Transaction API error: Unable to start a transaction in the given time.","meta":{"error":"Unable to start a transaction in the given time."},"error_code":"P2028"}
[Non-Blocking]LiteLLM Prisma Client Exception - update spend logs: 504 ...
  File "litellm/proxy/db/db_spend_update_writer.py", line 1136, in _commit_spend_updates_to_db
    async with prisma_client.db.tx(timeout=timedelta(seconds=60)) as transaction:

-- 压垮 DB 的 tokenized 查询(RDS Performance Insights 定位):
SELECT "api_key", SUM("spend")
FROM "LiteLLM_SpendLogs"
WHERE "api_key" = $1 AND "startTime" >= $2
GROUP BY "api_key"

原因分析

触发查询本身来自 litellm/proxy/db/spend_counter_reseed.pySpendCounterReseed.window_from_spend_logs(),经由 proxy_server.py_authoritative_floor_spend() 调用,运行在请求路径上。schema.prisma 中 LiteLLM_SpendLogs 只有 startTime(startTime, request_id)end_usersession_id 四个索引,没有 api_key 索引(team_id 也没有)。当窗口起点 $2 覆盖表的大部分数据时(如日/周窗口),startTime 索引选择性不足,Postgres 退化为顺序扫描逐行过滤 api_key,成本随表大小线性增长。多个 pod 同时冷启动时形成 reseed 风暴,进一步放大 DB 压力。

后续生产实测补充了一个关键细节:即便加了普通复合索引 (api_key, startTime),对于占表 ~97% 行数的 dominant key(单 key 约 353,554 / 356k 行),规划器仍会放弃索引走并行顺序扫描,因为通过索引取 97% 的表数据比顺序扫描更慢。只有覆盖索引(INCLUDE ("spend"))才能同时解决 selective key 和 dominant key 两种情况:聚合可以完全从索引计算(index-only scan),无需回表拉取宽行(messagesresponsemetadata)。

环境排查

  • LiteLLM 版本:确认是否 v1.93.0 或相近版本(litellm-database 镜像)。
  • Postgres 版本:确认是否为 16(AWS RDS)或兼容版本。
  • 实例规格:确认 DB 实例 vCPU 数(Issue 实测为 2 vCPU,CPU 饱和是 P2028 的直接诱因)。
  • 代理实例数:确认是否为多实例部署(Issue 为 3 个),多实例同时冷启动会放大 reseed 风暴。
  • Redis:确认已启用,且预算窗口计数器由 Redis 管理。
  • 表数据分布:查询 LiteLLM_SpendLogs 中各 api_key 的行数占比,判断是否存在 dominant key(Issue 中一个 key 占 97%)。
  • 确认当前 LiteLLM_SpendLogs 上已有的索引列表,排除是否已存在 api_key(api_key, startTime) 索引。

解决步骤

  1. 第一步:在 Postgres 上创建覆盖索引(可优先尝试)。直接在 RDS 上执行(CONCURRENTLY 避免锁表阻塞写入):
    CREATE INDEX CONCURRENTLY "LiteLLM_SpendLogs_api_key_startTime_spend_idx"
    ON "LiteLLM_SpendLogs" ("api_key", "startTime") INCLUDE ("spend");

    如果也用到 Team 维度的预算窗口,可同步创建:

    CREATE INDEX CONCURRENTLY "LiteLLM_SpendLogs_team_id_startTime_spend_idx"
    ON "LiteLLM_SpendLogs" ("team_id", "startTime") INCLUDE ("spend");
  2. 第二步:验证执行计划。EXPLAIN ANALYZE 跑 Issue 中的查询,确认是否命中 Index Only Scan 且 Heap Fetches 很少(Issue 实测为 17)。
  3. 第三步:跟进官方 schema 变更。由于 Prisma schema DSL 不支持 INCLUDE 列,需要在 litellm-proxy-extras 中通过原生 SQL 迁移来维护该索引。注意:schema.prisma 里加 @@index([api_key, startTime]) 只能生成普通复合索引,对 dominant key 无效(规划器会忽略)。
  4. 第四步:关注上游重构。LiteLLM 维护者已表示将重构 spend 计算方式并引入专门的 budget windows 表,此索引是临时缓解手段,后续应跟进版本更新。

验证方法

创建索引后,用 EXPLAIN ANALYZE 复现 Issue 中的聚合查询,确认执行计划从 Parallel Seq Scan(~170 ms)变为 Parallel Index Only Scan(~65 ms,selective key 为亚毫秒级)。同时观察 RDS CloudWatch 的 CPU 利用率是否回落到正常水位,确认 P2028 报错不再出现在 Slack 告警或 db_exceptions 日志中。建议在低峰期操作,并在创建索引后执行一次 VACUUM (ANALYZE) 更新统计信息(Issue 实测即在此操作后测量)。

参考来源

BerriAI/litellm #35766: [Bug]: LiteLLM_SpendLogs has no (api_key, startTime) index — budget-window spend reseed seq-scans the table and can saturate the DB (P2028)

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 21228

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注