上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-08-26 09:52 阅读量:673


周五晚 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),方便你接入现有监控体系

目录结构
全文