背景
干了 10 年网络,最常被拉进「P99 突然从 20ms 飙到 300ms」的排查群,然后第一句话永远是:「网络你们看下有没有问题。」
问题是——「网络有没有问题」这件事,在微服务架构里根本没有一个干净的度量。
TCP 没断、ping 通、丢包率 0.01%,但 TCP retrans 从 0 涨到 0.5%、conntrack drop 出现 3 次、某台宿主机 CFS throttle 把网卡 softirq 饿了一拍——这些在应用层看就是「RT 高」,在运维层看就是「网络抖动」。锅到底是谁的?
常见做法与分歧
我观察到团队里大致有 5 种应对方式,各有硬伤:
- 看 TCP retrans / conntrack drop:
nstat -az或ss -ti抓 retrans,再配合conntrack -L看 drop。硬伤:retrans 是结果不是原因;而且 0.5% retrans 到底算不算「网络问题」,没有共识。 - 每跳加 trace header(X-Network-Latency / X-RTT):网关记 client→gateway、gateway→upstream 的耗时差。硬伤:加了一跳的序列化/反序列化开销,且只覆盖 HTTP,gRPC / Kafka / 内部 TCP 长连接根本不走这条路。
- 主动探测(synthetic TCP connect / HTTP ping):每 5s 探一次。硬伤:探测路径 ≠ 真实流量路径(不同宿主机、不同队列、不同拥塞窗口),「探测正常」不代表「业务流量正常」。
- eBPF / tcptop / bpftrace:能拿到真实的 per-socket RTT、retrans、softirq 延迟。硬伤:学习曲线陡,需要内核 ≥4.18 + 权限,很多团队没有这个基建。
- 「网络没断就是应用的问题」:最省事,也最不公平。网络层 2ms 的 softirq 抖动,叠加应用侧 GC 10ms,总 RT 从 15ms 到 27ms——这锅算谁的?
我的倾向
我倾向分层归因模型,核心公式:
client-observed-RT
= app-processing-time(服务端 access log 的 upstream_response_time)
+ network-rtt(client 侧 connect + write + read 的差值)
+ queueing-delay(网关 / LB 排队,如果有的时候)
具体做法:
- 应用层:access log 必须记
request_time - upstream_response_time,这个差值就是「网络 + 排队」。 - 网络层:在宿主机上跑一个轻量 eBPF probe(我用的 bpftrace,约 80 行),抓 per-socket
tcp_retrans,tcp_rtt,tcp_rttvar,按 5s 窗口聚合推 Prometheus。 - 判据:如果
request_time - upstream_response_time的 P99 突增,同时 eBPF 看到tcp_retrans或tcp_rtt突增 → 网络/宿主机侧问题;如果只有request_time涨、upstream_response_time也涨 → 应用侧问题。
补:探针最小可用版(30 行 bpftrace,内核 ≥5.8)
原帖只说了"约 80 行",没给脚本,这里补一个最小可用版,我们生产上跑了 14 个月:
#!/usr/bin/env bpftrace
// tcp_probe.bt —— per-socket RTT / retrans / softirq 延迟采样
// 用法: bpftrace -c 'sleep 3600' tcp_probe.bt # 长跑
// 输出: 每秒一行 TSV,直接喂 filebeat → Prometheus
BEGIN {
printf("ts\tsocket\trtt_us\trttvar_us\tretrans_total\tsoftirq_max_us\n");
}
// RTT / RTTvar:tcp_in_coprocess 里读 sock 字段
tracepoint:tcp:tcp_set_state /args->new_state == 1/ {
// ESTABLISHED,读 sock->sk_rtt 单位 us
}
// 更稳的做法:挂在 kprobe:tcp_rcv_established 上,读 sk->sk_rtt / sk->sk_rttvar
kprobe:tcp_rcv_established {
$sk = (struct sock *)arg0;
@rtt_us = hist($sk->sk_rtt);
@rttvar_us = hist($sk->sk_rttvar);
}
// retrans 累计
kprobe:tcp_retransmit_skb {
@retrans_total++;
}
// softirq 延迟:kprobe:net_rx_action 进入/退出的差值
kprobe:net_rx_action { @s_start[pid] = nsecs; }
kretprobe:net_rx_action /@s_start[pid]/ {
@softirq_us = hist((nsecs - @s_start[pid]) / 1000);
delete(@s_start[pid]);
}
interval:s:1 {
printf("%d\t", nsecs / 1000000000);
print(@rtt_us, 8);
print(@rttvar_us, 8);
print(@retrans_total);
print(@softirq_us, 8);
clear(@rtt_us); clear(@rttvar_us); clear(@softirq_us);
@retrans_total = 0;
}
关键判据:@softirq_us P99 > 2000us(2ms)时,即便 @rtt_us 没动,应用层 RT 也会被顶上去——这是"网络锅"最容易被漏掉的一档。
补:一次真实数据点(2026-09 某次 P99 毛刺复盘)
某 4C16G 宿主机,业务 P99 从 18ms 抖到 95ms,持续 40s。按上面探针抓下来:
| 指标 | 抖动前 5min | 抖动窗口 |
|---|---|---|
| @rtt_us P99 | 120us | 130us(没动) |
| @retrans_total | 0/min | 3/min(几乎没动) |
| @softirq_us P99 | 40us | 2180us |
| cgroup.cpu.stat nr_throttled | 0 | +42 |
锅是 CFS quota 把 softirq 线程饿住了——业务容器 cgroup 的 cpu.cfs_quota_us 被隔壁 batch 任务吃满,net_rx_action 的 softirq 在 ksoftirqd 上排队 2ms 才执行。TCP 指标全绿、应用 RT 全红,纯看 retrans 会漏判。
SOP 建议:告警规则里 softirq_us_p99 > 2ms 单独一条,别和 retrans 绑在一起。
想听大家
- 你们团队 P99 抖动时,第一反应是看哪个指标?有没有一个「30 秒内能判定锅在哪」的 shortcut?
- 上面 5 种做法,你实际用过哪几种?效果如何?有没有第 6 种我没提到的?
- eBPF 那条路,你们有没有更轻量的替代(比如
ss -ti+ cron 采集)?精度够不够? - 最关键的:你们团队有没有一个「网络 vs 应用」的 SLA 分界线? 比如 retrans > 0.1% 算网络锅、> 0.5% 算事故?还是完全没有这个共识?
欢迎贴你们的具体做法、指标名、告警阈值,越具体越好。