向量数据库不只是存储成本:我们为什么不认同 Turbopuffer 的设计取舍

2026-03-06

By James Luan

向量数据库不只是存储成本:我们为什么不认同 Turbopuffer 的设计取舍

2023 年,DHH 在博客里写了一篇"我们为什么要离开云",震惊了整个工程师圈子。37signals 从 AWS 全部迁回自建服务器,每年节省超过 $2M。核心论点只有一句话:Renting computers is mostly a bad deal for medium-sized companies with stable growth.

这件事本质上不是在讨论云好不好。它揭示了一个更深层的认知陷阱:按需计费的成本,在规模增长之后往往比你预期的贵得多。

向量数据库领域现在正在重演同样的故事。

Serverless 的代价,从来不只是账单上那一行

Turbopuffer 是目前最被讨论的 serverless 向量数据库之一。Cursor 用它做代码索引,它的 pitch 很清晰:S3 存储,极低成本,按用量计费,冷热自动切换。

这套逻辑在小规模下完全成立。问题在于,serverless 的计费模型和你实际的资源消耗之间,隔着一层你最开始看不见的放大系数

我们用一个真实的代码助手场景测试了 Turbopuffer:1000 万向量,768 维,1000 个租户,均值 40 QPS,租户大小分布不均匀(和真实业务一样,不是均匀分布的)。官网计算器估算 $220/月。实际跑出来:$1000+/月,差了 10 倍。

原因在于 queried_bytes 的计费逻辑:不按实际查询触达的数据量算,而是按整个 namespace 的体积算。你查一个大租户,就付整个大 namespace 的钱,不管你 top-10 还是 top-100。租户越大,查询越贵,跟你实际做了多少计算无关。

这不是 Turbopuffer 独有的问题。Serverless 按请求计费的模型,在流量稳定、数据分布不均匀的生产场景下,几乎注定会超出预期。Cloudflare 为了解决类似的问题,专门重新设计了 Workers 的计费模型——只收 CPU 时间,不收 I/O 等待时间。他们承认,传统 serverless 计费"会为你不干活的时间收费"。

我们给这类隐性代价起了个名字:后果成本(Cost of Consequence)。它不出现在任何定价页面上。它出现在你有了第一个大客户之后的账单里,出现在你开始认真做生产压测的时候。

S3 可以用,但冷启动问题不能靠祈祷解决

Turbopuffer 的架构问题里,延迟是最难向用户解释的一个。

warm 状态下,Turbopuffer 的 p99 大约 30ms。这个数字不差。问题是你没有办法保证 namespace 一直是 warm 的。S3 GET 请求的 first-byte 延迟是 10–100ms,一个 namespace 的索引文件可能分散在多个对象里,冷启动要把这些文件全部拉下来、反序列化、重建索引,才能开始第一次查询。

我们测到的冷 p99:最高接近 4 秒

这不是 bug,这是 S3 的物理特性。没有人能在不改变存储架构的前提下把它优化掉。

Zilliz Cloud 的做法是:S3+ 本地 NVMe 磁盘缓存的两级存储架构。S3 作为持久存储层,活跃的索引文件缓存在计算节点的本地 NVMe 上。NVMe 的随机读延迟在 0.1ms 量级,比 S3 快 100–1000 倍。命中磁盘缓存的查询,延迟特性接近纯内存,而不是对象存储。

重要的是:缓存不是尽力而为的,是架构保证的。活跃 namespace 的缓存由系统主动管理,扩容不驱逐缓存,流量变化不导致意外的冷启动。用户不需要担心某个冷门时间段之后第一个查询会突然变慢。

TurbopufferZilliz Cloud
冷 p99~4s<100ms(磁盘缓存命中)
热 p99~30ms~20ms
冷启动可控性不可控磁盘缓存保证

写入延迟和数据可见性:S3 架构的另一面

冷启动是查询侧的问题。写入侧同样有一道坎,而且更容易被忽视,因为它不会直接报错。

S3 的写入路径和本地存储不同。一次向量写入需要经过序列化、上传 S3 对象、再触发索引更新,整个链路比写内存或本地磁盘多出一个数量级的延迟。更关键的是:写入成功不等于数据可被搜索到

我们在测试中已经观察到这个问题的极端表现——删除 namespace 一半的数据,API 秒回成功,但 result count 花了将近一个小时才恢复正常。写入也是同样的模式:数据写进去了,但索引更新是异步进行的,新写入的数据需要等待一段不确定的时间才会出现在搜索结果里。

