[Bug]: Nullable DailyTagSpend conflict columns cause duplicate inserts and database CPU exhaustion

该报错通常出现在 LiteLLM 代理使用 PostgreSQL 存储每日标签消费(DailyTagSpend)数据时,由于数据库唯一约束中的可空列(如 custom_llm_provider)在 upsert 时写入 NULL,导致同一逻辑键产生大量重复物理行,最终引发数据库 CPU 耗尽和消费报

快速结论:该报错通常出现在 LiteLLM 代理使用 PostgreSQL 存储每日标签消费(DailyTagSpend)数据时,由于数据库唯一约束中的可空列(如 custom_llm_provider)在 upsert 时写入 NULL,导致同一逻辑键产生大量重复物理行,最终引发数据库 CPU 耗尽和消费报表碎片化。优先排查 LiteLLM 版本是否为 1.94.0 或更早版本,并确认 PostgreSQL 中 LiteLLM_DailyTagSpend 表的行数和索引大小是否异常膨胀。

适用环境:Issue 中已确认的环境为 LiteLLM 1.94.0(代理模式)、PostgreSQL 16、28 个代理 worker。未提供 Python、CUDA、显卡等硬件信息。

最快修复方案:Issue 评论中已确认该问题由 PR #36448 修复:批处理 upsert 将 NULL 冲突键规范化为空字符串。请升级 LiteLLM 至包含该修复的版本(≥ #36448 合并后的版本),并手动执行一次性去重清理已有重复行。

注意事项:代码修复只能阻止新的重复行产生,无法自动清理历史重复数据。已有 NULL 键的旧行仍需手动去重,否则表体积和 CPU 占用不会自动回落。升级前建议先备份数据库。

问题场景

LiteLLM 代理在 PostgreSQL 后台上运行每日消费统计刷新(daily-spend flush)时,LiteLLM_DailyTagSpend 表积累了海量物理重复行。每次刷新操作都会对同一组逻辑键(tag、date、api_key、model、custom_llm_provider、mcp_namespaced_tool_name、endpoint)插入多条记录,导致表大小和索引大小急剧膨胀,flush 写操作消耗了 PostgreSQL 的大部分 CPU。在一个匿名的生产部署中,该表达到了约 515 万行、3.47 GB(含索引),一天样本中发现了 1,647 条跨 6 个逻辑键的重复物理行,最大逻辑键组包含约 804 条物理行。

报错原文

[Bug]: Nullable DailyTagSpend conflict columns cause duplicate inserts and database CPU exhaustion

原因分析

根本原因有两层:

1. PostgreSQL 唯一约束对 NULL 的处理:Prisma schema 将 tagmodelcustom_llm_providermcp_namespaced_tool_nameendpoint 设为可空列,并将它们全部包含在一个普通复合唯一约束中。PostgreSQL 默认将 NULL 值视为互不相同,因此当任何冲突列为 NULL 时,唯一约束不会拒绝插入另一个全等行。

2. upsert 的 where 和 create 值不一致_update_daily_spend 函数在构造 upsert 时存在矛盾——where 选择器将缺失的 provider 转换为空字符串 "",而 create 载荷却保留 None(即 NULL)。因此选择器查找空字符串,但写入的是 NULL,下次 flush 无法选中该行,PostgreSQL 又不会因 NULL 拒绝重复插入,于是不断产生新行。

同一通用函数也写入 user、team、organization、end-user、agent、tag 六张每日消费表,它们的复合约束也包含可空列,理论上都存在相同问题,只是 DailyTagSpend 在运营中暴露得最明显。

环境排查

  • LiteLLM 版本:确认是否 ≤ 1.94.0 或未包含 PR #36448 修复
  • PostgreSQL 版本:确认是否为 16(或 PostgreSQL 12+ 以确认是否支持 NULLS NOT DISTINCT
  • 数据库连接方式:确认使用 Prisma Client
  • 表膨胀检查:查询 LiteLLM_DailyTagSpend 的行数、表大小和索引大小
  • 代理 worker 数量:确认是否有多 worker 并发触发 flush 调度器

解决步骤

  1. 升级 LiteLLM:升级到包含 PR #36448 的版本,该修复将批处理 upsert 中的 NULL 冲突键规范化为空字符串,使 wherecreate 使用一致的密钥表示。
  2. 备份数据库:在执行任何去重操作前,对 PostgreSQL 数据库进行完整备份。
  3. 手动去重清理:对已写入的 NULL 键行执行一次性去重。可以用 SQL 按逻辑键分组,保留每组一条记录(如 min(ctid)),删除其余重复行。逻辑键应包含 tagdateapi_keymodelcustom_llm_providermcp_namespaced_tool_nameendpoint 七个字段,注意将 NULL 视为空字符串参与分组。
  4. (可选)加固数据库约束:如果 PostgreSQL 版本支持(12+),可考虑将唯一约束改为 NULLS NOT DISTINCT,在数据库层强制 NULL 值也遵循唯一性。但此操作需要先完成去重,否则会因现有重复行导致约束创建失败。
  5. (可选)规范化冲突列:在应用层统一将 NULL 冲突列转换为空字符串后写入,使 wherecreate 的值永远一致。

验证方法

升级并去重后,可通过以下方式确认问题已解决:

  • 查询 LiteLLM_DailyTagSpend 表的行数增长趋势,确认不再出现持续膨胀。
  • 执行分组统计,确认同一逻辑键不再有超过一条的物理行。
  • 监控 PostgreSQL 的 CPU 占用和该表相关的查询执行频率,确认 daily-spend flush 不再主导数据库负载。

参考来源

BerriAI/litellm #34232

GamsGo AI

AI 工具推荐

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

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

了解 GamsGo AI

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

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 18865

发表回复

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