Files
Easytier/plans/006-send-chain-cpu-optimization.md
fanyang 79035ea972 perf(hotpath): add cpu_hotspot_ring bench and send-chain optimization plan
- Add measure_all to PeerMap and CidrSet impl blocks (hotpath::measure_all)
- Add [profile.hotpath] for samply-compatible builds (strip=false, debug=line-tables-only)
- Add cpu_hotspot_ring example: 2-node ring tunnel with data-plane flooding (~234K pps)
- Add plans/006-send-chain-cpu-optimization.md based on hotpath+samply 423M sample analysis
  Key findings: dashmap redundancy (14.9%), metrics overhead (8.3%), mpsc (14.1%)
  Target: reduce send_msg_internal from 3.26us to ~2us per packet
2026-06-28 12:45:05 +08:00

201 lines
8.3 KiB
Markdown
Raw Permalink 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.
# 计划 006send_msg_internal 发包链路 CPU 优化
> **执行者说明**:按步骤执行本计划。每一步都必须运行验证命令,并确认结果符合预期后再继续。完成后更新 `plans/README.md` 中本计划的状态行。
## 状态
- **优先级**: P1
- **工作量**: M
- **风险**: LOW
- **依赖**: hotpath profiling infra (main branch commit `be2034dd`)
- **类别**: performance
- **数据来源**: hotpath-cpu samply 423,583,601 samples4 threads234K ppspkt_size=1400
## 为什么重要
`send_msg_internal` 是数据面包转发的核心路径,每包耗时 3.26µs(wall time)。在 234K pps 下占 wall time 的 ~70%。samply inclusive CPU 分解显示有多处可通过减少冗余操作来省 µs 级开销。每包省 1µs 即可将吞吐提升 ~30%。
## 数据基线
### timingwall time,含 await
| Function | Calls | Avg/包 | 级差 |
|---|---|---|---|
| `send_msg_internal` | 6.9M | 3.26µs | — |
| └─ `send_msg_directly` | 6.9M | 2.83µs | 0.43µs(路由决策) |
| └─ `Peer::send_msg` | 6.9M | 2.69µs | 0.14µsconn 选择) |
| └─ `PeerConn::send_msg` | 6.9M | 2.58µs | 0.11µssession 选择) |
### samply inclusive CPUsend_msg_internal 子树,11.5M samples
| % | Function | 含义 |
|---|---|---|
| 12.0% | `PeerMap::send_msg_directly` | 发包核心 |
| 7.5% | `tokio::mpsc::Sender::send` | mpsc 通道 |
| **7.1%** | **`TrafficMetricRecorder::record_tx`** | 每包流量统计 |
| **6.1%+4.8%+4.0%** | **`dashmap::get` ×3** | 冗余 dashmap 查询 |
| 5.6% | `batch_semaphore::Acquire::poll` | mpsc permit |
| **3.9%** | **`quanta::get_now`** | 时间戳获取 |
| 3.9% | `malloc` | 内存分配 |
| **1.2%** | **`TrafficCounters closure`** | 流量计数器 |
| 1.0% | `MpscTunnelSender::send` | tunnel 发送 |
## 当前代码
```rust
// easytier/src/peers/peer_manager.rs:1533-1588
async fn send_msg_internal(
peers: &Arc<PeerMap>,
foreign_network_client: &Arc<ForeignNetworkClient>,
relay_peer_map: &Arc<RelayPeerMap>,
direct_tx_metrics: Option<&Arc<TrafficMetricRecorder>>,
msg: ZCPacket,
dst_peer_id: PeerId,
) -> Result<(), Error> {
// ...
let send_result = if ... {
// relay path
} else if peers.has_peer(dst_peer_id) { // dashmap get #1 (contains_key)
peers.send_msg_directly(msg, dst_peer_id).await // 内部 get_peer_by_id = dashmap get #2
} else if foreign_network_client.has_next_hop(dst_peer_id) {
// foreign network path
} else if let Some(gateway) = peers.get_gateway_peer_id(dst_peer_id, policy.clone()).await {
if peers.has_peer(gateway) || ... { // dashmap get #3
relay_peer_map.send_msg(msg, dst_peer_id, policy).await
}
}
if send_result.is_ok() && let Some(metrics) = direct_tx_metrics {
metrics.record_tx(dst_peer_id, packet_type, msg_len).await; // 每包记录
}
send_result
}
```
```rust
// easytier/src/peers/peer_map.rs:136-164
pub async fn send_msg_directly(&self, msg: ZCPacket, dst_peer_id: PeerId) -> Result<(), Error> {
if dst_peer_id == self.my_peer_id {
// self-send path (tokio::spawn)
return Ok(());
}
match self.get_peer_by_id(dst_peer_id) { // dashmap get (重复)
Some(peer) => peer.send_msg(msg).await?,
None => return Err(Error::RouteError(...)),
}
Ok(())
}
```
## 优化项
### 步骤 1:合并 dashmap 冗余查询(P0,预期省 ~0.1-0.2µs/包)
**问题**happy path 上 `has_peer(dst_peer_id)` + `send_msg_directly → get_peer_by_id(dst_peer_id)` 对同一个 key 做了 2 次 dashmap 查询。每次 ~100nshash + shard read lock)。
**方案**:在 `send_msg_internal` 中直接调 `get_peer_by_id`,根据 `Option<Arc<Peer>>` 分支,跳过 `has_peer` 检查。
```rust
// 改前
} else if peers.has_peer(dst_peer_id) {
peers.send_msg_directly(msg, dst_peer_id).await
}
// 改后
} else if let Some(peer) = peers.get_peer_by_id(dst_peer_id) {
peer.send_msg(msg).await
}
```
注意:`send_msg_directly` 中的 self-send 分支(`dst_peer_id == my_peer_id`)需要在上层处理或保留。当前 bench 场景 `dst_peer_id != my_peer_id`,不触发 self-send。
**涉及文件**`easytier/src/peers/peer_manager.rs:1558-1559`
**冲突检查**advisor/001-002 改过此文件(队列背压 + metrics 连带),需 rebase 后确认行号。
**验证**`cargo test -p easytier -- send_msg_internal`
### 步骤 2TrafficMetricRecorder 降频记录(P1,预期省 ~0.25µs/包)
**问题**`record_tx` 每包都调用,占 inclusive CPU 的 7.1% + TrafficCounters 1.2% = 8.3%。内部做 histogram 记录(`hdrhistogram::record_n_inner`)和时间戳获取(`quanta::get_now`)。
**方案**:在 `TrafficMetricRecorder` 中引入 per-thread atomic 计数器,每 N 包(如 64)或每 T ms 刷入 histogram。
```rust
// 改前
metrics.record_tx(dst_peer_id, packet_type, msg_len).await;
// 改后
metrics.record_tx_fast(dst_peer_id, packet_type, msg_len); // sync, atomic counter
// 内部: counter.fetch_add(msg_len); if counter % 64 == 0 { flush_to_histogram() }
```
**涉及文件**`easytier/src/peers/traffic_metrics.rs``easytier/src/peers/peer_manager.rs:1584`
**冲突检查**traffic_metrics.rs 零冲突。peer_manager.rs 同步骤 1。
**验证**`cargo test -p easytier -- traffic_metrics`
### 步骤 3:缓存时间戳(P2,预期省 ~0.13µs/包)
**问题**`quanta::get_now` 占 inclusive CPU 的 3.9%。send_msg_internal 路径上多处获取当前时间(record_tx 内部、traffic counters 等)。
**方案**:在 `send_msg_internal` 入口取一次时间戳,传入子函数。
```rust
let now = quanta::Instant::now();
// ...
metrics.record_tx_with_time(dst_peer_id, packet_type, msg_len, now);
```
**涉及文件**`easytier/src/peers/peer_manager.rs``easytier/src/peers/traffic_metrics.rs`
**冲突检查**:同步骤 2。
**验证**bench pps 对比。
### 步骤 4mpsc batch sendP3,预期省 ~0.46µs/包)
**问题**`PeerConn::send_msg` 每包做 1 次 `MpscTunnelSender::send`,触发 mpsc `Sender::send` (7.5%) + `batch_semaphore::Acquire::poll` (5.6%) + `add_permits_locked` (3.82%) = 16.9%。
**方案**:在 `PeerConn``Peer` 层引入 batch buffer,攒满 N 个包后一次 `send`(使用 `try_send` 或 unbounded channel)。
**涉及文件**`easytier/src/peers/peer_conn.rs``easytier/src/tunnel/mpsc.rs`
**冲突检查**peer_conn.rs 被 advisor/001-002 改过。mpsc.rs 被 perf/001 改过。需要协调合并顺序。
**验证**bench pps 对比 + `cargo test -p easytier -- peer_conn`
### 步骤 5ZCPacket 池化(P4,预期省 ~0.21µs/包)
**问题**:每包 malloc 3.9% + free 1.2% + morecore 1.2% = 6.3%。全局 munmap 4.73% 也部分来自此。
**方案**:对 ZCPacket 引入池化(`crossbeam-queue::ArrayQueue``tokio::sync::Pool`)。
**涉及文件**`easytier/src/tunnel/packet_def.rs`
**冲突检查**packet_def.rs 被 perf/001-003 改过。需要在 perf PR 合并后实施。
**验证**bench pps + `cargo test -p easytier -- packet`
## 预期总收益
| 步骤 | 每包省 | 累计 |
|---|---|---|
| 步骤 1dashmap 合并) | ~0.15µs | 3.26→3.11µs |
| 步骤 2metrics 降频) | ~0.25µs | 3.11→2.86µs |
| 步骤 3(缓存时间戳) | ~0.13µs | 2.86→2.73µs |
| 步骤 4batch send | ~0.46µs | 2.73→2.27µs |
| 步骤 5packet 池化) | ~0.21µs | 2.27→2.06µs |
| **合计** | **~1.2µs** | **3.26→2.06µs-37%** |
在 4 threads 配置下,预期 pps 从 234K 提升到 ~320K-370K+37%-58%)。
## 验证方法
```bash
# baseline(当前 main + measure_all
export PATH=$HOME/.cargo/bin:$PATH
cargo run --profile hotpath --features hotpath,hotpath-cpu --example cpu_hotspot_ring
# 记录 pps 和 timing avg
# 每个步骤实施后重跑,对比 pps 和 send_msg_internal avg
```
## 风险
- **步骤 1**:改变路由决策逻辑的边界条件(self-send、foreign network)。需确保不破坏 `send_msg_internal_*` 测试。
- **步骤 2**:metrics 精度降低(从每包精确变为每 64 包近似)。需确认 stats 查询端能接受。
- **步骤 4**batch send 引入延迟(攒批期间包等待)。需设置 flush timeout。
- **步骤 5**:ZCPacket 池化改变生命周期模型,可能引入 use-after-free。需充分测试。