这在某些场景下可以接受。如果你的业务是批量建库、然后只读查询,这个 tradeoff 是合理的。但如果你的场景是:

  • 用户刚上传了文档,立刻触发一次搜索
  • 数据流式写入,需要近实时可见
  • 删除敏感数据后需要立即确认不可检索

……这种写后可见(read-your-writes)的一致性保证,S3 based 架构在不引入额外缓冲层的前提下,很难直接提供。

Zilliz Cloud 采用的是 WAL+ Growing Segment 机制:写入先记录到预写日志,同时写入内存中的 growing segment,这部分数据对查询立即可见;后台异步完成 segment 封存和 S3 持久化。写入确认意味着数据真的可以被搜索到,不是"可能要等一会儿"。

SPFresh 索引:S3 架构强加给你的扩展性天花板

这一节讲的是一个更深层的约束,也是 Turbopuffer 目前架构里最难绕过的问题之一。

向量索引的选择,在很大程度上被存储层绑定了。HNSW 这类图索引性能极高,但它有一个前提:整张图必须常驻内存,随机访问图节点的延迟要在微秒级。当你的索引存在 S3 上,这个前提就消失了——S3 的随机读延迟比内存高几个数量级,图索引的遍历会变得无法接受。

这就是为什么 Turbopuffer 使用 SPFresh 索引。SPFresh 是专门为 SSD/对象存储场景设计的:它把向量空间分成若干 partition,查询时只需要加载少数几个 partition,减少随机 I/O 的次数。在存储受限的环境里,这是一个合理的工程选择。

但它有两个不可回避的代价。

第一:随着数据更新,召回率会逐渐下降。

SPFresh 的索引结构在构建时是针对当时的数据分布优化的。每次写入或删除,数据分布就偏离了索引的预期,导致搜索时遗漏的向量越来越多。召回率不是一个固定的数字,它会随时间悄悄降低,直到你做一次完整的索引重建。对于数据更新频繁的应用(RAG 知识库、实时推荐),这是一个持续的维护负担。

第二:IVF 类索引的QPS上限比图索引低一个数量级。

这是一个经常被 benchmark 忽略、但在生产中非常关键的数字。HNSW 在单节点上可以轻松达到数万 QPS,而 IVF/SPFresh 类索引受制于 partition 加载和扫描的成本,QPS 通常在数百到低千级别。

两者的差距大约是 10x

这意味着什么?如果你的应用从 100 QPS 增长到 1000 QPS,基于 HNSW 的系统只需要线性扩容;而基于 SPFresh 的系统需要扩容 10 倍以上的节点来保持同等延迟——每个节点的 QPS 上限就摆在那里,硬件砸不进去。计算成本和节点数量的增长都会比预期陡峭得多。

SPFresh(Turbopuffer)HNSW(Zilliz Cloud)
索引适配存储S3/磁盘内存(NVMe 缓存加速)
单节点 QPS 上限数百–低千数万
数据更新后的召回随时间下降,需重建增量更新,召回稳定
扩展到高 QPS 的成本节点数线性放大容量弹性扩展

Zilliz Cloud 用的是 HNSW + Cardinal 量化的组合。Cardinal 解决了 HNSW 内存占用的问题——同等 bit 宽度下更高的召回率意味着你可以用更激进的压缩,把更多向量塞进内存,同时不牺牲搜索质量。S3 + NVMe 缓存保证了常用索引常驻磁盘(延迟远低于 S3),内存放不下的部分从本地 NVMe 加载,而不是走对象存储。

数据写入时,新数据先进 growing segment 立即可查,后台 compaction 完成后封存为 sealed segment 并做 HNSW 索引建立。索引是增量的,不需要因为新数据就推倒重来。

过滤查询的召回率,是一道分水岭

这个问题比延迟更隐蔽,因为它不会触发报错,也不会让监控报警。它只是悄悄让你的产品答案变差。

向量搜索加过滤条件,在多租户、RAG、推荐系统里是标配需求。我们用 1000 条 query 测了不同精度的范围过滤,结果如下:

过滤条件Turbopuffer 召回率Zilliz Cloud 召回率
无过滤~0.95~0.99
保留 50% 数据0.780.99
保留 10% 数据0.690.99
保留 1% 数据0.540.99

过滤条件越精确,Turbopuffer 的召回率越低,最差跌到 0.54。并且没有任何参数可以调。把 top_k 拉到最大值 1200,系统只返回了 ~500 条结果——召回率和结果数量同时出问题。

问题的根源是架构设计:过滤作为 ANN 搜索的后处理步骤,过滤越精,候选集里符合条件的文档越少,召回就越低。这是一个结构性问题,不是参数调优能解决的。

