周三下午三点,我正在给 Spanlens 接一个新的模型供应商,仪表盘突然卡了十几秒才刷出曲线。打开 Supabase 控制台一看,那张存着所有大模型调用记录的 requests 表,已经膨胀到让每次时间范围查询都在啃磁盘。这不是未来某天的问题,而是下一秒就可能崩给我的样子。
当时 Spanlens 还是一个小体量的开源可观测性工具,每一条发给 OpenAI、Anthropic 或 Gemini 的请求都记下了模型名、耗时、消耗的令牌数、成本,还有完整的请求与响应体。流量不大时,Supabase 那套基于 Postgres 的架构跑得很舒服。但稍微把规模往后推演,就能看见三个清晰的裂缝:所有请求都往这一张表里灌,其他表的上限由组织或项目数量兜着,只有 requests 表会跟着 API 调用量无休止增长;仪表盘的主查询模式就是对 created_at 做日期范围扫描,而 Postgres 没办法有效压缩 JSON 请求体和响应体,数据越多扫描越慢;每天的备份体积直接跟表体积挂钩,背着巨大的 JSON 做备份,迟早要把运维拖垮。那些如今在毫秒内跑完的聚合查询,像“最近 7 天最烧钱的提示词”,只要规模再上一个台阶,马上就会现原形。
我没有等到真正起火的那一刻。评估了几个选项:TimescaleDB 离 Postgres 太近,存储成本的问题几乎原样保留。DuckDB 做嵌入式分析很好,但在我的场景里要的是一个托管的服务,不想自己扛运维。BigQuery 的计费模型让我对成本预期没底。最后选了 ClickHouse Cloud 的开发者套餐,起步便宜,列式压缩处理起 JSON 这种大对象又格外高效。更深层的原因还是负载的形状——几乎是纯写入、追加不改,读全是带一两个等值过滤(组织标识和模型名)的时间范围扫描。这正好长在 ClickHouse 的甜区上。
新的架构是这样铺开的:Postgres 继续管所有关系型数据,组织、项目、成员、密钥、提示词、告警、账单,这些该有的策略和权限控制都留在原地。ClickHouse 只吃一张会不断长大的 requests 表。写入路径上,Web 或服务端直接发一个 fire-and-forget 的插入到 ClickHouse。一旦写入失败,就兜底落到 Supabase 里一张叫 requests_fallback 的耐久队列,然后每分钟一次的定时任务再把那些积压的数据回放到 ClickHouse。这本质上是一个小型发件箱模式,让我在踩坑的时候一条日志都不会丢。
迁移之后,我撞上了五个一开始没想到的坑,正是这篇文章想写给别人听的。第一个坑是日期范围扫描跟 JSON 列的组合。原来在 Postgres 里只是慢,到了 ClickHouse 如果照搬查询写法,依然可能扫很大范围,但利用分区和适当地物化部分字段,就能把延迟压到很低。第二个坑是备份与恢复的思维转换。Postgres 一张大表备份又慢又占空间,ClickHouse 的列存和压缩让备份体积降到之前几分之一,不过恢复时要重新组装数据管道,不是 pg_dump 那种一键还原。第三个坑是聚合查询的响应时间突然变短之后,反而暴露了前端渲染和缓存策略的薄弱,用户一下子看到秒级出报表,会不停刷新。第四个坑就出在 fallback 设计本身,当回放任务和正常写入叠在一起时,需要小心里特试顺序和组织级隔离,不然可能把不同租户的数据混串。最后一个坑跟保留策略有关:为了不给免费版用户永无止境地存日志,查询时统一注入一个基于计划级别的保留天数过滤,免费团队 14 天、专业版 90 天、团队版一年。结果有一次调试时忘了关掉忽略保留期的参数,硬是把所有历史都扫了一遍,瞬间账单就往上蹿。
现在回头看,那次提前的迁移救了自己一把。Spanlens 依然开源在 GitHub 上,MIT 协议,代码里能看到这套写路径加兜底、读路径加作用域过滤的完整实现。我想传达的经验其实很简单:不要等大模型调用日志把关系库压垮才去动。找对列式引擎,留好故障兜底的队列,然后在每一个读取的地方卡紧租户隔离和保留时间,这三件事做到了,那些因 JSON 膨胀而生出来的备份焦虑、查询停滞和成本失控,就不会在凌晨三点把你叫醒。
热门跟贴