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

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

发布人:Minchunlin 发布时间:2025-08-18 10:13 阅读量:776


半夜 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 环境复用过。你完全可以把脚本拷走,从“只读观测”开始,一步步把内核级网络延迟摁倒在地。祝你排障顺利。

目录结构
全文