首页 / 文章 / 数据本地性税:错误区域向量数据库对 RAG 流水线的成本
← 返回
IT技术

数据本地性税:错误区域向量数据库对 RAG 流水线的成本

✍️ zhirenhun 📅 2026/8/23 👁 98 阅读 ⏱ 73 分钟
数据本地性税:错误区域向量数据库对 RAG 流水线的成本

您的 RAG 流水线的 延迟讨论可能卡在了错误的问题上。团队对 GPU 进行基准测试,调整 HNSW 参数,争论 ef_search 的数值,并将近似 最近邻搜索 的延迟降低个位数毫秒。与此同时,存储其嵌入向量的向量数据库与运行其模型的 GPU 位于不同地区,每次检索调用都需要支付由两座建筑之间距离决定的往返税。没有任何索引参数能够弥补这段时间。光速没有配置开关。

本文围绕该差距做了三件事:通过计算器可验证的物理推导出税的下限,展示了税在多跳代理 RAG 中如何累积:一次原本不可见的每次调用成本会变成每项任务的纯地理秒数,并提供一个测量工具,以在 DigitalOcean GPU Droplets向量数据库 上测量您自己的路径,使得争论以您的数字而非我的数字结束。

本文数据的来源。 两个来源,严格分开。往返下限(5.5 ms、41.3 ms、62.0 ms、153.3 ms 及其余值)是通过将大圆距离除以光在光纤中的传播速度(约 200,000 km/秒)得出的。它们是物理设定的下限,而非测量值;实际路径更慢,因为光纤并不沿大圆行进。测量列来源于我于 2026 年 8 月 10 日在实时 DigitalOcean GPU Droplets向量数据库(PostgreSQL 带 pgvector)上运行的公开测试工具。测试脚本和结果位于我创建的 GitHub 仓库中:anishsingh20/data-locality-tax

太长;不读

  • 检索延迟可分为索引调节可控部分和仅由部署决定的部分。 服务器端 ANN 搜索对 HNSW 参数作出响应。网络传输除了将端点移得更近之外,对其他因素无响应。大多数优化工作集中在第一部分。第二部分通常更大。
    • 税的底线是可检验的算术,本文展示了其计算过程。 纽约与旧金山之间的往返时延在光纤中至少为 41.3 毫秒,纽约到法兰克福至少 62.0 毫秒,纽约到新加坡至少 153.3 毫秒,基于大约每秒 200,000 公里的大圆距离。这些是下界。实际路由更慢。在单个数据中心内,同样的下界低于一毫秒。
    • 检索直接阻塞了首个 token 的时间。 模型在检索到的上下文到达之前无法开始 prefill,因此用户在首个 token 生成之前会感受到检索路径上的每一毫秒延迟,除此之外,p50 与 p99 延迟 文章已经测量了服务方面的其他延迟。
    • 税在多跳 agentic RAG 中会累积,这就是故事不再仅仅关注毫秒的地方。 在一个包含 2 秒生成的单次检索聊天中,一次 62 毫秒的往返产生 3.1% 的开销,虽然真实存在但可以接受。在一个代理任务中,八次顺序检索跳会在生成第一个 token 前累计 496 毫秒的纯地理延迟。同样的税,不同的工作负载,结论相反。
    • 冷连接在查询发送前就会放大税。 TCP 握手需要一次往返,TLS 在 TLS 1.3 下额外增加一次往返,在 TLS 1.2 下增加两次往返。在一条 62 毫秒的跨区域路径上,未启用连接池的错误配置客户端每次连接需要支付 124 至 186 毫秒的纯握手延迟,并且会反复产生此开销。
    • 这里的一切都可以复现:测试脚本、绘图和测试结果都位于我创建的 Github 仓库中:anishsingh20/data-locality-tax

本文使用的术语表

如果你刚接触 RAG 或本文涉及的物理概念,请先浏览此表格。

