访问节点:11。比较次数:不变。
2026 年 9 月的一组本地测试中,发现原型反复报出相同计数。网络里有 32 个 Agent 时如此,增加到 512 个时仍然如此。请求最多在附近传播两跳,计数看起来完全符合预期。
耗时却变了。查询延迟的第 95 百分位从 5.66 毫秒升到 47.64 毫秒。网络扩大了 16 倍,查询慢了八倍多。
这组矛盾的数字把问题指向了一个地方:访问计数之外,还有工作在发生。
请求出发前,系统已经走遍全网
先介绍这条查询在做什么。原型中的每个 Agent 保存一份可用于发现的信号,并连接到少数邻居。请求经过一个节点时,需要判断附近是否还有值得探索的对象。系统为此构造一份邻域摘要,把这个节点及其一跳邻居的公开信号合在一起。
用城市地图类比,查询只准备走几条街;邻域摘要相当于路口的导览牌。它帮助查询决定往哪边继续。
早期实现选择了一种很顺手的写法:查询开始时,先给所有节点重新制作导览牌,再让请求出发。
# 早期请求路径的简化结构
adverts = build_all_neighborhood_summaries(network)
return walk_local_routes(request, adverts)
第二行确实只走局部路径。第一行已经扫描了整个网络。
现有资源计数主要覆盖传播和比较,所以它忠实地报告了“访问 11 个节点”。这项指标回答的是传播阶段的工作量。全量摘要重建发生在传播前,既消耗时间,也随着网络规模增长,却没有出现在这张账上。
这个现象在数据系统中并不陌生。数据库返回很少的行,也可能先扫描很多行;一个带 LIMIT 的查询,也可能先支付排序或索引构造的启动成本。PostgreSQL 的 EXPLAIN 文档专门区分输出行数、启动成本和整体成本。阅读这些量时,必须知道它们各自覆盖哪一段工作。
对这个 Agent 原型而言,问题已经足够具体:局部访问省下来的工作,在进入局部访问之前又被做了一遍。
把摘要构造推迟到真正需要的那一刻
我们把批量构造拆成了按节点构造。查询走到某个边缘、需要查看其邻域时,才制作那一份摘要。同一次同步查询再次用到它时,直接复用已经计算的结果。
# 修订后请求路径的简化结构
adverts = {}
def summary_for(edge_id):
if edge_id not in adverts:
adverts[edge_id] = build_one_neighborhood_summary(edge_id)
return adverts[edge_id]
return walk_local_routes(request, summary_for)
这里的 adverts 只活在一次查询里。下一次查询重新读取当前节点和连接,因此节点更新、加入或离开之后,可以自然得到新的摘要。这是有意保留的简单性:当前原型同步执行,一次发现期间没有并发修改网络,查询内复用就能拿到主要收益,也省下了跨请求缓存失效的复杂度。
完整构建所有摘要的入口仍然有用,例如诊断或导出全网快照。我们只是把它从普通查询的必经之路移开。
与此同时,资源计数新增了两个直接反映实际动作的量:构造了多少份摘要,读取了多少个摘要贡献者。原来的访问计数继续保留;新计数让准备阶段也进入观察范围。
同一组工作负载,延迟曲线变平了
比较沿用相同的机器、拓扑、查询和配置。每个规模运行 12 次发现请求,使用固定的双向环形连接,每个节点连接到偏移为 1 和 3 的邻居;查询最多传播两跳。下面是这组短时本地构造测试的历史记录。
| 网络中的 Agent 数量 | 修改前 p95 | 修改后 p95 |
|---|---|---|
| 32 | 5.66 ms | 4.28 ms |
| 128 | 13.21 ms | 4.91 ms |
| 512 | 47.64 ms | 5.15 ms |
图 1:同一组工作负载的历史前后比较。横轴按三个测试规模排列,连线用于引导阅读;各点为 12 次查询的 p95。
在 512 个 Agent 的一组里,吞吐量由每秒约 22 次查询提高到约 209 次。每次查询仍访问 11 个 Agent;现在它只构造 10 份摘要,检查 50 个贡献者。这两项计数在三个规模下都相同。
配对分析核对了候选数、交接数、访问数和原有资源计数,记录中的这些量保持一致。因此,这次改动有一个明确解释:省掉了查询用不到的预处理。新的时间曲线与新增加的工作量计数相互印证。
这个结果适用于当时的单机、同步、构造拓扑。每组只有 12 次请求,p95 是短样本上的线性插值统计量;生产尾延迟还会受到并发、网络与数据变化影响。这里最有价值的结果,是找到并消除了一段实际随全池规模增长的请求前置工作。
局部算法需要一条局部的请求路径
一个算法可以在核心循环里很节制,却在初始化、序列化、索引维护或清理阶段扫描全量数据。这次遗漏发生在摘要构造;新增计数把它纳入了同一条请求的成本。
在自己的系统里,可以做一个很小的实验:固定请求实际需要的对象,让无关对象不断增加,同时记录端到端时间。如果核心操作计数不变而时间持续增长,就沿请求路径寻找尚未入账的工作。
修复以后,请求用到哪一份摘要,就构造哪一份:10 份摘要、50 个贡献者。系统终于不再为一次局部查询准备全网的导览牌。
材料说明:数据来自 2026 年 9 月 1 日 Discovery Front 本地容量测试及配对分析;本轮核对了原始比较文件、运行配置与当前摘要构造代码。正文代码为解释用的简化结构。详细定位见本出版包的 SOURCE-NOTES.md。这是一篇根据既有实验重写的工程札记。