3、测量与基准:如何精确测量延迟?

做市系统里,延迟测量是个老生常谈却又容易翻车的话题。

我见过不少团队,优化了半天,结果发现测量方法本身就是错的。你想想看,如果连基准都不准,那优化方向很可能南辕北辙。

这一章,我们就来聊聊怎么把延迟测准。说白了,就是三个层次:硬件时间戳、内核旁路时间戳、以及内核路径追踪。

3.1 硬件时间戳:PTP 的玩法

先讲最准的——硬件时间戳。我个人习惯,只要条件允许,优先用 PTP(Precision Time Protocol)。

为什么?因为软件打时间戳,误差动辄几微秒。而 PTP 在网卡硬件层面打戳,精度能到纳秒级。

核心思路:网卡在报文进出物理层时,直接打上硬件时间戳。这个时间戳来自 PTP 同步后的时钟域,误差主要取决于晶振漂移和网络对称性。

我在项目中遇到过一个问题:两台机器用 PTP 同步后,时间戳偏差还是偶尔跳到 500 纳秒以上。排查了半天,发现是交换机上的 PTP 透明时钟没开对。嗯,这里要注意,PTP 的精度高度依赖网络设备的支持。

配置 PTP 的典型步骤:

# 安装 linuxptp 工具包
sudo apt-get install linuxptp

# 启动 ptp4l(主时钟模式)
sudo ptp4l -i eth0 -m -S

# 启动 phc2sys(将硬件时钟同步到系统时钟)
sudo phc2sys -s eth0 -c CLOCK_REALTIME -m

验证同步精度:

# 查看 PTP 偏移量
sudo pmc -u -b 0 'GET CURRENT_DATA_SET'

小技巧:ethtool -T eth0 查看网卡是否支持硬件时间戳。如果输出里没有 hardware-transmithardware-receive,那这网卡玩不了 PTP。

3.2 DPDK 的 rte_rdtsc():用户态的时间戳

如果你用 DPDK 做高频交易,那 rte_rdtsc() 应该是老朋友了。它直接读取 CPU 的 TSC 寄存器,开销极低——大概几十个 CPU 周期。

但这里有个坑:TSC 频率可能不稳定。我记得有一次,系统在睿频状态下,TSC 频率跳变了,导致我算出来的延迟忽高忽低。后来我强制锁频,才解决了这个问题。

使用示例:

#include <rte_cycles.h>

uint64_t start, end;
double latency_ns;

start = rte_rdtsc();
// 这里放你要测量的操作
end = rte_rdtsc();

// 转换为纳秒
latency_ns = (double)(end - start) / rte_get_tsc_hz() * 1e9;

警告:不要在生产环境里频繁调用 rte_rdtsc()。虽然它开销低,但每次调用都会刷新流水线。我建议只在关键路径上打戳,比如收包和发包的瞬间。

3.3 eBPF 程序追踪内核路径

有时候,我们需要知道报文在内核里到底走了哪些弯路。这时候 eBPF 就派上用场了。

eBPF 可以挂载到内核的各个函数入口和出口,精确记录时间戳。我常用它来追踪 __netif_receive_skb_coreip_rcv 这些关键函数。

一个简单的 eBPF 程序示例:

#include <linux/bpf.h>
#include <bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 1024);
    __type(key, u32);
    __type(value, u64);
} timestamps SEC(".maps");

SEC("kprobe/__netif_receive_skb_core")
int trace_entry(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&timestamps, &pid, &ts, BPF_ANY);
    return 0;
}

SEC("kretprobe/__netif_receive_skb_core")
int trace_exit(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 *start = bpf_map_lookup_elem(&timestamps, &pid);
    if (start) {
        u64 delta = bpf_ktime_get_ns() - *start;
        bpf_printk("内核路径耗时: %lld ns\n", delta);
    }
    return 0;
}

避坑指南:我曾经用 eBPF 追踪 tcp_v4_rcv,结果发现每次测量结果都偏大。后来才意识到,eBPF 程序本身也会占用 CPU 时间。解决办法是:在测量代码里减去 eBPF 自身的开销,或者用 tracepoint 替代 kprobe,因为 tracepoint 的开销更小。

3.4 搭建延迟基准测试环境

好了,工具都齐了。怎么搭一个靠谱的基准测试环境?

我的建议是:用两台机器直连,中间不要经过交换机。一台发报文,一台收报文并打时间戳。

硬件清单:

  • 两台服务器,CPU 支持 TSC 恒定频率(Intel 的 Invariant TSC)
  • 支持 PTP 的网卡(比如 Intel X710、Mellanox ConnectX-5)
  • 直连光纤或 DAC 线缆

软件配置:

  1. 关闭 CPU 睿频和节能模式(cpupower frequency-set -g performance
  2. 关闭中断合并(ethtool -C eth0 rx-usecs 0 tx-usecs 0
  3. 绑定网卡中断到特定 CPU 核心

测试流程:

# 发送端:以固定速率发送 UDP 报文
sockperf pp --tcp -i 192.168.1.2 -p 12345 -m 64 -t 10

# 接收端:记录每个报文的时间戳
sockperf sr --tcp -i 192.168.1.2 -p 12345 --time-stamp -m 64

个人经验:第一次跑基准测试时,先跑 100 万次,然后看延迟分布。不要只看平均值,P99 和 P99.9 才是关键。我曾经优化了半天平均值,结果 P99.9 反而变差了——因为优化手段引入了偶发的抖动。

3.5 知识体系总览

下面这张图,概括了本章的核心逻辑:

延迟测量技术栈 硬件时间戳 PTP (IEEE 1588) 网卡硬件打戳 精度:纳秒级 用户态时间戳 rte_rdtsc() 读取 TSC 寄存器 精度:几十纳秒 内核路径追踪 eBPF kprobe/tracepoint 函数级耗时分析 精度:微秒级 基准测试环境 直连架构 · 关闭睿频 · 关闭中断合并 · 绑定 CPU 输出:延迟分布(P50 / P99 / P99.9)

这张图展示了三个测量层次的关系。硬件时间戳最准,但依赖网卡支持。用户态时间戳灵活,但要注意 TSC 频率。内核追踪能帮你定位问题,但本身有开销。

实际项目中,我通常三管齐下:先用 eBPF 找出热点路径,然后用 rte_rdtsc() 做细粒度测量,最后用 PTP 做端到端验证。这样测出来的数据,才敢拿去指导优化。

最后说一句:测量本身也是一种开销。不要为了测量而测量,搞清楚你要优化什么,再选合适的测量工具。否则,你只是在制造数据,而不是在解决问题。


公众号:蓝海资料掘金营,微信deep3321