术语 通俗解释 示例
RAG (retrieval-augmented generation) 应用先查找相关事实,然后让语言模型基于这些事实来回答,这样模型就不是仅凭记忆猜测。 客服机器人会在你的文档中搜索 “退款政策”,然后根据找到的片段撰写回复。
Vector / embedding 一串代表文本(或其他项目)意义的数字列表,使相似的概念在数学空间中彼此靠近。 句子 “我该如何重置密码?” 会转换为 768 个数字;相似的帮助文章的数字则位于附近。
Vector database 一种专门用于存储这些数字列表并快速查找最近匹配的数据库。 带有 pgvector 的 DigitalOcean 托管 PostgreSQL、OpenSearch 或 Weaviate,用于保存你的知识库块。
Data locality tax 由于数据库和 GPU 相距较远,每次查询都会产生额外的等待时间:这是你无法通过调整来消除的距离开销。 GPU 位于纽约,向量数据库位于旧金山:在测试运行中每次汇总搜索约需 67 毫秒;当两者都在 NYC3 时,延迟降至 2 毫秒以下。
Latency 某个操作从开始到结束所需的时间,通常以毫秒(ms)为单位。1000 毫秒等于 1 秒。 同一数据中心内的搜索耗时 2 毫秒,而跨区域搜索则需 67 毫秒。
Round trip (RTT) 请求发送到另一台机器并收到回复所需的时间。 仅打开连接然后关闭的 TCP 探测(不涉及数据库操作)在纽约到旧金山的路径上仍需约 69 毫秒。
Latency floor / physics floor 在光纤中受光速限制、以及城市间距离决定的理论最低延迟。实际网络会更慢。 无论软件多么优秀,纽约到旧金山在光纤中的往返延迟也无法低于约 41.3 毫秒。
Speed of light in vacuum 光在真空中的传播速度:约每秒 300,000 公里。这是绝对的宇宙速度上限,而光纤永远无法达到此速度。 仅作参考使用。
光纤中的光速 信号在玻璃电缆中实际传播的速度:约每秒 200,000 公里,相当于真空速度的约三分之二。 将 4,129 公里(纽约至旧金山)除以 200,000 公里/秒,再乘以 2,得到 41.3 毫秒的下限。 折射率 玻璃相对于真空会减慢光速的属性。光纤的折射率正是导致其内部传播速度仅为真空速度的约三分之二,而非真空速度的原因。 相同的光纤、相同的距离,光子传播更慢——这就是折射率带来的惩罚,已内嵌在本文所有下限中。 光纤 承载数据中心间互联网流量的玻璃电缆。在长距离传输中,信号以光的形式传播,而非铜线中的电子。 NYC3 到 SFO3 的流量穿越大陆和海底的光纤传播,这正是物理下限存在的原因。 传播 / 传播延迟 即使在空闲、完美的网络中,信号也必须 传播 而产生的等待。这与数据库执行工作无关。 在 SFO3 分支上,TCP 探测(无 SQL)已约为 69 毫秒。这主要是传播和路由造成的,而非 HNSW。 大圆距离 球面上两点之间的最短路径,相当于在球面上拉紧一根细绳所形成的弧线。 纽约至旧金山的大圆距离为 4,129 公里,实际光纤线路通常更长。 哈弗赛因公式 用于根据两城市的纬度和经度计算大圆距离的学校几何公式。 派生表中的每个下限均采用此方法计算,随后除以 200,000 公里/秒并乘以 2。 下限 这是无法被超越的数字。实际情况等于或高于此值。 测得的 NYC 到 SFO TCP p50 为 68.63 ms,大约位于 41.3 ms 下限之上 27 ms 上方,正如预期。 路由绕道 / 路由开销 因为电缆沿着道路、海岸和对等点铺设,而不是沿着大圆路径,导致额外的距离和跳数。 大约 27 ms 的差距(即 41.3 ms 下限与 68.63 ms TCP 探测之间的差距)正是这种开销,而不是测量误差。 排队延迟 当其他流量在你前面时,在路由器或服务器上排队等待的时间。这不在物理下限内。 这些下限忽略了排队延迟。真实的 p95 数可以捕捉到它;此次运行中遥远的 p50 和 p95 保持接近,因此排队不是主要因素。 带宽 vs 传播 带宽表示管道的宽度(每秒字节数)。传播表示管道 的长度。更粗的管道不会缩短管道。 将 k=5 改为 k=100 对 SFO3 的数值几乎没有影响,因为这次旅行本身主导了额外的字节。 毫秒 (ms) 一秒的千分之一。人类对话能感知到几十毫秒的延迟;八跳 67 ms 的累计超过半秒。 1.90 ms 同机房搜索 vs 66.97 ms NYC→SFO 搜索 vs 八次连续 SFO 跳数的 536 ms。 三个数量级 千倍差异(10 × 10 × 10)。此处用于同机房与跨区域下限的比较。 在单个机房内,下限低于 0.01 ms;纽约到新加坡为 153.3 ms。这就是在一次比较中的架构论点。 地理税 与数据局部性税的概念相同,因其原因而命名:建筑物之间的物理距离。 八次连续检索 × 66.97 ms ≈ 在模型写入 token 之前的地理开销约为 536 ms。 洲内 vs 洲际 洲内表示同一陆块(纽约到旧金山)。洲际表示中间有海洋(纽约到新加坡)。 此次运行测量了洲内情况(下限 41.3 ms,实际 67 ms)。 Prefill 在开始生成答案 token 之前,模型对完整提示(包括检索到的文本)进行首次大量读取。 检索必须在 prefill 开始之前完成,因此查找延迟会在第一个词出现之前显现。 TTFT (time to first token) 用户看到答案第一部分所需的时间。 如果检索耗时 67 ms,那么这 67 ms 会在任何 token 流之前被加到 TTFT 上。 ANN (approximate nearest neighbor) 一种快速的“足够近”相似向量搜索,而不是逐项精确检查。 数据库通过遍历 HNSW 索引来查找近似匹配。 HNSW 一种常用的 ANN 索引形式(邻居链的层),用于加速向量搜索。 CREATE INDEX ...USING hnsw on the items table in the runbook. ef_search HNSW 的一个旋钮:更仔细地搜索(通常较慢,有时能得到更好的匹配)或更宽松地搜索(更快)。 在更大的延迟仍然是跨区域网络时,团队会调整 ef_search。 top-k 返回的最近匹配数量。k 越大,回复载荷越大。 在测量表中,k=5 与 k=100:结果更多,返回路径上的字节更多。 pgvector 一个 PostgreSQL 扩展,用于在 Postgres 内部存储向量并运行相似性搜索。 在 2026 年 8 月 10 日的运行中,用于 Arm A、B1 和 B2 的引擎。 Corpus 加载到数据库中的完整向量集合(以及通常背后的文档)。 100,000 个合成的 768 维向量在 NYC3 和 SFO3 中以相同方式加载。 Agentic / multi-hop RAG 一个代理:检索、思考、再次检索,为单个任务进行多次顺序查找。 八次跳转 × 62 ms ≈ 496 ms 的地理延迟发生在生成之前,即使每次跳转单独看起来很小。 TCP 互联网上两台计算机之间的基本“电话线”设置。 该 harness TCP 探针测量连接到端口 25060 的时间,且不执行 SQL 查询。 TLS 围绕该连接的加密(浏览器中的锁图标)。设置连接需要额外的往返次数。 纽约→旧金山路径上的冷启动首次调用耗时约670至720毫秒,因为握手过程叠加在长距离路径上。 冷连接 全新连接:在首个查询运行之前,需要先完成TCP + TLS握手。 每次无服务器请求都打开一个全新的数据库连接。 连接池 保持并复用已打开的连接,后续查询无需再次握手。 B1上k=5的稳态连接池耗时约67毫秒,而冷启动首次调用约718毫秒。 p50 / p95 / p99 百分位数:一半请求不高于p50;95%请求不高于p95;99%请求不高于p99。尾部延迟(p99)才是偶尔使用的用户真正感受到的。 Arm A在k=5时的检索p50为1.90毫秒;p95为3.51毫秒。 数据载荷 在网络上传输的数据:你的查询发出,结果返回。路径越长,载荷越大,成本越高。 跨区域返回100个完整文档与返回5个ID的对比。

