如何在香港服务器中通过 Ubuntu 20.04 启用 eBPF tracing,精准定位跨境网络延迟与抖动?

周五晚 23:40,北京业务侧的告警连续响了三轮:跨境 API 的 p99 延迟从 180ms 爬到了 700ms,抖动像心电图一样锯齿化。香港机房里风扇的白噪音像潮水,我盯着 NOC 屏幕,出口带宽并没打满——这意味着问题不只是“网不好”,而是“在哪里不好”。
我决定直接把“放大镜”插到内核里:用 eBPF 把从网卡收包、协议栈处理、队列排队、TCP 重传、RTT 采样的每个环节打点拉齐。只要数据闭环完整,跨境链路的“锅”就没地方躲。
1)环境与目标
目标:在 Ubuntu 20.04 上启用 eBPF 观测与排障链路,定位跨境访问的延迟来源与抖动根因(链路 vs 服务器本机栈/网卡/排队等),并给出针对性优化。
我的实机环境(真实可复现)
| 模块 | 参数 |
|---|---|
| 机房 | 香港(标准 42U 柜,双电双路) |
| 服务器 | Dell R650 / Xeon Silver 4314(16C/32T)/ 128GB RAM |
| 网卡 | Intel X710 10GbE(2 × 10G,固件 9.x),直连 ToR |
| 系统 | Ubuntu 20.04.6 LTS(Focal) |
| 内核 | 建议升级到 HWE 5.15(eBPF/BTF 更友好) |
| 业务 | Nginx + 自研 Go API,Docker 部署 |
| 需求 | 排查香港→大陆/北美的跨境访问 延迟 & 抖动 |
为什么要 HWE 5.15?
20.04 默认 5.4 内核也能跑 eBPF,但 5.15 在 BTF、tracepoints、kfunc 覆盖度上更舒服,工具链(bpftrace/BCC)体验明显更稳。
2)开箱:内核与工具准备
2.1 升级到 HWE 内核(可选但强烈推荐)
sudo apt update
sudo apt install -y linux-generic-hwe-20.04
# 重启后确认
uname -r # 预期类似 5.15.x-*-generic
2.2 安装 eBPF 工具链
sudo apt install -y bpfcc-tools bpftrace linux-headers-$(uname -r) \
llvm clang libbpf-dev build-essential iperf3 mtr-tiny ethtool iproute2
2.3 准备 BPF 文件系统与 BTF
# 挂载 bpffs
sudo mount -t bpf bpf /sys/fs/bpf
# 永久化
echo "bpf /sys/fs/bpf bpf defaults 0 0" | sudo tee -a /etc/fstab
# 检查 BTF 是否可用
ls /sys/kernel/btf/vmlinux # 存在即 OK
坑 1:Secure Boot/Lockdown
某些环境启用 Secure Boot 时,kprobe/bpftrace 可能被限制。若遇 permission denied 或 kprobe disallowed,在安全合规前提下关闭 Secure Boot,或按规范签名内核模块。
坑 2:容器网络命名空间
业务跑在 Docker 中时,bpftrace/BCC 默认看的是宿主机命名空间。可以通过 PID/NS 进入目标容器(nsenter -t <pid> -n)运行,或在程序里显式 attach 到指定 netns。
3)基线:先把“外因”跑出来(无内核探针)
先用 mtr/iperf3 做一轮不侵入基线,对比几个典型目的地(示例数据为当晚实测的一段窗口):
# 典型测试命令
mtr -c 100 -rwz example-cn-edge # 到国内边缘
mtr -c 100 -rwz example-us-west # 到美西
iperf3 -c example-cn-edge -t 60 -J # 吞吐/抖动粗看
| 目的地 | 平均 RTT | p95 RTT | p99 RTT | 丢包 | 备注 |
|---|---|---|---|---|---|
| CN-Edge (广州) | 45 ms | 110 ms | 290 ms | 0.2% | 抖动明显 |
| CN-Edge (上海) | 41 ms | 95 ms | 210 ms | 0.1% | 偶发尖刺 |
| US-West (LA) | 125 ms | 180 ms | 220 ms | 0.0% | 稳定 |
只看 mtr/iperf3,你会以为“链路就这样”。但p99 突刺到底是海外段拥塞、境内侧队列,还是我们本机软中断/排队出了问题?这就轮到 eBPF 上场了。
4)eBPF 观测的“分层法”
我把网络路径拆成 5 层,从外到内、从粗到细逐层收窄:
- 连接建立:SYN→SYN/ACK 的握手时延
- 往返时间 RTT 与 抖动分布
- 重传/丢包:是内核触发重传还是对端/链路问题
- 排队与软中断:qdisc/NAPI/softirq 是否飙高
- 网卡队列与中断亲和:单队列热点?中断绑核不当?
下面给出能“即抄即用”的工具与脚本。
5)连接建立:tcpconnect / tcpaccept
# 追踪 TCP connect(客户端向外发起)
sudo /usr/sbin/tcpconnect -p 0
# 追踪服务端接受连接
sudo /usr/sbin/tcpaccept
这些 BCC 工具能实时打印连接事件。抖动时段若出现大量短时失败/重试,会立即可见。
6)RTT 与抖动:tcprtt 或 bpftrace 脚本
6.1 BCC 自带 tcprtt(更省心)
sudo /usr/sbin/tcprtt -i eth0 # 按流输出 RTT 样本
输出里会显示远端 IP/端口与 RTT us 级样本,我通常把它重定向到文件再做分位数统计。
6.2 自写 bpftrace(可按目的 IP 聚合)
说明:不同内核的 struct sock/tcp_sock 字段有差异,5.15 下以下脚本可直接用;5.4 可能需要把 skc_daddr 替换为 inet_daddr。
保存为 rtt.bt:
#!/usr/bin/env bpftrace
#include <net/sock.h>
#include <net/tcp.h>
kprobe:tcp_rcv_established
{
$sk = (struct sock *)arg0;
$tp = (struct tcp_sock *)$sk;
$srtt = $tp->srtt_us >> 3; // 内核平滑 RTT(us)
$rtt_ms = $srtt / 1000;
$dport = (ntohs($sk->__sk_common.skc_dport));
$family = $sk->__sk_common.skc_family;
if ($family == AF_INET) {
$dip = $sk->__sk_common.skc_daddr;
@rtt_ms[ntop($dip), $dport] = hist($rtt_ms);
}
}
运行与观察直方图(按目的 IP:port):
sudo bpftrace rtt.bt
判读要点:
- 长尾(p99/p999)拉高且与重传/软中断尖刺同一时段出现,更像是本机或出口侧排队。
- RTT 直方图双峰,经常对应“路径切换/对端负载变化/跨运营商路由差异”。
7)重传与丢包:tcpretrans + kfree_skb_reason
7.1 看 TCP 重传
sudo /usr/sbin/tcpretrans # 重传事件,定位是否本端频繁重传
7.2 看内核“为什么丢包”(5.15 支持 kfree_skb_reason)
保存为 drop.bt:
#!/usr/bin/env bpftrace
tracepoint:skb:kfree_skb
{
@reasons[arg1] = count(); // arg1 是 reason(5.15 起)
}
interval:s:5
{
printf("---- drop reasons in last 5s ----\n");
print(@reasons);
clear(@reasons);
}
运行:
sudo bpftrace drop.bt
常见 reason:SKB_DROP_REASON_DEV_RX_BUSY(驱动繁忙)、...QDISC_DROP(队列丢弃)、...TCP_CSUM(校验失败)等。
这一步对区分链路丢 vs 本机丢很关键。
8)排队与软中断:qdisc、softirq、runqlat
8.1 qdisc 排队时间(发送路径)
net:net_dev_queue → net:net_dev_xmit 的间隔可以用 bpftrace 粗测(示例脚本,按 skb 指针追踪):
#!/usr/bin/env bpftrace
tracepoint:net:net_dev_queue
{
@ts[args->skbaddr] = nsecs;
}
tracepoint:net:net_dev_xmit
/@ts[args->skbaddr]/
{
$delta_us = (nsecs - @ts[args->skbaddr]) / 1000;
@qdisc_us = hist($delta_us);
delete(@ts[args->skbaddr]);
}
8.2 软中断 & CPU 运行队列
# BCC:软中断分布
sudo /usr/sbin/softirqs -d 1
# BCC:运行队列延迟(抖动常伴随 run queue 峰值)
sudo /usr/sbin/runqlat 1
判读要点:如果 NET_RX/NET_TX softirq 峰值与 RTT/重传同时段上扬,很可能是单队列打满或中断亲和不均导致的本机抖动。
9)网卡队列与中断亲和:RSS / RPS / XPS / NAPI
首先看网卡统计:
sudo ethtool -S eth0 | egrep "rx_|tx_"
cat /proc/interrupts | egrep "eth0|ixgbe|i40e"
优化动作(按需逐步启用、每次只改一项并回归验证):
# 开 RSS(多队列收包),通常 i40e/X710 默认开;检查并合理分配中断亲和
sudo apt install -y irqbalance
# 或手动绑核:写 /proc/irq/<irq>/smp_affinity_list
# 开启 RPS/XPS(软件侧收/发包多核扩展)
echo ffffff | sudo tee /sys/class/net/eth0/queues/rx-*/rps_cpus
echo 32768 | sudo tee /proc/sys/net/core/rps_sock_flow_entries
for q in /sys/class/net/eth0/queues/rx-*; do echo 32768 | sudo tee $q/rps_flow_cnt; done
# 适度中断合并(减少 CPU 抖动,但会增加微小时延;需压测验证)
sudo ethtool -C eth0 rx-usecs 50 rx-frames 0
# 出口队列:启用 fq + BBR(能有效“抑制尖刺”)
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
坑 3:过度合并(Coalesce)
rx-usecs 太大时,平均 RTT 可能反而升高,甚至出现批量化的“台阶抖动”。我的经验是 25~75us 区间逐步微调,边测边看 p95/p99。
10)一次“实战”排障复盘(当晚真实过程)
症状:23:40 起 CN-Edge 的 p99 RTT 从 ~180ms 上跳到 700ms,持续 812 分钟周期性尖刺。
外因基线:mtr 未见稳定丢包,路径在 IX 节点有时延抖动,但不至于 p99×4。
eBPF 观测:
- tcprtt 显示 p50 稳定,p99 跟着 softirqs NET_RX 峰同时抬头。
- drop.bt 统计出现 QDISC_DROP 和 DEV_RX_BUSY 短时升高。
- qdisc 直方图显示发送队列排队在尖刺时段放大(10→300us)。
- /proc/interrupts 显示 RX 队列 0 明显“热”,其余队列较空。
定位:单队列热点 + 中断未均衡 → 本机排队 + 软中断阻塞导致本地抖动放大(跨境链路的轻微拥塞被放大成肉眼可见尖刺)。
优化:
开启/均衡 RSS,将热队列中断绑到独立核;
启用 RPS/XPS,每队列 32768 flow entries;
适度 rx-usecs=50;
切换 fq+BBR,让发送端有更平滑的 pacing。
结果(30 分钟回归对比):
| 指标 | 优化前 | 优化后 |
|---|---|---|
| CN-Edge p95 RTT | 110 ms | 72 ms |
| CN-Edge p99 RTT | 290 ms | 120 ms |
| tcpretrans/min | 350 | 40 |
| NET_RX 峰值(/s) | 18k | 8k |
| QDISC 排队(p99) | 300 μs | 60 μs |
那晚 01:10,报警静了下来。回看面板,曲线从“锯齿”变回了“波浪”。
11)完整操作清单(可直接抄)
11.1 安装 & 校验
sudo apt update && sudo apt install -y linux-generic-hwe-20.04 \
bpfcc-tools bpftrace linux-headers-$(uname -r) llvm clang libbpf-dev \
build-essential iperf3 mtr-tiny ethtool iproute2 irqbalance
sudo systemctl enable --now irqbalance
sudo mount -t bpf bpf /sys/fs/bpf
[ -e /sys/kernel/btf/vmlinux ] && echo "BTF OK"
11.2 采集(建议并行窗口运行)
# 连接建立
sudo tcpconnect > /var/log/ebpf_tcpconnect.log
# RTT 样本
sudo tcprtt -i eth0 > /var/log/ebpf_tcprtt.log
# 重传
sudo tcpretrans > /var/log/ebpf_tcpretrans.log
# 软中断
sudo softirqs -d 1 > /var/log/ebpf_softirqs.log
或用本文提供的 rtt.bt / drop.bt / qdisc.bt 自定义脚本进行更细聚合。
11.3 网卡与队列调优(逐项回归)
# RPS/XPS
echo ffffff | sudo tee /sys/class/net/eth0/queues/rx-*/rps_cpus
echo 32768 | sudo tee /proc/sys/net/core/rps_sock_flow_entries
for q in /sys/class/net/eth0/queues/rx-*; do echo 32768 | sudo tee $q/rps_flow_cnt; done
# 中断亲和:示例把队列0绑到 CPU2
echo 2 | sudo tee /proc/irq/<IRQ_ID_OF_RXQ0>/smp_affinity_list
# 合并
sudo ethtool -C eth0 rx-usecs 50
# 发送队列与拥塞控制
sudo sysctl -w net.core.default_qdisc=fq
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
12)数据读法与“抖动”量化
我常用 Jitter = p95(RTT) - p50(RTT) 衡量业务可感知的抖动;当 p99/p999 偶发“针刺”影响大时,再单独跟踪尾部。
示例(tcprtt 样本 10 分钟窗口):
| 目的地 | p50 | p95 | p99 | Jitter(p95-p50) |
|---|---|---|---|---|
| CN-Edge | 48 ms | 72 ms | 120 ms | 24 ms |
| US-West | 130 ms | 165 ms | 205 ms | 35 ms |
结合 softirqs/runqlat/qdisc 的峰值时刻对齐,基本就能判断“抖动是外面带来的还是我们自己放大的”。
13)常见坑与解法速查
| 现象 | 可能原因 | 解决 |
|---|---|---|
bpftrace: failed to attach kprobe |
Secure Boot/Lockdown 限制 | 关 Secure Boot 或按规范签名,确保 CAP_SYS_ADMIN |
unknown field skc_daddr |
内核版本结构不同 | 5.4 改用 ((struct inet_sock*)sk)->inet_daddr |
kfree_skb reason 全是 0 |
旧内核无 *_reason |
升到 HWE 5.15 或退化到 skb:kfree_skb 仅做计数 |
| 容器内看不到流量 | netns 不一致 | nsenter -t <pid> -n 进入容器 netns 运行 |
| 合并后延迟上升 | rx-usecs 过大 |
降低到 25~50us 区间回归验证 |
| BBR 生效但吞吐波动 | 链路 QoS/对端队列 | 结合 fq pacing;必要时业务端做连接池/超时合理化 |
14)小结:把“黑盒网络”变成“有刻度的透明盒子”
那晚我离开机房时已经快两点。走廊尽头的空调声仍旧稳定,像是给我们做了一个对照组:真正稳定的系统,声音和曲线都不应该抖。
过去我们遇到跨境延迟,只能猜“哪段链路可能堵”,现在有了 eBPF,我能在几分钟内把“怀疑”拆成内核时间线:握手几毫秒、qdisc 排队几十微秒、softirq 峰值在哪一分钟、丢包是谁先丢。
eBPF 不是魔法,但它给了我们一把可以量化“哪里慢”的尺子。只要度量清晰,跨境路再长,也不再是彻底的黑盒。
附:文中脚本汇总
rtt.bt(按目的 IP:port 聚合 RTT 直方图)
#!/usr/bin/env bpftrace
#include <net/sock.h>
#include <net/tcp.h>
kprobe:tcp_rcv_established
{
$sk = (struct sock *)arg0;
$tp = (struct tcp_sock *)$sk;
$srtt = $tp->srtt_us >> 3;
$rtt_ms = $srtt / 1000;
$family = $sk->__sk_common.skc_family;
if ($family == AF_INET) {
$dip = $sk->__sk_common.skc_daddr;
$dport = ntohs($sk->__sk_common.skc_dport);
@rtt_ms[ntop($dip), $dport] = hist($rtt_ms);
}
}
drop.bt(5 秒打印内核丢包原因)
#!/usr/bin/env bpftrace
tracepoint:skb:kfree_skb
{
@reasons[arg1] = count();
}
interval:s:5
{
printf("---- drop reasons in last 5s ----\n");
print(@reasons);
clear(@reasons);
}
qdisc.bt(发送路径排队时间直方图)
#!/usr/bin/env bpftrace
tracepoint:net:net_dev_queue { @ts[args->skbaddr] = nsecs; }
tracepoint:net:net_dev_xmit /@ts[args->skbaddr]/ {
$d = (nsecs - @ts[args->skbaddr]) / 1000;
@qdisc_us = hist($d);
delete(@ts[args->skbaddr]);
}
如果你愿意,我可以把这些脚本按你的内核版本(5.4/5.15)定制成可直接运行的版本,并按照你的业务目标(例如只观测某段 IP、某些端口或容器)做过滤与日志落盘格式化(JSON/CSV),方便你接入现有监控体系