The async fn Future state machine overhead (~1.9us) dominated MpscTunnelSender::send, while RingSink operations were only ~40ns. Breakthrough: make send() an async fn that completes synchronously on the first poll for the direct (ring tunnel) path. Uses futures::task::noop_waker() to construct a dummy Context, then calls Sink trait methods (poll_ready, start_send, poll_flush) directly. RingSink always returns Ready immediately, so the waker is never invoked and the async fn completes without yielding. Channel mode (TCP/UDP/WG tunnels) still uses async send_async() with proper backpressure. Ring tunnels detected via tunnel_info() type check in PeerConn. Results (4 threads, 1400B, 15s): pps: 249K → 474K (+90%) send_msg_by_ip: 3.53us → 1.67us (-53%) send_msg_internal: 2.40us → 502ns (-79%) MpscTunnelSender::send: 1.97us → 144ns (-93%) All 207 peers:: tests pass. Netns-requiring tests (three_node, credential) unchanged (require root).
8.7 KiB
计划 004:复用 OSPF route-table 构图以减少拓扑更新成本
执行者说明:按步骤执行本计划。每一步都必须运行验证命令,并确认结果符合预期后再继续。如果触发“STOP 条件”中的任一情况,立即停止并报告,不要自行发挥。完成后更新
plans/README.md中本计划的状态行,除非 reviewer 明确说明由他们维护索引。漂移检查(首先运行):
git diff --stat 78146d16..HEAD -- easytier/src/peers/peer_ospf_route.rs easytier/src/tests如果本计划写成后任何范围内文件发生变化,继续前必须对照“当前状态”中的摘录与实时代码;如果不匹配,按 STOP 条件处理。
状态
- 优先级: P2
- 工作量: M
- 风险: MED
- 依赖: plans/003-avoid-noop-ospf-route-rebuilds.md
- 类别: perf
- 计划生成于: commit
78146d16, 2026-06-18
为什么重要
每次 OSPF 拓扑更新当前会分别为 least-hop 和 least-cost route table 调用 build_from_synced_info。每次调用都会从 synced info 重新构建 peer graph,并重新构建 peer/CIDR indexes。对于 peer 数和 proxy CIDR 数较大的 mesh,这把一次拓扑变化放大成两次完整构图和多次 map/trie 重建。完成后应保持 route selection 结果不变,同时复用同一份 graph/materialized synced view,减少 CPU 和分配成本。
当前状态
easytier/src/peers/peer_ospf_route.rs—update_route_table、graph builder、least-hop/least-cost map generation、CIDR trie rebuild 均在此文件。
当前代码摘录:
// easytier/src/peers/peer_ospf_route.rs:1628
// build next hop map
let (graph, start_node) =
Self::build_peer_graph_from_synced_info(my_peer_id, synced_info, cost_calc);
// easytier/src/peers/peer_ospf_route.rs:1649
if matches!(policy, NextHopPolicy::LeastHop) {
self.gen_next_hop_map_with_least_hop(&graph, &start_node, version);
} else {
self.gen_next_hop_map_with_least_cost(&graph, &start_node, version);
};
// easytier/src/peers/peer_ospf_route.rs:1655
let mut new_cidr_prefix_trie = PrefixMap::new();
let mut new_cidr_v6_prefix_trie = PrefixMap::new();
// easytier/src/peers/peer_ospf_route.rs:2453
fn update_route_table(&self) {
// ...
self.route_table.build_from_synced_info(
self.my_peer_id,
&self.synced_route_info,
NextHopPolicy::LeastHop,
calc_locked.as_ref().unwrap(),
);
self.route_table_with_cost.build_from_synced_info(
self.my_peer_id,
&self.synced_route_info,
NextHopPolicy::LeastCost,
calc_locked.as_ref().unwrap(),
);
}
仓库约定:core routing behavior must be preserved。先添加 characterization tests,再重构;不要在同一计划里改 route policy。
需要使用的命令
| Purpose | Command | Expected on success |
|---|---|---|
| Format | cargo fmt --all -- --check |
exit 0 |
| Lint | cargo clippy --all-targets --features full --all -- -D warnings |
exit 0, no warnings |
| Targeted tests | cargo test --package easytier peer_ospf_route --features full -- --nocapture |
exit 0; route characterization tests pass |
| Archive tests | cargo nextest archive --archive-file tests.tar.zst --package easytier --features full |
exit 0 |
临时目录约定
- 临时文件、scratch 目录和 disposable worktree 必须放在
$HOME/tmp下。 - 如果
$HOME/tmp不存在且本计划需要临时空间,先创建它。 - 不要把临时产物放进被修改仓库。
范围
范围内(只能修改这些文件):
easytier/src/peers/peer_ospf_route.rseasytier/src/tests/*(仅新增/调整 route-table characterization tests)
范围外(即使看起来相关也不要触碰):
- Route protocol schema and wire format。
- Credential/trust semantics。
- Peer center、foreign network manager、data-plane tunnels。
- Any UI or config surface。
Git 工作流
- Branch:
advisor/004-reuse-ospf-route-graph - Commit message style follows existing conventional commits, for example
refactor: introduce HedgeExt for task hedging; rewrite NatDstQuicConnector. - Do NOT push or open a PR unless the operator instructed it.
步骤
步骤 1:添加 route-table characterization tests
在修改实现前,新增测试覆盖至少一个包含以下元素的小拓扑:
- 本 peer、两个 reachable peers、一个 unreachable 或 outdated peer。
- 至少一个 IPv4 proxy CIDR 和一个 IPv6 proxy CIDR。
- least-hop 和 least-cost 结果不同或至少都被断言。
测试应断言当前 route_table 和 route_table_with_cost 对 peer next-hop、peer reachability、CIDR lookup 的结果。优先使用现有测试 helper;如果内部 API 不便,添加 #[cfg(test)] helper,不改变生产 API。
验证:cargo test --package easytier peer_ospf_route --features full -- --nocapture → exit 0;新增 characterization tests 在重构前通过。
步骤 2:抽出一次性 graph build 输入
在 peer_ospf_route.rs 中把 build_from_synced_info 内部的 graph construction 拆成私有 helper,例如:
build_peer_graph_from_synced_info(...)已存在则复用。- 新增 small struct 持有
graph、start_node、version和后续 index rebuild 需要的 synced snapshot references。
不要改变 gen_next_hop_map_with_least_hop 或 gen_next_hop_map_with_least_cost 的算法。
验证:cargo test --package easytier peer_ospf_route --features full -- --nocapture → exit 0。
步骤 3:让 update_route_table 对两种 policy 复用 graph
把 update_route_table 改为在持有 cost_calculator read lock 时构建一次 graph/materialized input,然后分别对 self.route_table 和 self.route_table_with_cost 应用 least-hop / least-cost generation。
如果现有 RouteTable::build_from_synced_info 是唯一封装点,可以新增一个 sibling method,例如 build_from_prebuilt_graph(...),保持旧方法用于兼容 tests 或其他调用方。
验证:cargo test --package easytier peer_ospf_route --features full -- --nocapture → exit 0;characterization tests 仍通过。
步骤 4:避免重复构建共享 indexes
检查 build_from_synced_info 中 peer info map、IPv4 map、CIDR tries 的生成是否依赖 policy-specific next-hop map。如果只依赖 reachability 或 synced info,可移动到共享 helper;如果依赖每个 RouteTable 自己的 next_hop_map,不要强行共享,避免改变 semantics。
允许分阶段收益:只共享 graph build 也可完成本计划;共享 CIDR/index 只有在 characterization tests 能证明 behavior 不变时才做。
验证:cargo test --package easytier peer_ospf_route --features full -- --nocapture → exit 0。
步骤 5:运行完整相关门禁
验证:
cargo fmt --all -- --check→ exit 0。cargo clippy --all-targets --features full --all -- -D warnings→ exit 0。cargo nextest archive --archive-file tests.tar.zst --package easytier --features full→ exit 0。
测试计划
- 新增 route-table characterization tests,先在重构前证明现有行为,再在重构后保持通过。
- 测试覆盖 least-hop、least-cost、CIDR lookup、unreachable peer exclusion。
- 如果可行,加入一个轻量 counter/helper 在 test-only path 确认 graph builder 调用次数从 2 降为 1;如果这需要侵入生产代码,则不要做。
完成标准
update_route_table不再对同一 synced topology 构建两次 peer graph。- least-hop 和 least-cost route outputs 与 characterization tests 中的旧行为一致。
- 没有改变 routing protocol、credential trust 或 config behavior。
cargo fmt --all -- --checkexits 0。cargo clippy --all-targets --features full --all -- -D warningsexits 0。cargo nextest archive --archive-file tests.tar.zst --package easytier --features fullexits 0。- 没有修改范围外文件。
- 已更新
plans/README.md中本计划的状态行。
STOP 条件
plans/003-avoid-noop-ospf-route-rebuilds.md未完成,且当前 route rebuild trigger 仍会对 no-op payload 重算。- 复用 graph 需要改变 least-hop 或 least-cost algorithm。
- 现有代码让
cost_calc在两次 build 之间发生有意状态变化;如果确认begin_update/end_update依赖两次独立 build,停止并报告。 - Characterization tests 无法稳定构造 route-table expected outputs。
维护说明
- reviewer 应重点审查是否在锁持有期间引入更长 critical section。
- 未来新增 route policy 时应复用本计划抽出的 prebuilt graph input,而不是再调用完整
build_from_synced_info。 - 本计划不拆分
peer_ospf_route.rs大文件;只做局部性能重构。