⚠️ 此页面为自动翻译,翻译可能不完美。
blog-post

在几台便宜的 VPS 上对比 Turbopuffer 和 Manticore Search

author image

无服务器向量数据库通常只卖一个简单承诺:你不用运维任何东西,不用调任何参数,存储、扩容和可用性都由别人操心。turbopuffer 就是这一类产品里比较典型的一个:它是构建在对象存储之上的快速搜索引擎,被 Cursor、Notion、Linear 等产品使用。

这个承诺是真的,但并不是免费的。所以一个显而易见的问题是:对于一个小而明确的工作负载,你到底真的需要其中多少能力?把同样的工作负载放到两台便宜的 VPS 上运行 Manticore Search,成本和性能又会怎样?

这篇文章用同一份数据集上的一组一比一基准测试数字来回答这个问题。

负载

我们选取了一个具体、适中的负载,也就是很多真实应用实际会遇到的那种,并据此为 turbopuffer 设定规模:

参数
数据集DBpedia (OpenAI embeddings)
存储文档数975k
向量维度1536(cosine similarity)
turbopuffer 延迟 P50 / P90 / P99(宣称值,warm namespace)14 / 17 / 27 ms
turbopuffer 估算成本$75 / month(2026 年 7 月)

表里的 turbopuffer 两个数字分别是它宣称的延迟和计算器估算的成本,不是实测值。成本来自 turbopuffer 的 cost calculator ,延迟则是 turbopuffer 公布的目标值:具体来说,是它自己基准测试中 warm namespace 的 P50/P90/P99:

turbopuffer 宣称的向量搜索延迟:warm namespace 为 14/17/27 ms,cold namespace 为 874/1214/1686 ms

这张截图直接给出了两个注意事项。第一,这 14/17/27 ms 是 warm namespace 的结果;同一基准测试对 cold namespace 报告的是 874 / 1214 / 1686 ms。一个已经从缓存中失活的 namespace,第一次查询会慢约 50–60×,这是 turbopuffer 对象存储分层的直接结果。第二,turbopuffer 的基准测试使用的是不同的负载(1024 维、1000 万文档),而不是我们这里的负载(1536 维、97.5 万文档)。turbopuffer 在我们这份数据集上的实际实测延迟和吞吐会在后文给出,并且和这些宣称值不同。我们的基准数据集是 97.5 万文档(DBpedia);两套系统加载的是同一份数据。

这个 $75/月 是直接来自 turbopuffer 自己的成本计算器,针对的是 1536 维向量、没有属性(计算器里的 "Attributes: None")。这和我们的基准测试完全一致:纯向量搜索,没有存储属性,也没有任何元数据过滤。两套引擎都支持属性过滤和全文搜索(Manticore 还额外支持 hybrid search );这里只是没有使用这些能力,所以比较的是纯 KNN 的同类对比。它的计费是按使用量计算的,拆分出来的细项才更有意思:

组件费率本次负载成本
存储≤ $0.33 / GB100 万文档(约 3 GB)$1.01
写入≤ $2.00 / GB1000 万次写入,约 4 WPS(约 61 GB)$61.44
查询≤ $1.00 / PB1000 万次查询,约 4 QPS(约 13 PB)$12.80
命名空间n/a100 × 1 万文档已包含
合计$75 / month(最低 $16 的 Launch 计划)

针对 1536 维、无属性向量的 turbopuffer 成本计算器:$75/月,主要成本来自写入

最醒目的数字几乎全部来自写入:$75 里的 $61.44 是摄入约 61 GB 向量数据的成本,而存储(3 GB)几乎可以忽略。

月度成本拆分:turbopuffer 为 $75(主要是写入),而 Manticore 是固定 $16.50

查询那一行里看起来很吓人的“约 13 PB”,其实是计费抽象,不是带宽。turbopuffer 按“已查询 GB 数”计费,而且每次查询的最低可计费量是 1.28 GB,所以计算器只是简单算出 10M queries * 1.28 GB ≈ 13 PB。实际上每次查询传输的数据非常少(一个 1536 维 float32 查询向量输入大约 6 KB,返回 top-10 结果也只有几 KB),但在这么小的 namespace 上,每次查询都会按 1.28 GB 的下限计费。按 ≤$1/PB 计算,整整 13 PB 也只要大约 $13,所以这里查询费用几乎可以忽略。

