快速结论:该报错通常出现在 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 将 tag、model、custom_llm_provider、mcp_namespaced_tool_name、endpoint 设为可空列,并将它们全部包含在一个普通复合唯一约束中。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 调度器
解决步骤
- 升级 LiteLLM:升级到包含 PR #36448 的版本,该修复将批处理 upsert 中的 NULL 冲突键规范化为空字符串,使
where和create使用一致的密钥表示。 - 备份数据库:在执行任何去重操作前,对 PostgreSQL 数据库进行完整备份。
- 手动去重清理:对已写入的 NULL 键行执行一次性去重。可以用 SQL 按逻辑键分组,保留每组一条记录(如 min(ctid)),删除其余重复行。逻辑键应包含
tag、date、api_key、model、custom_llm_provider、mcp_namespaced_tool_name、endpoint七个字段,注意将 NULL 视为空字符串参与分组。 - (可选)加固数据库约束:如果 PostgreSQL 版本支持(12+),可考虑将唯一约束改为
NULLS NOT DISTINCT,在数据库层强制 NULL 值也遵循唯一性。但此操作需要先完成去重,否则会因现有重复行导致约束创建失败。 - (可选)规范化冲突列:在应用层统一将 NULL 冲突列转换为空字符串后写入,使
where与create的值永远一致。
验证方法
升级并去重后,可通过以下方式确认问题已解决:
- 查询
LiteLLM_DailyTagSpend表的行数增长趋势,确认不再出现持续膨胀。 - 执行分组统计,确认同一逻辑键不再有超过一条的物理行。
- 监控 PostgreSQL 的 CPU 占用和该表相关的查询执行频率,确认 daily-spend flush 不再主导数据库负载。
参考来源
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。
![[Bug]: DashScope tiered pricing uses graduated slices instead of the request-size tier](https://www.chat-gpts.plus/wp-content/uploads/2026/08/34729-a68deaea-768x403.jpg)
![[Bug]:amd mi308x gpu, vllm 0.27.0~0.27.1, rocm 7.2.3, Kimi-K2.7-Coder start fails:AssertionError: mla_gluon requires gfx950 (CDNA4), got gfx](https://www.chat-gpts.plus/wp-content/uploads/2026/08/51964-339b289b-768x403.jpg)
