← 返回文章发现September 19, 202615 分钟阅读Copy article

Kitsune2:查找速度提升 251 倍,更新路径缩短 33 倍,信息传播提前 15 倍

在 Holochain 网络层的一天实验中:节点如何寻找同伴、获取缺失数据、恢复信息传播,以及 6 小补丁在此过程中如何改变了什么。

Developed by Robert E. Beckner III (Merlin) | rbeckner.com

我最近在 4:30 a.m. 之前醒来,正值日出前,享受着这一时刻。 9 月 19 日,我利用那天早晨开始了 Kitsune2 的实验室工作,Kitsune 是 Holochain 底层的点对点网络层,并寻找它花费时间的地方。到 12:20 p.m.,实验室已产生 6 个补丁。 端点查找:251× 更快,在 10,000 个保留身份上。 调用它们的对等更新路径:33× 更快. 完成获取后信息传播启动:3,002 毫秒降至 202 毫秒. 突发请求流量:96.9% 更少的消息. 3 的更改修复了不必要等待或等待永远不会发生的事情的工作。我已经编码超过 25 年,并自 GPT-3 起使用 AI 进行构建。 我用它来隔离我关心或尚未理解的事物,深入研究,并测试我们认为已学到的内容。 我喜欢那部分:将问题转化为实验并观察哪些结论成立。 今晨,我用 AI 编码代理将该方法应用于让 Holochain 节点保持彼此同步的机制。Holochain 应用在参与的计算机或 节点 之间共享数据。 每个参与者保留自己的签名历史,并帮助验证和存储应用共享数据的一部分。 那个共享存储是 分布式哈希表,或 DHT。副本保存在多个节点上,因此当其作者离线时数据仍可用。 Holochain's DHT guide 解释了这些职责如何分配。Kitsune2 处理此底层的点对点通信。 想象一个节点赶上进度:它学习可以到达的节点位置,比较自己持有的数据与它们持有的数据,并请求缺失的片段。 Gossip 是节点发现差异的交换方式;fetch 检索缺失的操作,Kitsune2 传输的数据信息单元。 接收节点将它们交给其宿主的操作存储。 Holochain 处理它们的验证和集成。
Diagram source
graph TD
    A[学习节点身份和端点] --> B[通过 gossip 比较数据]
    B --> C[识别缺失的操作]
    C --> D[从节点获取操作]
    D --> E[将接收的操作交给宿主存储]
    E --> F[发出已完成待处理获取的信号]
    F e1@--> B
    e1@{ animation: fast }
Copy source
这些补丁沿着该序列排列。 有些减少节点的工作量;其他则在工作已完成时让其继续。 上述数字衡量了本地测试节点上这些特定步骤的情况。

每个6更改实际上移动了什么#

我们将起点固定在上游提交 06555ee,开发源 0.6.0-dev.1。 每个比较使用该基线加上记录的补丁序列。 下面的标签将发现与我们的实验记录关联;所有6补丁现在已提交给上游审查。
标签变化内容测量结果
K3a在获取完成时调用现有的 drain helper完成获取后启动 Gossip:3,002 → 202 毫秒
K3b接收器锁限定在出队有效并发为 1。配置的工作者现在真正重叠
K3c生成标签化的入场所有权取消调用者的 strand 0 待定键。基线 stranded 8
K3d-P1基于集合的响应清理1.9–6.1× 在大规模待处理负载下更快的响应处理
K3d-P2选择同等对等批处理(默认 1)峰值:87.5–96.9% 更少的请求消息
K1将 URL 反向到代理索引在 10,000 保留身份时:更新 21–33×,查找 118–251× 更快,存储 3.10× 内存
第一次修复将获取完成与等待它的 gossip 任务重新连接。 该更改调用现有的辅助程序,告诉监听器待处理映射已清空。 我们测试了完成的获取和故意保持打开的获取。
Gossip initiation after a fetch completes, with the control that must not move (ms)
Chart datamilliseconds
Upstream 06555eeWith the drain notification restored
Fetch completed3001202
Fetch still pending (control)30023001
完成后,gossip 可以继续。 在获取仍待处理时,两个版本仍会触发回退计时器。 上述隔离测试测得 3,001 → 202 毫秒;后续组合重放测得 3,002 → 202 毫秒。 两者都使用了 3,000 毫秒的回退和 200 毫秒的初始延迟。 大约 15× 的差异记录了任务所走的路径:等待回退还是在完成时唤醒。