这对比较很重要,因为在自建引擎里,这些计费项根本不存在:写入只是 CPU 时间,查询没有按次最低计费,也没有按 GB 收费。(turbopuffer pricing , pricing changelog

参赛者

turbopuffer 对用户来说是无服务器的:存储放在 S3/GCS/Azure 对象存储上(约 $0.02/GB),热数据通过 NVMe 和 RAM 缓存分层处理。你不用预置或打补丁服务器,不用做复制管理,也不用做容量规划。你按存储 / 写入 / 查询的 GB 数付费,而查询计算跑在 turbopuffer 的托管基础设施上,而不是你自己提供的硬件上。

Manticore Search 是你自己运行的软件。为了这次对比,我们用了仍然具备冗余能力的最低成本现实方案:

参数
节点2 × Hetzner CX23(区域 "eu-central"),组成一个集群
每台 VPS 成本$8.25 / month
每台 VPS CPU2 cores
每台 VPS 磁盘40 GB(表数据约占 6 GB)
第二个节点的角色用作冗余副本
Manticore 总成本$16.50 / month(如果不需要冗余,则单节点为 $8.25)

Manticore 将向量存储在 HNSW 索引中(通过 Manticore Columnar Library ),并支持标量量化(8-bit 和 1-bit),可通过 oversampling 和 rescoring 在内存与速度和召回率之间做权衡。为了这次测试,我们使用了 1-bit(二值)量化,配合 oversampling=2.0 和 rescoring,参数就是专门选到和 turbopuffer 大致相同的召回率,因此延迟和吞吐是在相同质量水平上比较的

先看结论

turbopufferManticore(2×便宜 VPS)
月成本$75$16.50(高可用模式)/ $8.25(单节点)
recall@10(5k queries)0.96220.9663
查询延迟 P50 / P90 / P99(端到端)21 / 24 / 30 ms(含网络往返)10 / 13 / 16 ms(本地,无网络跳数,已完全压缩)
读取(查询)吞吐上限约 300 QPS,未批处理(concurrency=8);约 800–1,100 QPS,启用 query batching约 330 QPS(concurrency=2,未批处理)
持续写入吞吐约 1150 docs/s(bulk load)490-1100 docs/s(bulk load)
运维负担没有(完全托管)你自己运维和维护

在这个负载上,两台 $8.25 的 VPS 上运行 Manticore 的成本约低 4.5×,召回率与 turbopuffer 持平,而且在表被压缩后(稳定态)它实际上端到端更快(在同一区域约 10–13 ms,而 turbopuffer 约 21 ms),因为本地查询不需要支付 turbopuffer 永远要付的网络往返。反过来,turbopuffer 的查询吞吐上限更高,但前提是启用 query batching(约 800–1,100 QPS,而两台 2-core VPS 约 330 QPS);如果不批处理、且只从一台机器发起,两者其实差不多(都约 300 QPS)。对于 4 QPS 的负载来说,不管怎样两者都绰绰有余。你用 Manticore 换掉的是“零运维”这一部分,而这正是本文其余部分要讨论的核心。

质量:匹配的召回率

如果两套系统返回的结果质量不接近,就没必要比较延迟或成本。使用 DBpedia 数据集(1536 维 OpenAI embeddings、cosine similarity、97.5 万文档)和 5000 个查询:

系统recall@10
turbopuffer0.9622
Manticore0.9663

两者相差大约 0.4 个百分点,足够接近,后面的比较就是公平的。Manticore 通过 binary quantization + oversampling=2.0 + rescoring 达到了这个结果:二值向量表示比 float32 小 32×,而 rescoring 会在最终候选上用全精度向量重新计算距离。

延迟

真正重要的数字是你实际观察到的那个:从发起查询到拿到答案的端到端延迟。turbopuffer 是远程服务,所以这其中包括到 namespace 所在区域的网络往返。我们在与该 namespace 同一区域的机器上测量(gcp-europe-west3,与查询主机同国家):

turbopuffer (in-region, end-to-end):   P50=21ms   P90=24ms   P99=30ms

如果你的应用离这个区域更远,这个数会随着距离增加而上升(跨洲轻松超过 100 ms),所以让两者同地部署很重要,而且你总要至少支付一次同区域往返延迟。

turbopuffer 还报告了两个自己的内部计时器:它实际花在搜索上的时间(约 12 ms,即使在高负载下也很稳定),以及它一侧的总耗时,包括排队等执行槽位的时间。我们只用这些数据来解释下面的吞吐结果(引擎本身在高负载下依然很快),而不把它们当作延迟数,因为任何等待时间都是真实的延迟成本。还有一点要保持比较诚实:那个约 12 ms 是 turbopuffer 自己报告的、只算引擎内部的时间,不包含它内部排队。Manticore 也有 query profiling,但我们没有抽取出一个与之等价的单独引擎时间;这里所有 Manticore 数字都是端到端的,并且已经包含了它自身可能发生的任何内部排队。因此在本节里,我们始终做端到端对端到端的比较,而不是拿 turbopuffer 的引擎时间去对比 Manticore 的总时间。

Manticore 的延迟(本地查询,因此端到端且网络开销可忽略)在 4 QPS 查询 + 4 WPS 写入的混合负载下(10 分钟运行)高度取决于表被压缩成了多少个磁盘 chunk。一个刚批量写入完成的实时表会被拆成很多 chunk;Manticore 的 auto-optimize 会随着时间推移把它们合并,而每次 KNN 查询都必须访问每个 chunk,所以 chunk 越少,查询越快:

随着表不断压缩,Manticore 查询延迟下降,并在约 2 个 chunk 时低于 turbopuffer 的同区域区间

上面的 32 / 16 / 8 / 4 chunk 状态是刻意通过把表优化到固定 chunk 数来制造出来的,用于描绘延迟曲线。真实的批量加载最终会落在更少的 chunk 上:在本次基准的 batch=1000 条件下,表最终稳定在 7 个 chunk(1 个 insert thread)或 8 个 chunk(2 个线程),所以新加载完成的表大致处在 8 chunk 这个位置(约 28 ms)。最终结果其实是两个过程在赛跑:insert 会把 RAM chunk 刷到磁盘,而 auto-optimize 会在后台合并这些磁盘 chunk。摄入越快,新磁盘 chunk 堆积得越快,所以高吞吐加载(batch=1000)会超过后台合并器的速度,留下更多 chunk(7–8 个);而慢速的单文档插入(batch=1)会以足够慢的速度产生 chunk,使 auto-optimize 能跟上,表最终只剩 3 个 chunk,但此时摄入速率要低得多。也就是说,真正驱动最终 chunk 数的是摄入速率,而不是并发度本身。无论加载结束时留下什么状态,Manticore 的 auto-optimize 都会继续向 2 个 chunk(约 13 ms 平均)方向压缩。

结论:chunk 数是影响延迟的单一最大杠杆。32 个 chunk 时,Manticore 比 turbopuffer 的约 21 ms 端到端慢了好几倍。但当它通过默认下限自动优化到 2 个 chunk(约 12–13 ms)时,它的端到端已经比 turbopuffer 更快;压缩到单个 chunk(约 10 ms)后还会更快,而且全部都是在 $8.25 的硬件上实现的。原因主要是结构性的,而不是引擎速度本身的差异:两者的核心搜索大致都在相同的约 10–12 ms 内完成,但 Manticore 跑在你自己的 VPS 上,可以通过 localhost/LAN 查询,因此不需要支付网络往返,而 turbopuffer 至少总要付出约 9 ms 的同区域跳转成本。端到端差异基本上就是这段网络跳数。

读取(查询)延迟,而不是写入延迟,才反映表压缩得有多好,而保持压缩状态是 Manticore 的工作,不是你的。auto-optimize 在后台运行,并把 chunk 合并到一个它自动维护的目标(我们的表最终稳定在 2 个 chunk)。这个目标是一个配置项,不是额外的手工活:optimize_cutoff 选项(可通过 CREATE/ALTER 按表设置,或在 searchd 中全局设置)控制它,所以如果你想要最好的延迟,可以把 optimize_cutoff=1,后台优化器就会帮你把表维持在单个 chunk,稳态下不需要任何手工操作。真正要注意的只有时机:在一次大规模批量加载刚结束时,表仍然有很多 chunk,查询会慢一些,直到 auto-optimize 追上来。按我们这次运行的数据,concurrency=2 加载结束后还剩 8 个 chunk,后台优化器花了大约 15 分钟把它压到 2 个 chunk,期间查询延迟持续改善。turbopuffer 没有任何对应的步骤,这也是你为它支付溢价的一部分。

吞吐

因为每次查询都要消耗约 12 ms 的引擎时间,再加一次网络往返,单连接能够达到的吞吐大约受 1 / (engine + round-trip) 限制,所以提升吞吐的办法是并发发起查询,而下面的同区域数据就是这样测出来的。

turbopuffer,同区域,普通单查询(不批处理)。增加并发连接会提高吞吐,但只到某个点为止:

未批处理吞吐随并发变化:两套引擎都先达到峰值然后下降(turbopuffer 在 8 个连接时约 296 QPS,Manticore 在 2 个连接时 329)

未批处理吞吐大约在 8 个连接时达到峰值(约 296 QPS),随后下降(16 个连接时 233 QPS,32 个连接时 153 QPS),而端到端延迟则急剧上升(P90 从 33 → 108 → 423 ms,对应 8 → 16 → 32 个连接)。在这一切过程中,turbopuffer 自己报告的服务端时间始终稳定在约 12 ms。因为这个报告值没有变化,而端到端延迟却在上升,说明额外延迟没有被 turbopuffer 自己的计时器捕捉到:瓶颈可能在我们的机器、传输路径,或者在这些计时器之外的某些请求处理环节,而这次基准无法区分它们。它能说明的是,仅仅把未批处理并发提高到 8 以上,并没有让这台机器的吞吐继续提升。

turbopuffer 延迟随并发变化:端到端延迟在约 8 个连接后上升,而 turbopuffer 自报的引擎时间仍稳定在约 12 ms

因此,我们把约 300 QPS 视为从这台单机观察到的未批处理上限,而不是 turbopuffer 的官方服务限制。(如果换更多机器能否更高,取决于到底是哪一段在饱和,是单机资源还是共享路径,这次我们没有测试。)同一台机器把吞吐明显推过 300 的杠杆是 query batching。

query batching 把多次查询打包进一个请求里,从而摊薄网络往返成本,并把吞吐推得高得多,而且质量服务也呈现出清晰梯度:

  • 清爽地达到约 800 QPS:concurrency=4 / batch=8 时稳定在 821 QPS,服务端总时间 P50=15 / P90=20 / P99=29 ms。
  • 约 1,000–1,100 QPS,但尾延迟更高:concurrency=8 / batch=8 达到 1,015 QPS,但 P99 升到 58 ms;concurrency=4 / batch=16 达到 1,105 QPS,P99=61 ms。
  • 约 1,170 QPS,已经过载:concurrency=8 / batch=16 提供了 1,169 QPS,但每个请求的总耗时膨胀了(P99=455 ms,平均约 70 ms),尽管 turbopuffer 报告的服务端搜索时间仍只有约 12 ms,也就是说请求在服务器上排队等待了数百毫秒。这不是一个可持续的运行点。

所以,我们在这份数据集上观察到的实用批处理上限大约是:干净稳定地约 800 QPS,尾延迟升高时约 1,100 QPS,无论哪种情况都明显高于两台 VPS。

不过,关于 batching 还有三个注意事项:

  • 只有在一次会同时到达很多独立查询时才有用,这样才能合并成一个请求:高流量搜索后端可以,但典型的“一个用户,一个查询”路径做不到,所以对很多真实应用来说,相关数字是上面的非批处理上限(约 300 QPS),而不是约 1,000+ 的标题数字。
  • 批处理模式下每个查询的延迟是摊销值,不是墙钟时间。 一个必须等待批次凑满的查询,会经历批次形成延迟再加上自身请求延迟,因此最终用户感受到的延迟高于摊销后的数字。
  • 它把复杂性推给了你的应用。 你得收集查询,短暂等待它们凑成批次,把合并后的响应再分发给正确的调用方,还要处理部分失败,这通常麻烦到很多团队干脆就不做。

Manticore,在两台 2-core VPS 上,初始加载之后,在同时处理 4 WPS 写入的情况下提供查询服务(本地查询,因此没有网络往返):

并发连接数每秒查询数
1154
2329
4287

Manticore 在 2 个连接时大约达到 329 QPS 的峰值;再往上就已经把 2 核超额订阅了,所以 4 个连接会稍慢一些。不同于 turbopuffer 未批处理时的下降,这里是一个硬上限:它就是在 2 核上的引擎,想再往上只能加更多 CPU 或更多节点,而这就要更多钱。

这些 Manticore 数字都是未批处理、普通的一次一请求,和上面 turbopuffer 的未批处理扫描形状一样。批处理不会像对 turbopuffer 那样抬高 Manticore 的上限:批处理之所以主要能帮助 turbopuffer,是因为它能把约 9 ms 的网络往返摊到很多查询上;而本地 Manticore 查询没有这段往返可供摊销,这里的真正限制是 2 核 CPU,batching 并不能缓解。所以公平的一一对比仍然是未批处理对未批处理,在那里两者结果很接近:turbopuffer 约 296 QPS vs Manticore 约 329。不同的是,这个数字各自意味着什么:Manticore 的约 329 是 2 核的硬上限,而 turbopuffer 的约 296 只是我们这台单机跑到的峰值。再加并发只会更糟,而且我们无法定位原因(turbopuffer 自报的引擎时间保持不变,但那只是它自己的数字,不是本次基准能验证的东西)。所以在我们的设置里,约 300 就是实用的未批处理极限,而在 turbopuffer 上突破它的已验证路径是 query batching(同一台机器可达到约 800–1,100 QPS);在 Manticore 上则需要更多硬件。

所以在查询吞吐上,turbopuffer 的批处理上限(约 800–1,100 QPS,跑在它的托管计算上)高于两台 2-core VPS 的约 330 QPS而未批处理时两者差不多,并且 turbopuffer 的查询计费并不贵(整个负载的查询账单才约 $13)。Manticore 通过加硬件来扩容;turbopuffer 则在你不用管理的基础设施上扩容。Manticore 这一侧唯一值得注意的细节是:在低并发下,Manticore 其实单连接更快(1 个连接 154 QPS,而 turbopuffer 只有 40),因为本地查询不需要支付约 9 ms 的网络往返。

但对这个负载来说,吞吐这个维度其实几乎无关紧要。目标只是 4 QPS,而两套系统都远远超过:turbopuffer 在干净延迟下有约 200× 的余量,Manticore 也有约 80×。只有当你预计会持续出现每秒几百次查询时,吞吐才会变成决定因素;这时 turbopuffer 更高的批处理上限是更稳妥的选择,而 Manticore 就需要更大(也更贵)的集群。

写入侧两者的表现更接近:

  • turbopuffer bulk load:97.5 万文档,约 1150 docs/s。这是我们观察到的端到端速率;到底是什么在限制它(摄入路径、网络,还是它自己的索引过程),仅从外部看不出来。
  • Manticore bulk load(batch=1000):在第一个磁盘 chunk 刷出之前,速率为 750–1100 docs/s。这里瓶颈是可见的:2 核上的 HNSW 图构建,因为增加 insert worker 几乎没有帮助(1 → 2 个线程时从 451 提升到 490 docs/s)。

实际摄入

将完整的 DBpedia 集合加载到 Manticore 中(97.5 万文档、1536 维、构建 HNSW 图)花了:

并发Batch速率墙钟时间
11365 docs/s约 45 分钟
11000451 docs/s约 36 分钟
21000490 docs/s约 33 分钟

作为对比,turbopuffer 用约 1150 docs/s 加载了同样的数据,大约比这两台便宜 VPS 上的 Manticore 快 2.3 倍。这是符合预期的:turbopuffer 在自己更大、托管的计算资源上构建索引,而 Manticore 是在 2 个便宜 CPU 核上做 HNSW 图构建。所以 turbopuffer 在纯摄入速度上明显占优,不过对于一次性的初始加载来说,差别也就是大约 14 分钟对 33–36 分钟,不管怎样都是一次性成本。

在 batch=1000 的初始加载后,Manticore 表处于 7 个磁盘 chunk(1 个 insert thread)或 8 个 chunk(2 个线程);在 2-core VPS 上增加 insert worker 帮助不大。最终 chunk 数主要由摄入速率驱动,而不是并发驱动:insert 会把磁盘 chunk 刷出,而 auto-optimize 在后台合并它们,所以加载越快,后台合并器被甩开的 chunk 就越多。快速的 batch=1000 加载会留下 7–8 个 chunk;慢速的单文档插入(batch=1)会以足够温和的速度产生 chunk,使 auto-optimize 能跟上,表最终只剩 3 个 chunk,但速率也降到了 365 docs/s,而不是约 450–490。之后 Manticore 的 auto-optimize 会继续向 2 个 chunk(默认目标)压缩;把 optimize_cutoff 降到 1 会让它自动维持单个 chunk,也就是最低延迟状态。

存储与余量

在这个负载下,Manticore 的表数据大约 6 GB,而每台便宜 VPS 有 40 GB 磁盘,所以单节点在需要更大方案之前,可以容纳这个数据集的好几倍。在 turbopuffer 上,存储按 ≤$0.33/GB 计费,这个数据集的存储费用只有 $1.01,确实可以忽略。正如上面的成本拆分所示,$75 和 $16.50 的差距不在存储,而在 Manticore 根本没有的按 GB 写入费用($61.44)。

你真正交换了什么

这些数字在成本和压缩后延迟上都更偏向 Manticore。但比较并不只是数字:turbopuffer 的价格也包含了那些不会出现在延迟表里的东西。

如果改为自建 Manticore,你会放弃:

  • 零运维。 turbopuffer 没有服务器要打补丁,没有副本要故障切换,没有磁盘要扩容。用 Manticore 时,这些都归你负责(不过压缩本身是自动的,见下文)。
  • 弹性扩容和更高的吞吐上限。 turbopuffer 把存储和计算分离,并且可分别独立扩容;你可以从 100 万扩到 10 亿文档而无需重构架构,而且它的托管计算在启用 batching 后达到了约 800–1,100 QPS,而两台 2-core VPS 只能到约 330。两台固定 VPS 的上限也是固定的:想提高只能买更多硬件。
  • 按需归零的经济性。 对于突发或很小的负载,按量计费甚至可能比 $16.50/月 的固定成本更便宜:你只为你查询的部分付费。
  • 托管 SLA(可用性保障、支持)在更高档位上可获得。

使用 Manticore 你会得到:

  • 在这个负载上成本约低 4.5×:固定、可预测的账单,没有按 GB 写入费用,也没有按查询计量。
  • 没有网络往返。 通过 localhost/LAN 查询,避免了远程服务永远要付出的往返成本(即便同地部署也至少有约 9 ms 的下限,跨区域更高),所以压缩后的表端到端更快。
  • 完全控制和数据本地性:你的数据在你自己的服务器上,同时同一个引擎里就有全文搜索、过滤、hybrid search 和 SQL。
  • 安全与数据主权:你的向量和源文档不会离开你控制的基础设施,所以不会被发送到或存储在第三方那里。对于有数据驻留、GDPR 或保密要求的团队,这往往是决定性的:加密、网络隔离和访问策略都由你自己掌控,而且没有外部服务需要你审核、信任或承担泄露风险。对托管服务来说,同样的数据按设计就会存在于服务商的云里。
  • 你自己的 VPS 上的专属计算:固定、可预测的资源,不与其他租户共享。

结论

对于大规模、弹性强或流量突发的负载,或者对于根本不想运维数据库的团队来说,turbopuffer 的 $75/月 以及其背后的托管、无服务器模式是完全说得通的。

但对于非常常见的那种有明确边界的负载(大约 100 万向量、少量 QPS/WPS),只要有人愿意跑几台服务器,两台 $8.25 的 VPS 上的 Manticore 就能提供相同的召回率、自动优化后相当的延迟(而压缩到单个 chunk 时延迟更好,而且没有网络往返)、足够大的吞吐余量,以及约 4.5× 更低的账单。代价是你要自己运行并保持集群健康,而且如果你以后真的需要每秒几百次查询,turbopuffer 的托管计算确实能超过两核固定硬件。

无服务器溢价确实带来了真实价值,而不是单纯加价。这个基准测试回答的问题只是:对于一个小而清楚的负载,这些价值到底会被消耗掉多少,而答案是:通常并不值 $75。

安装Manticore Search

在 Linux 或 macOS 上用一条命令安装 Manticore Search:

curl https://manticoresearch.com | sh

有关高级安装选项,请参阅完整安装指南手册

安装Manticore Search