一次检索往返的解剖

在测量任何数据之前,我们先来拆解一次向量搜索调用的组成部分,因为拆解本身就是论证的核心。这是我在本文中试图强调的关键区别:索引调优优化的是组件部署位置无法触及的部分,而部署位置决定了调优无法触及的组件。一个团队在数据库位于大洲另一端时还在benchmarking ef_search取值,这相当于在打磨一条慢路径上的快部分。

从你的应用程序决定发起搜索到结果可用,一次检索调用在五个环节上耗费时间。

按控制因素对这五部分进行归类。ANN搜索时间取决于索引参数、硬件和语料库规模。而其他所有部分都取决于部署位置和连接管理纪律。

一次检索调用在时间线上分解为五个阶段:连接建立、查询序列化、出站传输、服务器端 ANN 搜索和返回传输,其中与位置相关的阶段用砖红色标记,与调优相关的阶段用青绿色标记。 五个阶段,两个所有者。调优拥有青绿色阶段。地理位置负责红色阶段,且没有任何索引参数能够影响它们。来源:作者对一次检索调用的分解;检索块预填行为遵循 DigitalOcean 的端到端 RAG 教程 中的管道阶段以及 p50 与 p99 延迟 中的 TTFT 框架。

这直接影响用户体验,而不是隐藏在仪表盘中,原因在于检索块预填。

在标准的 RAG 流程中,模型只有在拿到检索到的上下文后才能开始处理提示词,因此检索路径上的每一毫秒都会增加首次 token 生成时间,这是用户在看到任何内容之前一直关注的指标。

税收的下限,源自推断而非断言

光在光纤中的传播速度大约为每秒 200,000 公里,约为真空中光速的三分之二,这是因为光纤的折射率。这个数值把距离转化为无法通过工程手段消除的延迟下限。将两城市之间的大圆距离除以每秒 200,000 公里,再乘以 2 得到往返时间,这就是它们之间的最小可能往返时间,在出现排队、路由绕道或服务器进行任何工作之前。

本文数据的来源。 下表是我根据该方法自行推导的。我使用哈弗赛尔公式和每个都市区的坐标计算了每个大圆距离。这些是下限,而非实际性能的估计:生产光纤路线不遵循大圆路径,因此实际往返时延会超过这里的每个数字。

路径 大圆距离 光纤往返下限
单个数据中心内部 低于 1 km 低于 0.01 ms
纽约至多伦多 550 km 5.5 ms
纽约至旧金山 4,129 km 41.3 ms
纽约至伦敦 5,570 km 55.7 ms
纽约至阿姆斯特丹 5,863 km 58.6 ms
纽约至法兰克福 6,203 km 62.0 ms
纽约至班加罗尔 13,368 km 133.7 ms
纽约至新加坡 15,332 km 153.3 ms
纽约至悉尼 15,989 km 159.9 ms

将第一行与其余行对比,因为第一行是整个架构论点。同一 DigitalOcean 数据中心内的 GPU Droplet 和托管向量数据库通过 VPC 网络通信,其传播下限比任何跨区域配对低三个数量级。DigitalOcean 自己的 VPC 文档 确认了相关属性:VPC 网络仅限于单个数据中心区域,VPC 内部流量免费且私密,跨数据中心连接 VPC 需要 VPC 对等互连,该服务在所有数据中心之间均可用,除了 BLR1 外,按跨数据中心流量收费 $0.01 每 GiB。物理和定价指向同一方向。

圆往返时延下限的柱状图,基于光在光纤中的速度推算,从单个数据中心内部低于 0.01 毫秒,到纽约-多伦多 5.5 毫秒,旧金山 41.3 毫秒,法兰克福 62.0 毫秒,新加坡 153.3 毫秒;每根柱子旁的注释标明这是实际路由会超过的下限。 这是推导得出的,而非实际测量。实际路径的时延会慢于图中每个柱子所示的数值,因为光纤并不沿着大圆路径敷设。第一个柱子是论点的基础。来源:作者的推导,哈弗赛因大圆距离除以约 200,000 公里/秒的光纤传播速度,再乘以 2 得到往返时延;方法及完整表格见本文的“基准”部分。

实验:一次查询,三种部署

这是 harness 存在的章节。下面的设计已经完全指定;harness 和原始结果已在我创建的 GitHub 仓库中发布:数据局部税 — — 跨地区检索延迟测量

我在 2026 年 8 月 10 日进行了此测试。我从 NYC3 Droplet 向 NYC3 和 SFO3 的托管 PostgreSQL 16 + pgvector 填充了 Arms A、B1 和 B2(每个单元格使用 10 万个合成的 768 维向量,HNSW 余弦,在 10 次预热后进行 75 次试验)。

固定变量

在固定区域的一个 GPU 实例(推荐使用 NYC 区域),运行嵌入模型和 LLM。一个语料库,大约包含 100 万个向量,维度为现实中的 768 或 1,024,在所有被测存储中以完全相同的方式加载。在引擎允许的情况下,各臂的索引类型和参数保持一致,这样服务器端的 ANN 时间在比较中相互抵消,剩余的差异仅反映路径的影响。

