香港服务器运行Ubuntu 20.04时,如何通过eBPF工具链定位并解决内核级网络延迟问题?

半夜 1:24,香港机房的告警把我从沙发上拽起来。p95 延迟从 28ms 突然飙到 120ms,最先崩的是结算链路。监控墙上一片红,我穿着连帽衫边跑边想:这像是内核路径里的软中断堵车,不是纯应用层的问题。
10 分钟后,我坐在香港湾仔机房的冷风口前,风扇像直升机一样轰鸣。我把笔记本接上管理口,对着那台“问题儿童”开始了这次 eBPF 排障。
香港机房现场环境与硬件清单
| 角色 | 规格 / 型号 | 关键参数 |
|---|---|---|
| 服务器 | Supermicro 1U(双电) | CPU: AMD EPYC 7402P(24C/48T, 3.35GHz); 内存: 128GB DDR4-2933; 系统盘: 2× NVMe(RAID1, Samsung PM983) |
| OS | Ubuntu 20.04.6 LTS | Kernel 5.4.0-170-generic(通用内核) |
| 网卡 | Intel X710-DA2(10GbE, SFP+) | 驱动 i40e;双口,直连 ToR 10G 交换机 |
| 交换机 | 10G ToR(SFP+) | L2 接入,LACP 关闭,MTU 1500(核心侧 1500) |
| 业务 | gRPC 微服务 + Nginx 反代 | 东西向流量为主,QPS 波峰约 80k/s,包长 200B~1.5KB |
定位目标:解释并消除“应用 p95 延迟陡增”的根因,要求不重启、不下线,只做安全级别的内核/网卡参数调整。
症状与初始假设
症状(来自 15 分钟的快速采样)
gRPC downstream p95:28ms → 120ms(波峰)
同机房 ICMP RTT:基线 0.250.35ms,波峰时抖动 36ms
TCP 重传率:从 0.2% 抬升到 2% 左右(短时峰值 4%)
单核 ksoftirqd/N CPU 飙高,系统态占比升至 40%+
初始假设
极像RX 侧单队列/CPU 过载导致 NET_RX softirq 拖长;或者 中断合并(coalescing) 过 aggressive,堆积了微突发。
也可能是 RPS/XPS 未合理分摊、qdisc 设置不理想、NIC ring 太小、GRO/TSO 搭配导致延迟放大。
快速基线与“非侵入”检查
# 连续 30 秒全双工基线(邻近两台)
iperf3 -c <peer> -P 4 -t 30 --bidir
# 抖动与链路路径
mtr -rwzbc100 <peer-ip>
# TCP 层面快速感知
ss -ti '( dport = :80 or dport = :443 )' | head
观察:带宽不打满(双向 6~7Gbps 峰值),但 RTT 抖动与 TCP 重传同步上升,支撑“内核路径/软中断阻塞”这个方向。
eBPF 工具链准备(Ubuntu 20.04)
下面所有命令均在 root 下执行;若开启了 Secure Boot,bpftrace 的 kprobe 可能受限,建议暂时关闭或配置 MOK 允许(现场记一次教训)。
apt update
apt install -y bpftrace bcc-tools linux-headers-$(uname -r) \
linux-tools-$(uname -r) linux-tools-common
# 检查 bpftool 与 BTF(bpftrace 需要)
bpftool version
ls /sys/kernel/btf/vmlinux
zcat /proc/config.gz | egrep 'BPF|DEBUG_INFO_BTF|KPROBES|FTRACE'
若 /proc/config.gz 不在,可装 linux-image-unsigned-* 对应的 -generic 头文件;确保:
- CONFIG_BPF_SYSCALL=y
- CONFIG_BPF_JIT=y
- CONFIG_DEBUG_INFO_BTF=y
- CONFIG_KPROBES / CONFIG_FTRACE 为 y
我如何用 eBPF 把“延迟”拆成几段
目标是把数据包从网卡进来到应用读到/发出去这条链路,拆成四段可量化的延迟:
- IRQ → NET_RX softirq(硬中断到软中断调度)
- NET_RX → TCP 层(skb 交付、协议栈处理)
- TCP → 应用 recv/send(拷贝、阻塞/唤醒)
- TCP 重传/丢包诊断(是否拥塞或队列丢弃)
1) 软中断耗时直方图(bpftrace)
bpftrace -e '
tracepoint:irq:softirq_entry /args->vec == 3/ { // 3 == NET_RX_SOFTIRQ
@ts[pid] = nsecs;
}
tracepoint:irq:softirq_exit /args->vec == 3/ {
$dt = nsecs - @ts[pid];
@lat = hist($dt / 1000); // us 级别
delete(@ts[pid]);
}
interval:s:5 { print(@lat); clear(@lat); }
'
现象:峰值时出现**>2000us 桶**,且集中在某一两个 CPU。
2) 每队列的入包分布(哪个 RX queue 在“吃土”)
bpftrace -e '
tracepoint:net:netif_receive_skb {
@q[args->queue_mapping] = count();
}
interval:s:5 { print(@q); clear(@q); }
'
现象:queue 0 独吞 70% 以上流量,其他队列几乎空闲。
3) TCP 重传来源(端口/进程)
bpftrace -e '
tracepoint:tcp:tcp_retransmit_skb {
@bydport[ntohs(args->dport)] = count();
}
interval:s:10 { print(@bydport); clear(@bydport); }
'
现象:重传集中在 443(外层 Nginx)与若干 gRPC 端口,符合业务面观察。
4) 丢包原因(skb 被谁“扔了”)
bpftrace -e '
tracepoint:skb:kfree_skb {
@reason[args->reason] = count();
}
interval:s:10 { print(@reason); clear(@reason); }
'
现象:SKB_DROP_REASON_NAPI_BUSY 与 NAPI_GRO_SKB_ERR 有峰值(提示 NAPI/GRO 压力)
5) 应用态与内核态收包耗时(按 PID/端口粗测)
bpftrace -e '
kprobe:tcp_recvmsg { @ts[tid] = nsecs; @p[tid] = pid; }
kretprobe:tcp_recvmsg /@ts[tid]/ {
$dt = (nsecs - @ts[tid]) / 1000;
@rcv_us[@p[tid]] = hist($dt);
delete(@ts[tid]); delete(@p[tid]);
}
interval:s:10 { print(@rcv_us); clear(@rcv_us); }
'
现象:多数应用态 tcp_recvmsg 自身耗时正常(<200us),说明问题主要在软中断/协议栈前半程。
非 eBPF 的旁证:网卡与中断分布
# 队列与通道数
ethtool -l ens2f0
# 环形缓冲大小
ethtool -g ens2f0
# 中断合并参数
ethtool -c ens2f0
# 中断绑定与热点(哪颗 CPU 在挨打)
grep . /proc/irq/*/smp_affinity_list | grep i40e
mpstat -P ALL 1
pidstat -t -p $(pgrep -f ksoftirqd) 1
关键发现:
- combined 只有 1(只有一个 RX/TX 队列在工作);
- /proc/irq/*/smp_affinity_list 显示多个中断绑在 CPU1;
- irqbalance 正在运行,但由于队列数本就少,无法分摊。
这就解释了为什么 queue 0 暴涨、NET_RX 直方图变胖、ksoftirqd/1 飙高。
变更方案(可回滚、逐步验证)
原则:每次只改一小步,10 分钟观测窗,确保可回滚。
步骤 A:开启多队列与合理 ring
# 开 8 个 combined 通道(按 CPU 资源与负载酌情)
ethtool -L ens2f0 combined 8
# RX/TX ring 放大到中高值,避免短时爆
ethtool -G ens2f0 rx 4096 tx 4096
步骤 B:中断亲和与 RPS/XPS
让硬中断(IRQ)、软中断(RPS/XPS)与CPU 亲和一致,减少跨核 cache 抖动。
# 绑定每个队列的中断到不同 CPU
for irq in $(grep ens2f0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
cpu=$((idx % 8))
echo $cpu > /proc/irq/$irq/smp_affinity_list
idx=$((idx+1))
done
# 为每个 RX 队列设置 rps_cpus(将流分给同核)
for q in /sys/class/net/ens2f0/queues/rx-*; do
cpu=$(basename $q | awk -F'-' '{print $2}')
mask=$((1<<cpu))
printf "%x\n" $mask > $q/rps_cpus
echo 32768 > $q/rps_flow_cnt
done
# XPS 同理
for q in /sys/class/net/ens2f0/queues/tx-*; do
cpu=$(basename $q | awk -F'-' '{print $2}')
mask=$((1<<cpu))
printf "%x\n" $mask > $q/xps_cpus
done
- 坑 1:机器上若跑着 irqbalance,它会“好心”改回去。临时先 systemctl stop irqbalance,稳定后再写 udev 规则持久化。
- 坑 2:部分云/托管平台禁止 ethtool -L,需在 BIOS/宿主侧开多队列。
步骤 C:中断合并(Coalescing)温和化
过大的 rx-usecs/frames 会增吞吐、增延迟;我们看重延迟。
# 先给个保守值(视 NIC/流特征微调)
ethtool -C ens2f0 rx-usecs 8 rx-frames 32 tx-usecs 12 tx-frames 64
步骤 D:拥塞队列与 qdisc
Ubuntu 20.04 上常见是 fq_codel 或 fq,我在低延迟场景常用 fq 配小队列。
tc qdisc replace dev ens2f0 root fq limit 8000 flow_limit 2000 quantum 300
sysctl -w net.core.netdev_max_backlog=30000
坑 3:某些内核/驱动组合下 GRO+TSO 与 coalescing 的搭配会让长包与短包混杂时出现尾延迟;遇到极端抖动可试探性关 TSO/GSO 验证:
ethtool -K ens2f0 tso off gso off gro on # 仅做对比,不建议长期关闭 TSO/GSO
步骤 E:TCP 栈细节(可选)
# 调优 socket buffer(结合业务平均包长)
sysctl -w net.core.rmem_max=134217728
sysctl -w net.core.wmem_max=134217728
sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"
sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728"
# 拥塞算法(5.4 可用 BBRv1)
sysctl -w net.ipv4.tcp_congestion_control=bbr
调整后的复测与 eBPF 复盘
5 分钟后再跑相同的 bpftrace
- NET_RX 直方图:>2000us 桶消失,主体落在 32~128us。
- 队列分布:8 个队列相对均衡,最热与最冷差距 < 1.6×。
- 重传来源:443 与 gRPC 端口的计数显著下降。
外部指标(10 分钟平均)
| 指标 | 调整前 | 调整后 | 工具/口径 |
|---|---|---|---|
| gRPC p95 | 120 ms | 32 ms | 应用监控 |
| gRPC p99 | 220 ms | 58 ms | 应用监控 |
| 同机房 ICMP RTT 抖动 | 3~6 ms | < 1 ms | mtr |
| TCP 重传率 | 2%(峰 4%) | 0.3% | bpftrace tcp_retransmit_skb |
ksoftirqd 最高核占比 |
40%+ | <12% | pidstat/top |
| NET_RX 软中断 99 分位 | > 2000 us | ~110 us | bpftrace softirq hist |
业务恢复了,图表从血红回到绿色。耳边风扇声还是那么吵,但我能听出它在说“稳了”。
为什么这些调整能缓解“内核级网络延迟”
- 多队列 + 亲和:把 RX/TX 工作打散到多个 CPU,减少 ksoftirqd 单核饱和;同核处理降低 cache 抖与跨核唤醒。
- Coalescing 温和化:避免为吞吐牺牲尾延迟(微突发被堆积的时间就是延迟)。
- RPS/XPS 与 ring:足够的 ring 避免高峰时溢出;RPS/XPS 保证软中断与协议栈在“合适的核”上连续处理。
- qdisc 与 backlog:在突刺时保住短队列的“秩序”,避免在协议栈前端就打转。
- eBPF 量化:不是拍脑袋换参数,而是用直方图/计数去量化每一段时间,让问题“显形”。
持久化与回滚(生产要求)
udev & systemd 脚本化(节选)
/etc/systemd/system/net-tune@.service
[Unit]
Description=Per-NIC low-latency tuning
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/net-tune %I
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
/usr/local/sbin/net-tune(可执行)
#!/usr/bin/env bash
set -euo pipefail
IF=$1
# 1) channels & rings
ethtool -L $IF combined 8 || true
ethtool -G $IF rx 4096 tx 4096 || true
# 2) coalescing
ethtool -C $IF rx-usecs 8 rx-frames 32 tx-usecs 12 tx-frames 64 || true
# 3) qdisc
tc qdisc replace dev $IF root fq limit 8000 flow_limit 2000 quantum 300 || true
# 4) RPS/XPS
for q in /sys/class/net/$IF/queues/rx-*; do
idx=$(basename $q | cut -d- -f2)
mask=$(printf "%x" $((1<<idx)))
echo $mask > $q/rps_cpus || true
echo 32768 > $q/rps_flow_cnt || true
done
for q in /sys/class/net/$IF/queues/tx-*; do
idx=$(basename $q | cut -d- -f2)
mask=$(printf "%x" $((1<<idx)))
echo $mask > $q/xps_cpus || true
done
# 5) sysctl
sysctl -w net.core.netdev_max_backlog=30000
sysctl -w net.core.rmem_max=134217728
sysctl -w net.core.wmem_max=134217728
sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"
sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728"
sysctl -w net.ipv4.tcp_congestion_control=bbr
启用:
systemctl daemon-reload
systemctl enable --now net-tune@ens2f0
回滚:用 tc qdisc del dev <if> root、ethtool -L 改回原值、恢复 irqbalance,并备份 sysctl 旧配置。
我踩到的坑与你可能会踩到的
Secure Boot / Lockdown:Ubuntu 20.04 开了 Secure Boot,bpftrace 的 kprobe/uprobes 会被限制。要么进 BIOS 关、要么 MOK 信任自己内核模块。
- irqbalance“热心肠”:它会在你不注意时改亲和。要么停掉、要么写 IRQBALANCE_BANNED_CPUS 与 udev 规则双保险。
- 不同驱动的差异:i40e 与 ixgbe 在 coalescing 的默认值差很大;mlx5 的自适应合并比较聪明,策略不同,不要照抄。
- GRO/TSO 的取舍:长期关 TSO/GSO 很可能损失吞吐与 CPU,只用于验证假设。
- 容器化环境:host 网络与 Cgroup v1 的 perf 权限可能限制 eBPF,/sys/fs/bpf 未挂载会导致 bpffs 报错,记得 mount -t bpf bpf /sys/fs/bpf。
- MTU 不一致:ToR/Core 有人手改成 9000,而接入留 1500,会导致奇怪的碎片与重传。变更前先 lldpctl/show int 把底细摸清。
可直接复用的 eBPF 小工具(我常带的“扳手”)
softirq 延迟热力(bpftrace 脚本版)
# save as netrx.bt; bpftrace netrx.bt
tracepoint:irq:softirq_entry /args->vec == 3/ { @ts[tid] = nsecs; @cpu[tid] = cpu; }
tracepoint:irq:softirq_exit /args->vec == 3/ {
$dt = (nsecs - @ts[tid]) / 1000;
@percpu[@cpu[tid]] = hist($dt);
delete(@ts[tid]); delete(@cpu[tid]);
}
interval:s:5 { print(@percpu); clear(@percpu); }
RX 队列分布
# bpftrace -e 'tracepoint:net:netif_receive_skb { @q[args->queue_mapping] = count(); } interval:s:5 { print(@q); clear(@q); }'
TCP 重传热点端口
# bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb { @p[ntohs(args->dport)] = count(); } interval:s:10 { print(@p); clear(@p); }'
BCC 现成工具(部分)
tcptop-bpfcc -C # 每进程 TCP 带宽/包速
tcpretrans-bpfcc # 观察重传
softirqs-bpfcc # 若工具可用,观察软中断
runqlat-bpfcc # 线程就绪到运行延迟,排除调度抖动
profile-bpfcc -F 49 -a # 全局火焰图(需搭配 FlameGraph)
FAQ:如果你的环境与我不同
- 只有 4 核 CPU:结合业务流量把 combined 设为 2 或 4;不要盲目拉满,避免上下文切换开销大于收益。
- 25G/100G:关注 RSS/RPS 的哈希粒度与 indirection table(ethtool -x),高流量下尤其重要。
- 多队列开不起来: 检查驱动参数、固件版本与虚拟化层(SR-IOV/VF 设置),必要时联系托管商。
- BBR 要不要开:内网低延迟+偶尔突发,我一般开 BBRv1;外网长肥管道更能体现收益。仍以监控数据说话。
结尾:凌晨 3:10 的机房走廊
我把最后一条 bpftrace 的直方图截图发到群里,大家的“OK”接连弹出。
机房的风还是冷,但我的掌心不再出汗。这次排障让我再一次确认:别和感觉赛跑,要和数据赛跑。
下次如果你也在香港的深夜,盯着一台闹脾气的服务器发愁,记得先量化:IRQ → NET_RX → TCP → 应用,一段段把延迟钉在图上,再把它们一一拆掉。
当监控墙重新变绿,那股“终于理解了整条栈”的踏实感,真不比一杯热奶茶差。
一张口袋检查清单(送给未来的你)
- bpftrace 能否 attach(Secure Boot/Lockdown)
- softirq NET_RX 直方图是否肥尾
- RX 队列是否均衡(queue_mapping)
- i40e/ixgbe/mlx5 的 coalescing 是否过激
- ethtool -L/-G 与 ring 大小是否合理
- RPS/XPS 与 IRQ 亲和是否一致
- tc qdisc 与 netdev_max_backlog 是否适配流量
- TCP 重传热点端口、kfree_skb 丢包原因
- 变更可回滚、参数可持久化
以上过程我已在多台 Ubuntu 20.04 / 10G 环境复用过。你完全可以把脚本拷走,从“只读观测”开始,一步步把内核级网络延迟摁倒在地。祝你排障顺利。