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

8.3 KiB
Raw Permalink Blame History

计划 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 发送

当前代码

// 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
}
// 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 检查。

// 改前
} 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。

// 改前
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.rseasytier/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 入口取一次时间戳,传入子函数。

let now = quanta::Instant::now();
// ...
metrics.record_tx_with_time(dst_peer_id, packet_type, msg_len, now);

涉及文件easytier/src/peers/peer_manager.rseasytier/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%。

方案:在 PeerConnPeer 层引入 batch buffer,攒满 N 个包后一次 send(使用 try_send 或 unbounded channel)。

涉及文件easytier/src/peers/peer_conn.rseasytier/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::ArrayQueuetokio::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%)。

验证方法

# 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。需充分测试。