每个更改都位于一个 crate 中,且 5 的 6 在单个文件中#

一个 Rust crate 是代码包。 所有 6 补丁都位于核心 crate 中,该 crate 包含同伴记录和获取机制。 它们改变了 gossip 获取数据和完成信号的方式,而不编辑 gossip 算法或底层网络传输。 批处理添加了一个可选的核心配置字段。
标签文件行
K1core/src/factories/core_known_peers.rs+33 / −10
K3acore/src/factories/core_fetch.rs+1
K3bcore/src/factories/core_fetch.rs+11 / −3
K3ccore/src/factories/core_fetch.rs+71 / −53
K3d-P1core/src/factories/core_fetch.rs+6 / −1
K3d-P2core/src/factories/core_fetch.rs, 1个测试文件+89 / −17
5个补丁按记录顺序堆叠在 core_fetch.rs 上,要求缺失的操作并处理响应。 其余补丁位于 core_known_peers.rs,保留了哪个身份与哪个网络端点关联的记录。 随后我们将所有 6 应用于单个源树并一起运行。

数据已到达,但 gossip 仍在等待#

CoreFetch 保留它已请求的操作及其来源的映射。 当 peer 的响应到达且操作成功存储后,代码会从映射中移除这些标识符。 文件的其他部分,send-error 路径和 peer-cleanup 路径,在移除条目后会立即调用名为 notify_listeners_if_queue_drained 的辅助函数。 成功路径从未调用它。节点可以扩展其存储的 DHT 部分,称为其 storage arc. 为此,它需要在更大的范围内赶上数据。 在我们测试的 arc-growth 路径中,gossip 在发起与 peer 的请求前等待未完成的 fetch 完成,并以计时器作为后备。 数据已到达且待处理映射已清空,但等待该信号的任务仍然保持睡眠,直到计时器到期。
Diagram source
sequenceDiagram
    participant G as Gossip arc-growth loop
    participant F as CoreFetch
    participant P as Peer
    G->>F: Register a drain waiter
    P->>F: Final response, operations stored
    F->>F: retain() empties the pending map
    Note over F: helper exists, never called
    F--xG: Notification that never arrives
    G->>G: Initiate after the roughly 3-second fallback
Copy source
完成后检查隐藏了差异:新注册的 waiter 看到空映射并立即返回。 我们在完成前已注册了我们的 before 完成。 一旦数据被存储,那个等待者仍然没有收到原始代码的通知;通过补丁,它收到了一个。 这修复了接收数据与启动下一次聊天交换之间的交接。我们还检查了部分响应、存储错误、多个监听器,以及监听器在请求完成或新工作到达时注册。 部分响应必须使等待者保持挂起;只有完成最后一次未完成的获取才会唤醒它。

5 工作器已配置,且 1 实际上正在发送#

