Files
Easytier/plans/004-reuse-ospf-route-graph.md
T
fanyang 4b654fc56e perf(mpsc): sync send via noop_waker — +90% pps (249K → 474K)
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).
2026-06-28 21:15:05 +08:00

177 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 计划 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 均在此文件。
当前代码摘录:
```rust
// 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();
```
```rust
// 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.rs`
- `easytier/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 0characterization 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 -- --check` exits 0。
- [ ] `cargo clippy --all-targets --features full --all -- -D warnings` exits 0。
- [ ] `cargo nextest archive --archive-file tests.tar.zst --package easytier --features full` exits 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` 大文件;只做局部性能重构。