三个实验臂

Arm A,同一数据中心通过 VPC A DigitalOcean 向量数据库 与 GPU Droplet 位于同一数据中心,附加到同一 VPC,通过专用网络进行查询。由于托管 PostgreSQL 在各地区广泛可用,带有 pgvector 的 PostgreSQL 是最安全的引擎选择。OpenSearch 是混合搜索工作负载的替代方案,Managed Weaviate 根据 Vector Databases 发行说明于 2026 年 7 月 1 日进入公开预览,因此在将其作为一个 arm 配置之前,请查看您所在地区配对的可用性页面。

您可以参考 在 OpenSearch、Weaviate 和 pgvector 之间进行选择 以获取更多关于选择权衡的信息。

Arm B,远程 DigitalOcean 区域 相同的数据库,相同的引擎,相同的计划,相同的索引,在远程区域进行配置:大陆情况下的 NYC 到 SFO。通过公共端点进行查询,并在配置了的情况下,此外还通过数据中心间 VPC 对等互连进行查询,两条路径分别记录。

Arm C,第三方托管向量存储 一种 SaaS 向量数据库,尽可能按照其套餐层级匹配地区。现在提出一个框架规则,以便结果部分继承它:此 arm 的目的不是为了点名道姓地指责供应商对其无法控制的物理现象。它之所以存在,是因为许多团队默认使用 SaaS 向量存储,却从未检查其集群实际所在的地区,而 arm C 用来衡量这种未经审查的默认选择所带来的成本。在结果中披露地区匹配的尝试以及供应商声明的地区。目标是决策模式,而非供应商。

测量什么

每个 arm,每种配置:在剔除预热后,每个单元至少 75 个请求,跨两个时间窗口,报告 p50、p95 和 p99,遵循来自 无服务器推理中重要的指标 的测量标准。

每个单元格在 top-k 取值为 5、20、100 下运行,以暴露载荷大小与距离的交互作用;每个单元格分别记录冷连接和池化连接的时序,因为池化混淆变量值得拥有独立列,而不是污染平均值。测试套件还会对每个臂执行裸 TCP 连接探测,这近似于一次无数据库工作的网络往返,从而得到测量的路径底线,可与推导出的物理底线并列。

实验设计示意图:纽约的一个 GPU Droplet 持有嵌入模型和 LLM,查询三个臂,同一数据中心通过 VPC 的向量数据库,同一数据库位于遥远地区,以及一个第三方托管存储,全部加载了完全相同的语料库和索引参数,每个臂报告延迟分布以及一个裸 TCP 往返探测。相同的语料库、相同的索引、相同的查询。唯一的变量是路径,这就是重点。来源:本文的实验设计;臂的定义参见 DigitalOcean 向量数据库VPC 文档;试验次数参见 服务器无推理中的重要指标

结果:税项逐项

测量表(2026年8月10日运行)

客户端使用的是位于 NYC3 的 Droplet(s-2vcpu-4gb, Ubuntu 24.04),存储为托管的 PostgreSQL 16 + pgvector,方案为 db-s-1vcpu-1gb,数据集为相同的 100k × 768 维 HNSW。测试时间窗口为 10:04–10:05 UTC。完整 JSON 见:anishsingh20/data-locality-tax

方案 路径 TCP RTT 探测 p50 检索 p50 (k=5) 检索 p95 (k=5) 检索 p50 (k=100) 冷连接首次调用
A 同一数据中心,VPC(NYC3 私有) 2.43 ms 1.90 ms 3.51 ms 11.37 ms 111.34 ms
B1 NYC3 到 SFO3,公共网络 68.63 ms 66.97 ms 69.55 ms 70.11 ms 717.91 ms
B2 NYC3 到 SFO3,对等 VPC 67.73 ms 69.90 ms 70.57 ms 75.44 ms 668.87 ms