CoreFetch 的出站端运行配置数量的工作任务,从一个共享通道中拉取。 循环写成这样:rustCopy source
while let Some(..) = outgoing_request_rx.lock().await.recv().await {    // send the request}
锁允许 1 工作器从共享队列中获取下一个请求。 但 Rust 通过整个 while let 的主体保持该临时锁守护,包括网络发送的等待。 当 1 工作器正在发送时,其他 4 甚至无法收集它们的下一个请求。 所有请求仍然最终会到达,因此交付测试可能通过,而配置的并发度未被使用。我们将接收移入一个短块,以便在发送开始前释放锁。 工作器仍轮流收集请求,然后并发发送。 慢速发送不再在其他工作器已准备好请求时持有队列锁。
Outgoing sends in flight at the same moment, by configured worker count
Chart dataconcurrent sends observed
Upstream 06555eeDequeue-scoped guard
1 worker11
2 workers12
5 workers15
测量在暂停计时器的情况下保持每个发送打开,并计数在任何释放之前已进入的数量,然后确认没有超过配置上限的进入。

取消调用者会让节点等待 8 未发送的操作#

一个 fetch 请求通过 2 记录:挂起映射说明节点正在等待什么,外发队列保存发送工作者的工作。 request_ops,该方法请求操作,先将整个批次添加到挂起映射,然后等待队列有空间。如果调用者在等待容量时被取消,一些操作已被标记为挂起,尽管没有工作者接收它们。 我们填满队列,取消一个被阻塞的调用者,并让所有 16,385 已被接纳的请求完成。 8 操作仍保持挂起。 它们从未进入外发队列,因此没有响应在其路径上清除它们。这直接与第一个修复相关:完成通知无法在映射仍包含未完成工作时触发。 在这里,节点自己的簿记使其保持等待。补丁首先保留队列容量,然后记录并排队操作,且在这两步之间没有另一次 await. 每个条目还携带一个生成标签,因此来自旧失败发送的清理不会擦除同一操作的新请求。 已被接纳的工作在调用者取消后仍然存活;仍在等待接纳的工作不会留下挂起条目。这修复了请求的所有权。 总内存限制仍是一个单独的设计问题,因为调用者仍可提供大批量。

请求消息更少,每条消息大小有限制#

一个赶上进度的节点可能需要来自同一对等体的许多操作。 Kitsune2的请求格式已支持标识符列表,但出站代码每条消息发送1. 在合并这些请求之前,我们检查了更大响应的处理方式。对于每个挂起操作,响应清理扫描返回的标识符列表,查看是否可以移除该条目。 有8,192个挂起条目和1,024个返回标识符时,这会重复大量搜索。 构建返回标识符的HashSet可使每次成员检查变得廉价;在这种情况下,响应处理速度提升了6.1×。 仅有16个返回标识符时,存储数据占主导,变化带来的收益很小。然后我们合并同一对等体的就绪请求。 在一次64个小操作的突发中,批大小为8和32,分别将请求消息减少了87.5%和96.9%.
Burst behaviour relative to the default batch size of 1 (percent of control)
Chart datapercent of control
Request messagesElapsed time
Batch 1 (control)100105.4
Batch 812.572.6
Batch 323.167.7
突发大约在27–32%之前完成。 同样的操作仍需传输和存储;节省在于携带它们的请求数量。 batch-1 控制测量了基线经过时间的105.4%,显示即使批处理未激活也有大约5%的时间漂移。工作者只合并已就绪的请求,因此单个请求永不等待批处理填满。 批拆分同时使用配置的标识符限制和4,096-字节编码请求阈值. 仅计数项目并不能限制其总大小;使用200个标识符的测试将它们拆分为若干受限消息。 单个标识符仍会被发送,即使它单独超过阈值,因此这不是一个通用的线缆尺寸限制。 响应携带实际数据,需要在响应节点上设置单独的大小限制。候选人保持批量大小 1 为默认值,并接受现有配置不变。 我们保留了更大批量的可选项,因为单独请求的测试产生了不一致的尾部延迟结果,已在下文说明。

将扫描移出更新路径值得33×,并且在查找本身上值得251×#

CoreKnownPeers 记住身份及其关联的网络端点。 多个身份可以共享一个端点,且被阻止的身份在从活动对等存储中移除后仍需可识别。 10,000 保留身份意味着此存储中的身份历史,而不是 10,000 电脑同时连接。在对等存储更新时,访问控制询问哪些身份属于该端点。 查找,get_by_url,每次都在锁定下扫描整个保留映射。 随着该历史增长,更新当前对等信息变得更昂贵。我们添加了从端点 URL 到其关联身份的索引:HashMap<Url, HashSet<AgentId>>。原始记录保持权威。当身份更改端点时,索引将其移动到相应集合;查找随后可直接进入该集合。两个版本均通过了覆盖更新、陈旧广告、移除和共享端点的 19,422 结果检查。
Microseconds per 16-advertisement update call (log scale)
Chart datamicroseconds per update call
retained identitiesUpstream linear scanDerived URL index
1006.8574.911
1,00018.1034.774
10,000154.8494.72
上述每个定时调用插入 16 广告并运行访问控制监听器。 在 10,000 保留身份时,耗时从约 155 µs 降至 4.72 µs,获得配对 33× 的提升。 由于这些监听器不再扫描增长的历史,索引版本在这些规模下保持几乎平坦。在 API 处测量而非通过调用者,查找本身从扫描变为哈希。 在 10,000 保留身份时,未命中从 11,423 ns 降至 45 ns,配对 251×,单次命中从 9,309 ns 降至 79 ns,118×. 返回许多身份的查询仍按其返回量扩展,因此共享-URL 情况获得 4.4× 的提升,而非两位数的量级。 251× 是孤立的查找结果。 33× 衡量使用它的更大更新调用;这些提升不能相加或相乘。
Requested store memory at 10,000 retained identities, by endpoint shape
Chart datamegabytes
UpstreamWith the index
Mixed endpoints1.23.71
All shared1.21.74
All URL-less1.21.2
3.10× 在混合存储上,多出 2.51 MB 于 10,000 身份。 当每个身份共享一个端点时,派生映射很小,成本降至 1.45×. 当没有身份拥有 URL 时,没有可派生的内容,成本为零。 这些数字是分配器请求的字节数,用于保留存储,而非总节点内存。

将所有 6 补丁组合在一起,节点即可完成并继续#

组合回放使用 Kitsune2 的真实 gossip、fetch、peer-access 和 memory-store 组件,通过其内存测试传输进行通信。 我们在原始和已补丁的源树上运行了一个 5 步骤序列:
  1. 更新对等信息。 身份更改端点。 后来到达的陈旧信息会保留较新的记录,已被阻止的身份仍可被访问控制解析。 两个版本均通过;索引保留现有决策。
  2. 在 fetch 完成后启动 gossip。 已补丁节点在 202 毫秒后启动。 原始节点在 3,002 毫秒时采用回退路径。
  3. 接收乱序数据。 3 请求的操作以 4 响应到达,包括一个重复项。 两个版本均一致存储它们,并将字节计数与正确的操作关联。
  4. 完成 fetch。 已存储的操作可被检索。 已补丁节点也唤醒在这些操作待处理时注册的监听器。
  5. 在对等失败后继续。 控制的发送失败会清除该对等的请求状态。 另一个对等提供操作,待处理映射清空,随后一次 fetch 成功。 在原始版本中,完成通知检查失败。
单独的交互测试将取消与并发发送以及组合批处理与完成通知结合。 修复保持一致:已录入的工作完成,已取消的录入未留下孤立条目,监听器在最终响应后唤醒。这使我们达到了数据进入主机存储并能够进行下一步网络操作的阶段。 Holochain 的验证和集成仍然遵循实验室外的流程。 我们尚未测量这些变化如何缩短真实节点的完整同步时间。

每项收益的成本#

索引占用内存并使端点更改更昂贵。 在原始小存储测试中,将固定集合的身份更新为新 URL 的速度为 0.946× 基线速度,保留身份为 100. 在 10,000,单独的端点更改写操作成本为 3.68×. 维护额外索引有成本;查找节省必须偿还它。由于这种权衡是真实的,我们寻找索引失效的工作负载。 36 配置在测量前已预声明,432 配对运行已执行,结果转录在变体之间哈希相同。
Paired speedup across the 36-config workload envelope, by mix (log scale)
Chart datapaired speedup
retained identitiesSteady, no rebind100% rebindShared endpointsURL-less heavy
1001.611.471.421.09
1,0007.26.264.122.13
10,00055.5636.425.5512.16

Reference line, parity: 1

该更广泛矩阵中的每个工作负载都有超过平价的中位数,包括每个接受的更新都更改端点的混合:1.47× 在 100 身份和 36.42× 在 10,000. 这些更新也触发访问控制查找,其节省超过了额外写入。 这更广泛的工作负载与早期固定集合测试不同;其正面结果使该小存储回归保持记录。100 身份、主要 URL-less 情况仍然 不确定:其 1.09× 中位数包含一个低于平价的配对结果。 我们仍需要来自真实节点的跟踪,显示保留身份计数、端点更改和查找频率,然后才能为特定工作负载推荐索引。并发发送暴露了清理交互。 一旦工人可以重叠,失败发送的清理可能在另一请求到同一对等体时进行。 这使得现有的对等体范围清理比失败的工作更广泛。 所有权补丁将删除范围缩小到仍属于该请求生成的条目。 一起测试补丁在此处很重要。批处理单请求延迟仍不确定。 我们测量了 p95,即在此时间内 95% 的请求完成。 对于单独发送的请求,工人的最终运行显示 1.01–1.08× 基线 p95;独立重跑显示 1.44–1.58×. 这些装置需要数十微秒,它们的控制也漂移了。 该突发改进得以复制,但单请求结果未能稳定,因此更大的批量仍需选择加入。

反复比较和停滞发送仍然开放#

我们还检查了 2 同一补齐序列的其他部分。 两者都未产生可接受的补丁。比较同行已持有的内容 (K4)。 Gossip 使用存储数据的紧凑摘要来发现差异。 在最大的测试中,生成并比较未更改的快照产生了 262,144 个哈希;重复 10 次每次产生相同输出。 重用先前的摘要看起来很吸引人,但主存储可能会在没有可靠信号来使该保存结果失效的情况下更改。 缓存的摘要可能会掩盖同行之间的差异。 我们评估的替代方案也未能达到在内存存储上所需的 20% 时间节省。 来自真实 Holochain 存储的测量是下一个缺失的部分。当 Gossip 发送停滞时恢复 (K2)。 读取源代码提出了关于发送持有锁,而超时任务也需要该锁的问题。 我们还需要确定如果该发送在途中被取消,底层 Iroh 传输流会发生什么。 代理提供者阻止了重现驱动程序的工作,因此运行时测试从未完成。 这仍然是一个未经测试的关注点,没有补丁或可靠性结果。

我们测量了什么,以及还有什么需要测量#

这些实验在开发源 0.6.0-dev.1 上运行,在 1 台共享 Apple M4 Max 上,配备 16 个逻辑核心。 我们使用本地测试节点和内存传输,不做核心绑定或第二台物理主机。 结果涵盖命名组件及其交互。 网络吞吐量、全网收敛、总节点内存和 Holochain 应用延迟仍未测量。该方法遵循我在 benchmark-driven development 上的工作。 我们记录了每个问题和接受阈值,在每次比较中保持源和夹具不变,并将命令、样本和失败记录在追加式收据中。对于3修复,同样的测试在原始代码上在预期断言处失败,在补丁后通过。 对于优化,两版都必须保持行为,然后测量必须满足声明的阈值。 定时运行交替顺序,并将编译保持在测量窗口之外。 监督代理在冻结源上重新运行接受命令,然后接受每个结果;单请求延迟的分歧保留在最终报告中。完整包包含204收据目录。 组合测试于 12:17 p.m. 被接受,终端结果于 12:20 p.m. 记录。最终关闭检查保留在 2:38 p.m.。所有 6 个补丁现已开放供上游审查: completion notification、worker concurrency、admission ownership、response cleanup、request batching 和 endpoint indexing。

关键归属#

由我。 实验室本身:我的 benchmark-driven 方法,针对 Kitsune2 以找出其耗时所在。由 GPT-6 Astra。 完成通知和入职所有权修复:诊断、补丁以及在原始代码上失败的测试。 它还根据实验室记录完善了本文。由 GLM-5.3。 工作者并发修复、响应清理与批处理、36 配置工作负载矩阵用于索引、DHT 摘要分析在开放 K4 问题背后,以及所有 6 补丁的综合回放。由 AI。 端点索引及其测量,一个监督代理在冻结源代码前重新运行每一次接受,并计入结果,以及本文的第一稿。保持的批评。共同达成。 文章遵循的跟进序列:节点学习其同伴,比较其持有的内容,获取缺失的内容,存储后继续,每个补丁放置在其改变的步骤。基于。 Holochain 核心团队的可替换组件工厂和内存传输,使实验室能够在真实 Kitsune2 组件中测试这些行为。#benchmarking#distributed-systems#rust#performance#AI#agents#open-source#experiential#insights—Robert E. Beckner III (Merlin)继续阅读上一页智能无法服从:判断、权威与拒绝所示下一页提示缓存错误可能让您的组织在每一次 AI 交互中几乎多花 6xQwen 3.8-27B 在 M1 Ultra 上:从 12.1 到 20.3 tok/s 通过测量一切发现August 14, 20268 分钟阅读MTPLX 在 M1 Ultra 上:25 tok/s 调优 那个服务了 16.5发现August 15, 20267 分钟阅读最后 CLI: 重建我工具所依赖的基地 51发现August 19, 202610 分钟阅读相关项目175-页中文手稿AI 创新AIA - AI顾问AI 创新