Zilliz Cloud 在两个层面上解决了这个问题。

第一层:Cardinal 自研量化算法。传统向量量化在压缩比和召回率之间存在固定的 tradeoff——压缩越激进,向量表示的精度越低,召回就越差。Cardinal 在同等 bit 宽度下,通过更精确的量化方式,显著提高了压缩后的召回率。这意味着你可以用更少的内存存更多的向量,同时不牺牲搜索质量。

第二层:filter-aware 索引。我们对过滤查询做了深度优化,核心思路是在索引构建阶段就考虑过滤条件的分布,而不是把过滤当作事后的筛选步骤。结合动态路由策略,无论过滤的 cardinality 高还是低,召回率都能保持在 90% 以上。这是 Milvus 在这个问题上投入了相当工程量的地方,也是我们认为 Turbopuffer 在短期内难以追上的差距。

Dedicated 还是 Serverless?这是一道选择题,不是一道答案题

Turbopuffer 只有一种形态:serverless。这在某些场景下是优势,在另一些场景下是约束。

Zilliz Cloud 提供两种部署模式,不是为了让选择更复杂,而是因为这两种场景的需求确实不同:

Serverless:适合早期探索、流量不稳定、对 p99 延迟容忍度高的场景。零基础设施,按用量计费,快速上手。如果你的业务还在验证期,或者查询本来就是低频的,serverless 的低门槛是真实的价值。

Dedicated:适合生产部署、多租户、有 SLA 要求的场景。独享计算资源,p99 延迟可预期,成本随使用量线性增长,没有隐性的 namespace 体积放大系数。当你有了稳定的用户基数,dedicated 通常是更诚实的选择。

这两种模式背后用的是同一套索引和查询引擎,Cardinal 量化和 filter-aware 索引在两种模式下都可用。

评估向量数据库的另一张账单

回到开头的问题:为什么按需计费经常比预期贵?

因为我们评估基础设施的时候,习惯看定价页面上的单价,而不是使用方式和计费模型共同决定的实际账单。DHH 花了几年时间、$180k/月的 AWS 账单才想明白这件事。我们想让做向量数据库选型的团队早一点想清楚。

成本不只是钱的问题。它包括:

  • 冷启动导致的 p99 尖刺,以及解释不清楚的 oncall
  • 写入后数据不可见,以及用户刚上传文档却搜不到的 bug 报告
  • 索引随更新老化、召回率悄悄下降,以及 RAG 答案质量莫名变差的反馈
  • SPFresh 的 QPS 天花板,以及应用流行之后你发现需要扩容到 10x 节点的成本
  • 窄过滤下召回率下降,以及用户反馈答案质量变差
  • 最终一致性的窗口期,以及 result count 对不上的 debug 成本
  • 租户规模不均匀导致的账单暴涨,以及向 finance 解释为什么估算差了 10 倍

这些成本都是真实的,都发生在生产环境里,都不出现在任何产品的定价页面上。

维度TurbopufferZilliz Cloud
冷查询 p99~4s<100ms(NVMe 磁盘缓存)
热查询 p99~30ms~20ms
写入后可见性异步,延迟不确定立即(Growing Segment)
删除后一致性~1 小时收敛立即
索引类型SPFresh(S3 优化)HNSW(图索引)
单节点 QPS 上限数百–低千数万(10x+)
更新后召回率变化随时间下降增量维护,稳定
过滤下召回率(1% cardinality)0.540.99+
量化算法标准Cardinal 自研,同等 bit 下更高召回
部署形态仅 ServerlessServerless + Dedicated
大租户成本线性度非线性(namespace 体积放大)线性

最后说一句

我们并不认为 Turbopuffer 是一个坏产品。对于冷数据索引、低频查询、原型探索,它的架构选择是自洽的,定价也确实很低。但如果你在做一个面向用户的 AI 产品,用户的每一次搜索都在等待结果,有多租户过滤需求,需要在数据更新之后立刻看到准确结果——这些场景下,架构选择的后果会在生产环境里显现出来。

选向量数据库,不只是比 $/TB 的存储成本。


*本文测试数据来自内部 benchmark(1000 万向量,768 维,1000 租户场景)。完整方法论将单独发布。

文中引用:Why we're leaving the cloud — DHH,2023;New Workers pricing — Cloudflare,2023。*

  • James Luan

    James Luan

博客向量数据库不只是存储成本:我们为什么不认同 Turbopuffer 的设计取舍

AI Assistant