以下是此表格的含义:

  • 物理预测了顺序,且运行落在了地板之上。 光在光纤中无法以大约 41.3 ms 的时延从纽约到旧金山。测得的 TCP 路径大约比该基准高出 ~27 ms,这是预期中的:真实光纤并不沿着大圆路径。没有任何测量落在基准之下,这是运行手册要求的健全性检查。
  • 私有对等连接不会消除地理影响。 Arm B2 通过 VPC 对等连接使用了私有主机名。中位数搜索延迟为 69.90 ms,与公共路径相差仅几毫秒。对等连接使流量脱离公共互联网,并改变了你为字节付费的方式。它不会让纽约更接近旧金山。
  • 当路径较长时,获取更多结果几乎没有影响。 返回 100 个邻居而非 5 个,使本地臂的延迟从 1.90 ms 增至 11.37 ms,因为在网络已经很快时,额外的有效载荷会变得可见。在 SFO3 公共环境下,k=100 时延迟为 70.11 ms,而 k=5 时为 66.97 ms。这些额外文档相对于 67 ms 的往返时间只是微小的误差。
  • 冷连接会放大延迟税。 全新连接上的第一次调用在查询甚至运行之前就需要支付 TCP 加 TLS 握手的开销。在本地,该首次调用的延迟为 111.34 ms。横跨大陆时,公共路径为 717.91 ms,对等路径为 668.87 ms,大约是汇总搜索的十倍。如果你的应用为每个请求打开一个新的数据库连接,你将多次支付地理开销,而不仅仅是一次。
  • 典型调用同样是典型调用。 在远端臂上,p50 和 p95 的值非常接近(在 B1 上分别为 66.97 ms 和 69.55 ms)。该路径持续较慢,而非偶尔变慢。这是一个放置问题,而非噪声索引导致的。
  • 为了让事情更易理解,我制作了这些图表来说明得更清楚:

    下面的图表是根据 anishsingh20/data-locality-tax 中的原始 JSON 渲染得到的。重新运行 python3 analysis/plot_results.py,图形将随文件一起移动。

    图 1. 主要结果。相同查询、相同索引、相同语料库。将存储从 NYC3 移动到 SFO3 使中位数汇总检索延迟乘以 35×

    显示 Arm A(1.9 ms)、Arm B1(67.0 ms)和 Arm B2(69.9 ms)在 k=5 时汇总检索 p50 的柱状图,并标注从 B1 到 A 的 35× 倍增。

    图 2. 在远端臂上,汇总检索位于 TCP 探测之上。索引不是瓶颈。冷首次调用(连接 + TLS + 首次查询)大约在 670–718 毫秒。来源:匹配 *_tcp.json*_k5.json 文件。*

    比较三个臂的 TCP connect p50、汇总检索 p50 和冷首次调用的分组柱状图。

    图 3. 仅当网络已经很快时,额外邻居才会产生实时开销。臂 A 从 k=5 时的 1.90 毫秒上升到 k=100 时的 11.37 毫秒。B1 保持在 60 毫秒中段至低 70 毫秒范围。来源:*_k5.json*_k20.json*_k100.json

    每个臂在 k=5、20、100 时的汇总检索 p50 的分组柱状图。

    图 4. 对物理进行的合理性检查。NYC–SFO 在光纤中无法低于 41.3 毫秒。测得的 TCP p50 为公共网络 68.63 毫秒、对等网络 67.73 毫秒,大约比基准高出 27 毫秒的路由开销。没有任何测量值低于基准。来源:本文的 TCP 探测以及哈弗赛因基准。

    每个臂的物理光纤基准与测得的 TCP connect p50 的柱状图。

    图 5. 对测得的 k=5 p50 进行算术,这不是第二次实验。B1 上的八个顺序跳数在地理上累计为 536 毫秒,在此之后才会生成令牌。臂 A 在相同跳数下保持在 16 毫秒以下。来源:a_k5.jsonb1_k5.json,乘以跳数。

    根据测得的臂 A 和臂 B1 p50 值,绘制的检索税总和随跳数变化的折线图。

    图 6. 远程路径始终较慢。B1 的 p95 仅比 p50 高出 2.6 ms。这属于布置问题,而非噪声索引。来源:汇总自 *_k5.json

    每个臂部在 k=5 时检索 p50 与 p95 的分组柱状图

    复合表,这是算术的

    单次 RAG 对每个用户查询执行一次检索。代理 RAG 会执行多次检索,且是顺序的,因为每跳的结果决定下一跳的查询:检索、推理、再次检索、重新排序、获取邻居、验证。每任务五到十次顺序检索是代理模式的常见形式。顺序意味着税费相加。

    每次调用税 1跳 5跳 8跳 10跳
    ~1 ms (同一数据中心,预期顺序) 1 ms 5 ms 8 ms 10 ms
    41.3 ms (NYC-SFO floor) 41 ms 207 ms 330 ms 413 ms
    62.0 ms (NYC-FRA floor) 62 ms 310 ms 496 ms 620 ms
    153.3 ms (NYC-SGP floor) 153 ms 767 ms 1,226 ms 1,533 ms

    这些是楼层乘以跳数得到的,是对推导数字的纯粹算术,实际总数位于每个单元格之上。

    中间的行讲述了故事:大陆错位导致每个 8 跳代理任务额外花费三分之一到半秒的纯地理延迟,而跨洲错位则超过一秒,在任何查询执行之前,任何 token 生成之前,对每个任务而言,这种开销将永远存在,直到有人移动数据。

    复合图显示总地理税与顺序检索跳数的关系,包含四条线:同一数据中心、纽约至旧金山、纽约至法兰克福、纽约至新加坡的楼层;同一数据中心的线保持在零附近平坦,而新加坡线在八跳时超过一秒。相同的每次调用税乘以代理实际检索的次数。平坦的线代表 colocations 带来的收益。来源:作者的算术,本文推导出的光纤楼层乘以跳数;实际总数位于每条线之上。

    比例性检查

    对于一次检索的聊天查询,生成耗时 2 秒,跨地区税费 62 毫秒仅占响应时间的 3.1%——这是真实存在的、可测量的且能够承受的开销;如果这正是你的工作负载特征,那么进行索引调优和缓存比迁移更值得你投入一周的精力。对于每个步骤仅生成很短时间的 8 跳代理任务,累计的 496 毫秒税费已不再是可以忽略的误差:相对于每步平均生成 300 毫秒的步骤,地理位置使任务的关键路径增加约 17%;相对于更短的工具选择步骤,这一税费几乎与计算本身持平。这两个说法都是基于同一推导税费的算术计算。最终的判断取决于工作负载本身,而非税费。

    连接池混淆因子,免费放大税费

    冷连接需要支付 TCP 握手、一次往返,然后是 TLS 握手,在 TLS 1.3 上再加一次往返,在 TLS 1.2 上再加两次往返,之后才能发送查询。这是协议的算术运算,而非实际测量。在同一数据中心的路径上,这些握手只需几毫秒,基本无人察觉。

    在 62 毫秒的跨地区路径上,相同的握手每连接会产生 124 至 186 毫秒的纯粹设置开销。如果客户端错误地配置为每个请求都打开一个新连接——这正是无服务器函数和快速编写脚本的常见默认行为——则每次调用都会承担设置税,大约使地理位置费用增加三倍。连接池 是免费的,能够消除整个倍增因子,因而成为最廉价的解决方案;它不会减小税额,而是阻止你三次支付该税。

    池化连接每查询支付一次往返与冷连接每查询支付 TCP 加 TLS 握手的双车道对比,跨区域情况下在第一个字节之前的设置时间为 124 至 186 毫秒。 相同路径,相同查询,费用翻三倍。连接池是本文中最便宜的开支项。来源:根据 TLS 1.3 规范(RFC 8446)和 TLS 1.2(RFC 5246)的握手往返次数,应用于本文推导的 62 毫秒纽约-法兰克福基准。

    放置层次结构

    每个档次的税费是物理设定的推导基准,以及仅靠测量得到的模板条目。

    档位 1,同一数据中心通过私有网络。 GPU Droplet 和向量数据库位于同一个 DC、同一个 VPC。传播延迟低于 0.01 毫秒,实际往返时间预计在个位数毫秒范围内,VPC 内部流量免费,依据 DigitalOcean 的 VPC 文档。这是测试 harness 建立的测量基准,其他档位的费用均相对此基准定价。

    档位 2,同一地区,不同放置。 同一都市区,使用公共端点而非 VPC,或资源位于同一地区的兄弟数据中心。基准低于 1 毫秒,实际成本主要由路由而非距离决定。通常可以接受,通常也不必要,因为档位 1 可用。

    档位 3,跨地区,同一提供商。 纽约到旧金山和纽约到新加坡的链路:基准分别为 41.3 毫秒和 153.3 毫秒,另外如果保持路径私有,则跨数据中心 VPC 对等互连费用为每 GiB 0.01 美元。此时的税费高于大多数调校良好的 ANN 搜索,意味着网络主导了检索调用。

    档位 4,跨提供商,第三方 SaaS 存储。 包含档位 3 的一切,此外还有您可能从未刻意选择的地区、提供商之间的互联网路径,以及您无法观察到的供应商自身负载。臂 C 测量此档位的实际成本。反复出现的失败模式不是选择此档位,而是默认落在此档位上且从未检查。

    第5层,未完整测量即被标记:基于对象存储的检索。 从对象存储提供的索引,即真正的冷存储层。如果可行,则在测试套件中进行一次指示性测量,仅作指示披露。底层逻辑仍然适用,存储延迟叠加在其上。

    架构含义颠倒了通常的优化顺序:先将数据与计算共置,再调优索引。 索引调优可以恢复毫秒级延迟,而这一段的放置从未涉及。放置每次调用可以恢复十到几百毫秒,而这些段的调优从未触及。先做大而乏味的结构性修复,再做小而有趣的参数化调整。

    包含五个层级的放置层级梯子,从同一数据中心 VPC 的底层开始,经过同一区域、跨区域、第三方 SaaS 以及基于对象存储的检索,每个层级均标注其推导出的底层或待测量标记。 五个层级的定价:在物理能够给出答案的地方由物理决定,在仅能通过测试套件得到的地方由测试套件决定。不要超过你的工作负载所能承受的高度。来源:本文推导得出的楼层;VPC 范围划分、免费的 intra-VPC 流量,以及每 GiB $0.01 的跨数据中心对等互连,依据 DigitalOcean VPC 特性VPC 可用性,已于 2026 年 8 月 6 日验证。

    决策框架:局部性何时重要、何时不重要

    局部性在以下情况下至关重要: 您的工作负载是代理式或多-hop RAG 时,因为顺序跳会将开销乘倍。当您对 TTFT 有严格预算时,因为检索会阻塞预填充,且开销完全落在用户首先感知的数量上。当查询量很大时,因为开销与量的乘积也是一种出流量和对等互连成本的故事,每 GiB 跨数据中心收费 $0.01。以及当您的架构在顺序阶段重新排序或获取邻居时,这其实是多-hop RAG 只是换了个名字。

    局部性在何时可以协商 您的流程在长生成之前执行一次检索,比例检查显示开销为 3%。当您的管道是异步或批处理时,没有人会等待任何单个调用。此外,当语料库因合规或驻留原因必须位于特定地区时,决策将由系统为您完成,剩余的操作是将计算向数据迁移或将索引复制到计算区域,取决于您的更新率哪种更便宜:变化缓慢的语料库复制效果良好,变化快速的语料库通常会将计算拉向自身。

    实际指南,按顺序进行三项检查

    运行手册:在 DigitalOcean 上运行此实验

    DigitalOcean 产品需求。如果您想从实际提供模型服务的机器上进行测量,可以在您的固定区域使用一个 GPU Droplet;而在同一数据中心内的普通 CPU Droplet 对网络测量本身的效果完全一致,因为测试工具测量的是路径,而非 GPU。使用 PostgreSQL 引擎的两个 Vector Database 集群:一个与 Droplet 位于同一数据中心(用于 A 组),另一个位于远端区域(用于 B 组),两者均采用最小套餐,因为语料库是合成的且索引很小。每个区域的默认 VPC(所有资源自动加入),以及如果您需要 B2 私有路径单元,还可以在两个区域之间建立可选的 VPC 对等连接。C 组需要一个第三方 SaaS 向量存储账户,并将其区域设置截图以用于披露。

    Droplet 上的工具。Python 3(DigitalOcean 镜像预装)、通过 pip install "psycopg[binary]" 安装的测试工具唯一依赖项,以及通过 apt install postgresql-client 安装的 PostgreSQL 客户端,用于加载语料库。doctl 是可选的,用于从命令行创建集群,而不是通过控制面板。

    步骤 1:创建两个集群。Vector Databases 页面中,在 Droplet 的数据中心创建一个 PostgreSQL 集群(用于 A 组),并在远端区域创建另一个(用于 B 组),两者采用相同的套餐。将两个区域记录到结果文件中。

    步骤 2:锁定访问权限并收集连接字符串。将 Droplet 添加到每个集群的受信任来源中。每个托管集群都会公开两个主机名:私有主机名通过 VPC 访问集群,仅在同一数据中心网络内部有效;公有主机名则通过互联网路由。A 组使用私有主机名。B1 组使用公有主机名;如果您配置了对等连接,B2 组则通过对等的 VPC 使用私有主机名。将每个连接字符串导出为独立的 DSN 环境变量,以免各次运行在不知不觉中混淆不同组。

    步骤 3:将相同的语料库加载到两个集群中。相同的表、相同的维度、相同的索引、相同的参数。使用 psql "$DSN_ARM_A" 加载,然后针对 B 组再执行一次:

    CREATE EXTENSION IF NOT EXISTS vector;
    CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(768));
    INSERT INTO items (embedding)
    SELECT ARRAY(SELECT random()*2-1 FROM generate_series(1,768))::vector
    FROM generate_series(1,100000);
    CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
    

    100,000 个合成向量可以保持加载快速且比较公平,因为相同的语料库和相同的索引参数会抵消臂对臂增量中的 ANN 时间。如果你想要生产级别的索引深度,可将其扩展到 100 万,并记录你使用的数量。这里使用随机向量也没关系,因为此实验测量的是路径,而不是召回率。

    步骤 4. 首先对每个臂运行 TCP 探测。 DigitalOcean 托管的 PostgreSQL 监听在端口 25060。该探测不需要数据库凭据,近似等于一次原始网络往返:

    python3 locality_bench.py --mode tcp --host <arm-a-private-host> --port 25060 --out a_tcp.json
    python3 locality_bench.py --mode tcp --host <arm-b-public-host>  --port 25060 --out b1_tcp.json
    

    注意:文件/Python 脚本 locality_bench.py 位于下一节,并且我也在创建的 GitHub 仓库中提供了该文件 局部性基准脚本.py,用于此测试。

    将每个探针的 p50 放在该路径推导出的基准旁边。差距即为路径相对于物理极限的开销;如果探针落在其基准之下,则说明有误标,因为没有任何东西会低于基准。

    步骤 5. 运行检索单元。 对于每个臂,在 k 为 5、20、100 时:

    python3 locality_bench.py --mode pgvector --dsn "$DSN_ARM_A" --k 5   --out a_k5.json
    python3 locality_bench.py --mode pgvector --dsn "$DSN_ARM_A" --k 20  --out a_k20.json
    python3 locality_bench.py --mode pgvector --dsn "$DSN_ARM_A" --k 100 --out a_k100.json
    

    依据该系列文章已采用的测量标准,在另一天的第二个时间窗口(或隔夜)重复完整测量。测绳(harness)在每次运行中将冷连接首次调用与汇总稳态分开记录,因而汇总混杂因素自动获得了独立的一列。

    步骤 6. 运行臂 C。 如果 SaaS 商店的端点使用 PostgreSQL 线协议,则用相同的测绳指向它;否则,使用相同的试验次数和 k 值对其原生客户端进行计时,并在结果中披露客户端差异。将供应商声明的地区记录在数字旁边。

    步骤 7. 填写模板并拆除。 将 Droplet 上的所有 JSON 文件复制下来,填写本文中的测量表,并从 销毁页面 销毁两个集群,以使最小套餐集群停止计费。如果当天拆除,整个运行的基础设施费用仅需几美元。

    测绳(harness)

    可在任何装有 Python 3 的 GPU Droplet 上运行。TCP 探测模式仅使用标准库。pgvector 模式需要一个依赖项,可通过 pip install "psycopg[binary]" 安装。依次将 --dsn 指向各个臂,并保存输出文件。

    #!/usr/bin/env python3
    """
    Data-locality retrieval latency harness.
    
    Modes:
      tcp     : bare TCP connect probe, approximates one network RTT (stdlib only)
      pgvector: timed vector similarity queries against a pgvector database
    
    Per cell: >=75 trials after warmup discards, p50/p95/p99, cold-connection
    first-call time recorded separately from pooled steady state.
    
    Run each arm from the same GPU Droplet:
      python3 locality_bench.py --mode tcp --host db-host --port 25060 --out a_tcp.json
      python3 locality_bench.py --mode pgvector --dsn "$DSN_ARM_A" --k 5 --out a_k5.json
    """
    import argparse, json, os, random, socket, statistics, time
    
    def pctl(xs, p):
        xs = sorted(xs)
        k = (len(xs) - 1) * p / 100
        f = int(k); c = min(f + 1, len(xs) - 1)
        return xs[f] if f == c else xs[f] * (c - k) + xs[c] * (k - f)
    
    def summarize(xs):
        return {"n": len(xs), "p50_ms": round(pctl(xs, 50), 2),
                "p95_ms": round(pctl(xs, 95), 2), "p99_ms": round(pctl(xs, 99), 2),
                "mean_ms": round(statistics.fmean(xs), 2)}
    
    def tcp_probe(host, port, trials, warmup):
        times = []
        for i in range(trials + warmup):
            start = time.perf_counter()
            s = socket.create_connection((host, port), timeout=10)
            elapsed = (time.perf_counter() - start) * 1000
            s.close()
            if i >= warmup:
                times.append(elapsed)
            time.sleep(0.05)
        return {"mode": "tcp", "host": host, "port": port, "summary": summarize(times),
                "note": "TCP connect approximates one network round trip, no DB work"}
    
    def pgvector_bench(dsn, k, dim, trials, warmup, table):
        import psycopg  # pip install "psycopg[binary]"
        from psycopg import sql as psql
        rng = random.Random(7)
        qvec = "[" + ",".join(f"{rng.uniform(-1, 1):.6f}" for _ in range(dim)) + "]"
        query = psql.SQL("SELECT id FROM {} ORDER BY embedding <=> %s::vector LIMIT %s").format(
            psql.Identifier(table)
        )
    
        # cold connection: connect + first query, timed together
        start = time.perf_counter()
        conn = psycopg.connect(dsn)
        with conn.cursor() as cur:
            cur.execute(query, (qvec, k))
            cur.fetchall()
        cold_ms = (time.perf_counter() - start) * 1000
    
        # pooled steady state: reuse the connection
        times = []
        with conn.cursor() as cur:
            for i in range(trials + warmup):
                q = "[" + ",".join(f"{rng.uniform(-1, 1):.6f}" for _ in range(dim)) + "]"
                start = time.perf_counter()
                cur.execute(query, (q, k))
                cur.fetchall()
                elapsed = (time.perf_counter() - start) * 1000
                if i >= warmup:
                    times.append(elapsed)
        conn.close()
        return {"mode": "pgvector", "k": k, "dim": dim,
                "cold_connection_first_call_ms": round(cold_ms, 2),
                "pooled": summarize(times)}
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--mode", choices=["tcp", "pgvector"], required=True)
        ap.add_argument("--host"); ap.add_argument("--port", type=int, default=25060)
        ap.add_argument("--dsn", default=os.environ.get("PG_DSN"))
        ap.add_argument("--k", type=int, default=5)
        ap.add_argument("--dim", type=int, default=768)
        ap.add_argument("--table", default="items")
        ap.add_argument("--trials", type=int, default=75)
        ap.add_argument("--warmup", type=int, default=10)
        ap.add_argument("--out", required=True)
        args = ap.parse_args()
    
        if args.mode == "tcp":
            result = tcp_probe(args.host, args.port, args.trials, args.warmup)
        else:
            result = pgvector_bench(args.dsn, args.k, args.dim,
                                    args.trials, args.warmup, args.table)
        result["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
        with open(args.out, "w") as f:
            json.dump(result, f, indent=2)
        print(json.dumps(result, indent=2))
    
    if __name__ == "__main__":
        main()
    

    此脚本用于在机械臂上运行 TCP 和 pgvector 探测。

    首先在每个机械臂上运行 TCP 探测,并将其 p50 放在该路径的推导基准旁边。它们之间的差距就是您的路由相对于物理极限的开销。随后在每个机械臂上分别在独立的时间窗口内以 k=5、20、100 运行两次 pgvector 模式,并填充模板表。对于调试意外路径的读者,网络性能诊断教程 介绍了此测试套件会显现但不会解释的不对称路由问题。

    此主题的常见问题?

    1. VPC 对等连接能否修复跨区域向量数据库延迟?

    否。在我的测试中,NYC3→SFO3 的对等私有主机名(B2)在 TCP 和检索上与公共主机名(B1)在几毫秒内匹配。对等连接改变了隐私和出站计费;它并未让建筑物更近。

    2. 约 60–70 ms 的洲际税对于单次 RAG 迁移是否值得?

    通常不值得。相对于 2 秒的生成时间,67 ms 的检索税仅占响应时间的几个百分点。索引调优和缓存可能是一周时间的更好利用。对于多跳代理 RAG,相同的税乘以跳数后,结论会倾向于更近的部署。

    3. 为什么我的冷 pgvector 调用大约是汇总 p50 的 10 倍?

    冷连接在第一次查询之前需要先完成 TCP 加上 TLS 握手。在约 68 毫秒的纽约到旧金山路径上,仅此设置就可能耗费数百毫秒。使用连接池; 请勿为每个请求打开新连接。

    4. 向量数据库是否必须始终与 GPU 同置?

    在你同时控制两端且驻留允许的情况下,答案是肯定的。在我的测试中,同一数据中心的 VPC 在池化 k=5 时延迟为 1.90 毫秒,而跨区域则为 66.97 毫秒。合规驻留可能会强制采取相反的措施:将计算迁移到数据附近,或复制一个变化缓慢的索引。

    5. 我能否复现这些数字?

    是的。测试 harness、加载 SQL 和原始 JSON 位于 anishsingh20/data-locality-tax。预期顺序和基准相同;绝对毫秒数会随路径和时间而变化。

    配套仓库

    测试 harness、加载脚本、逐个单元的原始 JSON 以及运行元数据: https://github.com/anishsingh20/data-locality-tax

    结论

    行业基准测试只关注 GPU,而忽略了管道。得出的层次结构为每个放置层级定价,复合表格展示了为什么智能体工作负载会改变结论,而测试 harness 能让你在一个下午的 GPU Droplet 上将论点转化为自己的数字。

    此集群中的优化在整个路径上要么协同生效,要么共同失效。一个 温暖的提示缓存 能够节省预填充的毫秒,而错误地区的向量数据库会把这些毫秒直接退回。一个 经过调优的服务尾部 在面对 496 毫秒的地理延迟时几乎不起作用,而 推理三难题 的框架对部署的适用方式与其对服务的适用方式完全相同:您需要选择要承担的约束,本文将为您定价一种大多数团队甚至未意识到自己正在承担的约束。

    经验法则是:先 colocate,再调优,最后两者都测量。

    参考文献

    DigitalOcean 文档

    DigitalOcean 社区与博客

    ——

    🧑‍💻

    zhirenhun

    一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

    ← 上一篇
    在修改前如何利用AI理解遗留代码库
    下一篇 →
    神经网络详解:它们是什么以及如何用 Python 构建一个

    📌 相关推荐

    GraphRAG 是推理问题,而非数据库问题
    2026/8/30
    构建市场时光机:使用 Python 和 WebSocket 重放交易会话
    2026/8/30
    如何自行基准测试LLM推理:值得信赖的数字设计标准
    2026/8/30
    ← 返回文章列表