用户说”慢”。这是后端工程师听到频率最高、信息量最低的故障描述。“慢”可能是 DNS 解析卡了 2 秒,可能是 TCP 握手跨了半个地球,可能是 TLS 协商用了过时的密码套件,可能是服务端查询命中了慢 SQL,也可能是响应体太大在窄带宽链路上传输了 5 秒。
如果不把”慢”分解成可测量的阶段,你就只能猜。猜对了是运气,猜错了浪费所有人的时间。
本文建立一套系统化的延迟分析方法论。核心思想是延迟分解——把端到端延迟拆成独立可测量的阶段,逐段定位瓶颈。
一、延迟的本质:不可压缩与可优化
1.1 延迟的物理下界
光在真空中的速度是 299,792 km/s,在光纤中约为 200,000 km/s(折射率约 1.5)。这意味着:
| 路径 | 直线距离 | 光纤距离(约 1.4x) | 单程延迟 | RTT |
|---|---|---|---|---|
| 北京 → 上海 | 1,060 km | 1,484 km | 7.4 ms | 14.8 ms |
| 北京 → 广州 | 1,890 km | 2,646 km | 13.2 ms | 26.4 ms |
| 上海 → 旧金山 | 9,500 km | 13,300 km | 66.5 ms | 133 ms |
| 伦敦 → 悉尼 | 16,980 km | 23,772 km | 118.9 ms | 237.8 ms |
这是物理下界,实际延迟通常是理论值的 1.5 到 3 倍,因为:
- 光纤不走直线,跟着地形和海缆路由绕行
- 每经过一个路由器增加 0.1–1 ms 的转发延迟
- 拥塞导致排队延迟
工程启示:如果你的北京用户访问上海机房的 RTT 是 15 ms,这已经接近物理极限,优化空间在别处。如果 RTT 是 80 ms,说明路由绕行严重,可以考虑换运营商或用 CDN。
1.2 延迟的四个组成部分
任何网络延迟都可以分解为四个部分:
总延迟 = 传播延迟 + 传输延迟 + 处理延迟 + 排队延迟
传播延迟(Propagation Delay):信号在介质中传播的时间,由距离和光速决定,不可压缩。
传输延迟(Transmission
Delay):把数据包从发送端”推上”链路的时间,等于
包大小 / 链路带宽。1500 字节在 1 Gbps
链路上的传输延迟是 12 μs,在 100 Mbps 链路上是 120 μs。
处理延迟(Processing Delay):路由器/交换机查路由表、执行 ACL 检查、修改 TTL 的时间。现代硬件通常在微秒级。
排队延迟(Queuing Delay):数据包在路由器缓冲区中等待的时间。这是延迟中最不确定的部分——空闲时接近 0,拥塞时可以达到数百毫秒。
1.3 延迟与带宽的关系
一个常见的误解:“带宽大了延迟就低”。不对。带宽和延迟是两个独立维度:
- 高带宽 + 高延迟:跨太平洋的 10 Gbps 海缆,RTT 130 ms
- 低带宽 + 低延迟:同机房的 100 Mbps 链路,RTT 0.1 ms
带宽影响的是吞吐量,不是延迟。带宽-延迟积(Bandwidth-Delay Product,BDP)决定了链路上能”在途”的数据量:
BDP = 带宽 × RTT
10 Gbps × 130 ms = 10 × 10⁹ × 0.13 / 8 = 162.5 MB
这意味着在这条跨太平洋链路上,TCP 的窗口需要至少 162.5 MB 才能填满管道。默认的 TCP 窗口远不够,这就是为什么跨洋传输需要调优 TCP 缓冲区。
二、全链路延迟分解模型
2.1 HTTP 请求的延迟阶段
一个完整的 HTTPS 请求经历以下延迟阶段:
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ DNS │→│ TCP │→│ TLS │→│ Request │→│ Server │→│ Response │
│ Resolve │ │Handshake │ │Handshake │ │ Transfer │ │Processing│ │ Transfer │
└─────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
t_dns t_tcp t_tls t_req t_server t_resp
TTFB = t_dns + t_tcp + t_tls + t_req + t_server
Total = TTFB + t_resp
各阶段的典型值:
| 阶段 | 典型范围 | 异常阈值 | 常见瓶颈原因 |
|---|---|---|---|
| DNS 解析 | 0–50 ms | > 200 ms | DNS 服务器远、缓存未命中、DNSSEC 验证 |
| TCP 握手 | 0.5–150 ms | > 300 ms | 物理距离远、中间设备慢、SYN 重传 |
| TLS 握手 | 0–300 ms | > 500 ms | TLS 1.2 的 2-RTT、OCSP 查询慢、证书链过长 |
| 请求传输 | < 1 ms | > 50 ms | POST body 过大 + 上行带宽窄 |
| 服务端处理 | 1–500 ms | > 2000 ms | 慢查询、锁竞争、外部依赖超时 |
| 响应传输 | 1–5000 ms | > 10000 ms | 响应体过大、带宽受限、TCP 窗口小 |
2.2 用 curl 做延迟分解
curl 内置了精确的时间测量,是最快的延迟分解工具:
curl -w "\
DNS: %{time_namelookup}s\n\
TCP: %{time_connect}s\n\
TLS: %{time_appconnect}s\n\
TTFB: %{time_starttransfer}s\n\
Total: %{time_total}s\n\
Size: %{size_download} bytes\n\
Speed: %{speed_download} bytes/s\n" \
-o /dev/null -s https://example.com/api/data输出示例:
DNS: 0.028s
TCP: 0.045s
TLS: 0.112s
TTFB: 0.358s
Total: 0.412s
Size: 52480 bytes
Speed: 127378 bytes/s
分解计算:
DNS 解析耗时 = time_namelookup = 28 ms
TCP 握手耗时 = time_connect - time_namelookup = 17 ms (1 RTT)
TLS 握手耗时 = time_appconnect - time_connect = 67 ms (约 2 RTT → TLS 1.2)
服务端处理耗时 = time_starttransfer - time_appconnect = 246 ms
响应传输耗时 = time_total - time_starttransfer = 54 ms
这个例子的瓶颈在服务端处理(246 ms),不在网络。如果盲目优化网络,完全是浪费时间。
2.3 批量延迟分解脚本
单次测量有噪声,需要多次采样取统计值:
#!/bin/bash
# latency-breakdown.sh — 批量延迟分解测量
URL="${1:?Usage: $0 <url> [count]}"
COUNT="${2:-20}"
TMPFILE=$(mktemp)
FORMAT='%{time_namelookup} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}\n'
for i in $(seq 1 "$COUNT"); do
curl -w "$FORMAT" -o /dev/null -s "$URL" >> "$TMPFILE"
sleep 0.5
done
echo "=== Latency Breakdown (${COUNT} samples) ==="
echo "Phase P50 P95 P99 Max"
echo "----- --- --- --- ---"
awk '{
dns[NR] = $1 * 1000;
tcp[NR] = ($2 - $1) * 1000;
tls[NR] = ($3 - $2) * 1000;
server[NR]= ($4 - $3) * 1000;
xfer[NR] = ($5 - $4) * 1000;
total[NR] = $5 * 1000;
n = NR;
}
function percentile(arr, n, p, k, sorted) {
for (i=1; i<=n; i++) sorted[i] = arr[i];
asort(sorted);
k = int(n * p / 100 + 0.5);
if (k < 1) k = 1;
return sorted[k];
}
END {
printf "DNS %7.1f %7.1f %7.1f %7.1f ms\n", \
percentile(dns,n,50), percentile(dns,n,95), percentile(dns,n,99), percentile(dns,n,100);
printf "TCP %7.1f %7.1f %7.1f %7.1f ms\n", \
percentile(tcp,n,50), percentile(tcp,n,95), percentile(tcp,n,99), percentile(tcp,n,100);
printf "TLS %7.1f %7.1f %7.1f %7.1f ms\n", \
percentile(tls,n,50), percentile(tls,n,95), percentile(tls,n,99), percentile(tls,n,100);
printf "Server %7.1f %7.1f %7.1f %7.1f ms\n", \
percentile(server,n,50), percentile(server,n,95), percentile(server,n,99), percentile(server,n,100);
printf "Transfer %7.1f %7.1f %7.1f %7.1f ms\n", \
percentile(xfer,n,50), percentile(xfer,n,95), percentile(xfer,n,99), percentile(xfer,n,100);
printf "Total %7.1f %7.1f %7.1f %7.1f ms\n", \
percentile(total,n,50), percentile(total,n,95), percentile(total,n,99), percentile(total,n,100);
}' "$TMPFILE"
rm -f "$TMPFILE"用法:
chmod +x latency-breakdown.sh
./latency-breakdown.sh https://api.example.com/health 50输出示例:
=== Latency Breakdown (50 samples) ===
Phase P50 P95 P99 Max
----- --- --- --- ---
DNS 1.2 3.8 28.4 45.2 ms
TCP 15.3 16.8 18.2 22.1 ms
TLS 32.4 35.1 38.6 42.3 ms
Server 85.2 245.6 580.3 1203.4 ms
Transfer 2.1 4.8 8.2 12.5 ms
Total 138.4 302.1 645.2 1298.6 ms
这个输出告诉你:P50 延迟 138 ms,瓶颈在 Server(85 ms)。但 P99 延迟 645 ms,Server 的 P99 飙到 580 ms——说明服务端有长尾延迟问题,可能是 GC、锁竞争或偶发慢查询。
三、TCP 层延迟分析
3.1 用 Wireshark 测量 TCP RTT
TCP 层的 RTT 是网络延迟的基础指标。Wireshark 会自动计算
tcp.analysis.ack_rtt——从数据段发出到收到 ACK
的时间。
步骤 1:过滤目标连接
ip.addr == 10.0.1.50 && tcp.port == 443
步骤 2:添加 RTT 列
右键列标题 → Column Preferences → 添加:
Title: ACK RTT
Type: Custom
Fields: tcp.analysis.ack_rtt
步骤 3:用 IO Graph 绘制 RTT 趋势
Statistics → I/O Graphs:
- Y Axis: AVG(tcp.analysis.ack_rtt)
- Filter:
tcp.stream eq 5(替换为目标流编号) - Interval: 100ms
RTT 突然飙高通常意味着:
| RTT 模式 | 可能原因 |
|---|---|
| 持续偏高(> 100 ms) | 物理距离远或路由绕行 |
| 周期性尖峰 | 链路拥塞的排队延迟 |
| 突然阶跃式升高 | 路由变更 |
| 逐渐增长 | TCP 拥塞窗口被压缩,重传导致 RTO 翻倍 |
3.2 SYN-ACK RTT:最纯净的网络延迟
TCP 三次握手的 SYN → SYN-ACK 延迟是最”干净”的网络 RTT 测量——它不受服务端处理逻辑影响,只反映网络路径延迟。
用 tshark 批量提取 SYN-ACK RTT:
tshark -r capture.pcap \
-Y "tcp.flags.syn==1 && tcp.flags.ack==1" \
-T fields \
-e frame.time_relative \
-e ip.src \
-e tcp.analysis.ack_rtt \
| sort -t$'\t' -k3 -n输出示例:
1.234 10.0.1.100 0.015234
2.567 10.0.1.100 0.016012
5.891 10.0.1.100 0.015876
12.345 10.0.1.100 0.312456 ← 这个连接的 SYN-ACK 延迟异常
312 ms 的 SYN-ACK
延迟说明要么经过了不同的网络路径,要么服务端的 SYN
队列积压(net.ipv4.tcp_max_syn_backlog
满了)。
3.3 TCP 重传对延迟的放大
TCP 重传是延迟的放大器。一次丢包导致的重传至少增加 1 个 RTO(默认最小 200 ms),连续丢包导致 RTO 指数退避:
第 1 次重传: RTO ≈ 200 ms
第 2 次重传: RTO ≈ 400 ms
第 3 次重传: RTO ≈ 800 ms
第 4 次重传: RTO ≈ 1600 ms
第 5 次重传: RTO ≈ 3200 ms
用 tshark 统计重传情况:
# 重传统计
tshark -r capture.pcap -Y "tcp.analysis.retransmission" \
-T fields -e frame.time_relative -e ip.dst -e tcp.stream \
| awk '{streams[$3]++} END {
for (s in streams) printf "Stream %s: %d retransmissions\n", s, streams[s]
}' | sort -t: -k2 -rn | head -10判断重传率是否正常:
| 重传率 | 评估 | 行动 |
|---|---|---|
| < 0.1% | 正常 | 无需行动 |
| 0.1%–1% | 轻微 | 监控趋势 |
| 1%–5% | 明显 | 排查链路质量 |
| > 5% | 严重 | 立即排查,可能是链路故障 |
3.4 TCP 窗口对传输延迟的影响
即使网络 RTT 很低,如果 TCP 接收窗口过小,吞吐量也会受限,表现为”传输慢”:
最大吞吐量 = 接收窗口 / RTT
Window = 64 KB, RTT = 100 ms → 最大吞吐量 = 640 KB/s = 5.12 Mbps
Window = 1 MB, RTT = 100 ms → 最大吞吐量 = 10 MB/s = 80 Mbps
在 Wireshark 中检查窗口大小:
tcp.window_size_value < 16384
如果看到频繁的 TCP Window Full 或
TCP Zero Window,说明接收端的缓冲区是瓶颈。解决方法:
# 增大 TCP 接收缓冲区
sysctl -w net.ipv4.tcp_rmem="4096 1048576 16777216"
sysctl -w net.core.rmem_max=16777216
# 确保窗口缩放已启用(默认启用)
sysctl net.ipv4.tcp_window_scaling四、路径延迟分析
4.1 traceroute 的三种实现
traceroute 通过逐跳增加 TTL 来探测路径上每一跳的延迟。但不同实现有不同特点:
# ICMP traceroute(默认在 Windows,Linux 需要 -I)
traceroute -I example.com
# UDP traceroute(Linux 默认)
traceroute example.com
# TCP traceroute(穿透防火墙能力最强)
traceroute -T -p 443 example.com为什么 TCP traceroute 最实用:很多防火墙和路由器会丢弃 ICMP 和高端口 UDP,但不会阻止目标端口(如 80/443)的 TCP SYN。TCP traceroute 能探测到更完整的路径。
4.2 mtr:持续路径监控
mtr 结合了 traceroute 和 ping 的功能,持续发包并统计每一跳的延迟和丢包率:
# 基本用法
mtr -rw -c 100 example.com
# TCP 模式(推荐,穿透防火墙)
mtr -rw -c 100 -T -P 443 example.com
# 报告模式(非交互,适合脚本)
mtr -rw -c 200 --json example.com > mtr-report.json典型输出:
HOST Loss% Snt Last Avg Best Wrst StDev
1. gateway.local 0.0% 100 0.4 0.5 0.3 1.2 0.2
2. 10.0.0.1 0.0% 100 1.2 1.3 1.0 2.8 0.3
3. isp-core-router.net 0.0% 100 5.4 5.6 4.8 8.2 0.7
4. peer-exchange.net 2.0% 100 12.3 15.2 11.8 45.6 8.4 ← 丢包 + 延迟抖动
5. cdn-edge.provider.net 0.0% 100 14.1 14.5 13.2 18.6 1.2
6. target.example.com 0.0% 100 14.8 15.1 13.8 19.2 1.3
mtr 结果解读规则:
某一跳丢包但后续跳无丢包:该路由器对 ICMP 限速(ICMP Rate Limiting),不是真丢包,忽略。
某一跳开始丢包且后续所有跳都丢包:该跳是真实的丢包点。上面的例子中第 4 跳丢包 2%,但第 5、6 跳丢包 0%——说明第 4 跳只是 ICMP 限速。
延迟在某一跳突然增大且后续保持高位:该跳是延迟增加的位置(可能是长距离链路或拥塞点)。
StDev(标准差)大:延迟抖动(jitter)高,说明该链路拥塞或负载不稳定。
4.3 hping3:精确延迟测量
hping3 可以用 TCP SYN 包做精确的延迟测量,不受 ICMP 限速影响:
# TCP SYN ping(最准确的 RTT 测量)
hping3 -S -p 443 -c 20 example.com
# 指定间隔和数据大小
hping3 -S -p 443 -c 100 -i u100000 example.com # 100ms 间隔
# 测量到不同端口的延迟(检测端口级别的路由差异)
hping3 -S -p 80 -c 10 example.com
hping3 -S -p 443 -c 10 example.com
hping3 -S -p 8080 -c 10 example.com输出示例:
HPING example.com (eth0 93.184.216.34): S set, 40 headers + 0 data bytes
len=46 ip=93.184.216.34 ttl=56 id=0 sport=443 flags=SA seq=0 win=65535 rtt=14.8 ms
len=46 ip=93.184.216.34 ttl=56 id=0 sport=443 flags=SA seq=1 win=65535 rtt=15.1 ms
len=46 ip=93.184.216.34 ttl=56 id=0 sport=443 flags=SA seq=2 win=65535 rtt=14.9 ms
--- example.com hping statistic ---
20 packets transmitted, 20 packets received, 0% packet loss
round-trip min/avg/max = 14.2/15.0/16.3 ms
flags=SA 表示收到了
SYN-ACK,这是服务器在线且端口开放的确认。如果收到
flags=RA(RST-ACK),端口关闭但主机可达。
4.4 路径变化检测
网络路径不是固定的,BGP 路由变化、链路故障切换、负载均衡都会导致路径变化,进而导致延迟变化。
#!/bin/bash
# path-monitor.sh — 路径变化检测
TARGET="${1:?Usage: $0 <target>}"
PREV_PATH=""
while true; do
CURRENT_PATH=$(traceroute -n -q 1 -w 2 "$TARGET" 2>/dev/null \
| awk '{print $2}' | grep -v '*' | tr '\n' ' ')
if [ -n "$PREV_PATH" ] && [ "$CURRENT_PATH" != "$PREV_PATH" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Path changed!"
echo " Old: $PREV_PATH"
echo " New: $CURRENT_PATH"
fi
PREV_PATH="$CURRENT_PATH"
sleep 60
done五、DNS 延迟分析
5.1 DNS 解析耗时测量
DNS 解析延迟经常被忽略,但它是每个新连接的第一步:
# 精确测量 DNS 解析时间
dig +stats example.com | grep "Query time"
;; Query time: 23 msec
# 测试不同 DNS 服务器的响应时间
for dns in 8.8.8.8 1.1.1.1 223.5.5.5 119.29.29.29; do
time=$(dig @$dns +stats example.com 2>/dev/null | grep "Query time" | awk '{print $4}')
echo "$dns: ${time}ms"
done输出示例:
8.8.8.8: 28ms
1.1.1.1: 12ms
223.5.5.5: 5ms
119.29.29.29: 8ms
5.2 DNS 缓存层级分析
DNS 缓存存在多个层级,每一层未命中都会增加延迟:
# 查看本地 DNS 缓存(systemd-resolved)
resolvectl statistics
# 强制不使用缓存查询(测量完整解析链路延迟)
dig +trace +stats example.com
# 查看每一级解析的延迟
dig +trace example.com 2>&1 | grep -E "(Query time|Received)"DNS 延迟优化检查清单:
| 检查项 | 命令 | 正常值 |
|---|---|---|
| 本地缓存命中率 | resolvectl statistics |
> 80% |
| 到 DNS 服务器的 RTT | ping <dns-server> |
< 10 ms |
| DNSSEC 验证时间 | dig +dnssec +stats |
< 100 ms |
| 递归解析链路延迟 | dig +trace +stats |
< 500 ms |
5.3 DNS 查询的延迟陷阱
几个 DNS 相关的延迟陷阱:
陷阱 1:AAAA 查询超时。当系统启用 IPv6 但网络不支持时,每次解析都会先查 AAAA 记录,超时后才查 A 记录,增加 2–5 秒延迟。
# 检查是否有 AAAA 超时
dig AAAA example.com +time=2
# 如果 IPv6 不可用,在 /etc/gai.conf 中禁用
echo "precedence ::ffff:0:0/96 100" >> /etc/gai.conf陷阱 2:search domain
追加。/etc/resolv.conf 中的
search
行会导致短域名被追加后缀后查询,增加无效查询。
# 查看当前 search domain
cat /etc/resolv.conf | grep search
# 高并发服务中,设置 ndots:1 减少无效查询
# /etc/resolv.conf
options ndots:1陷阱 3:连接池复用不足。每个新连接都需要 DNS 解析。如果 HTTP 连接池过小或 Keep-Alive 超时太短,DNS 解析的开销会被放大。
六、TLS 延迟分析
6.1 TLS 握手延迟分解
TLS 握手在 TCP 三次握手之后,是第二大网络延迟来源。不同 TLS 版本的握手开销:
| 版本 | 完整握手 | 恢复握手 | 0-RTT |
|---|---|---|---|
| TLS 1.2(RSA) | 2 RTT | 1 RTT (Session ID) | 不支持 |
| TLS 1.2(ECDHE) | 2 RTT | 1 RTT (Session Ticket) | 不支持 |
| TLS 1.3 | 1 RTT | 1 RTT (PSK) | 0 RTT (PSK + Early Data) |
用 openssl 测量 TLS 握手时间:
# 测量完整 TLS 握手
openssl s_client -connect example.com:443 -servername example.com \
< /dev/null 2>&1 | grep -E "(Protocol|Cipher|Verify)"
# 用 curl 精确分解
curl -w "TCP: %{time_connect}s\nTLS: %{time_appconnect}s\nDelta: $(echo '%{time_appconnect} - %{time_connect}' | bc)s\n" \
-o /dev/null -s https://example.com6.2 TLS 延迟优化检查
#!/bin/bash
# tls-latency-check.sh — TLS 延迟诊断
HOST="${1:?Usage: $0 <hostname>}"
echo "=== TLS Version ==="
echo | openssl s_client -connect "$HOST:443" -servername "$HOST" 2>/dev/null \
| grep "Protocol :"
echo ""
echo "=== Certificate Chain Length ==="
echo | openssl s_client -connect "$HOST:443" -servername "$HOST" -showcerts 2>/dev/null \
| grep -c "BEGIN CERTIFICATE"
echo ""
echo "=== OCSP Stapling ==="
echo | openssl s_client -connect "$HOST:443" -servername "$HOST" -status 2>/dev/null \
| grep -A 2 "OCSP Response Status"
echo ""
echo "=== Session Resumption ==="
# 第一次连接,保存 session
openssl s_client -connect "$HOST:443" -servername "$HOST" \
-sess_out /tmp/tls_session 2>/dev/null < /dev/null > /dev/null
# 第二次连接,尝试恢复
REUSED=$(openssl s_client -connect "$HOST:443" -servername "$HOST" \
-sess_in /tmp/tls_session 2>/dev/null < /dev/null | grep "Reused")
if [ -n "$REUSED" ]; then
echo "Session resumption: SUPPORTED"
else
echo "Session resumption: NOT SUPPORTED"
fi
rm -f /tmp/tls_session常见 TLS 延迟问题与解决:
| 问题 | 延迟影响 | 解决方案 |
|---|---|---|
| 仍在用 TLS 1.2 | 多 1 RTT | 升级到 TLS 1.3 |
| 证书链过长(> 3 级) | 额外验证时间 | 优化证书链,移除不必要的中间证书 |
| 无 OCSP Stapling | CRL/OCSP 查询 0.1–2s | 启用 OCSP Stapling |
| 无 Session 恢复 | 每次完整握手 | 配置 Session Ticket 或 PSK |
| ECDSA 未启用 | RSA 签名慢 | 使用 ECDSA P-256 证书 |
七、服务端延迟分析
7.1 TTFB 分解
TTFB(Time To First Byte)包含了网络延迟和服务端处理时间。分离两者:
服务端实际处理时间 = TTFB - TCP_RTT - TLS_Handshake_Time
如果 TTFB 是 500 ms,TCP RTT 是 15 ms,TLS 握手 30 ms,那么服务端处理时间是 455 ms。
用 Wireshark 精确测量服务端处理时间:
1. 过滤 HTTP 请求: http.request
2. 找到请求的最后一个 TCP 段(携带 HTTP 请求体的段)
3. 找到响应的第一个 TCP 段(HTTP 200 OK 的第一个段)
4. 两者的时间差 = 服务端处理时间 + 1 个单程传播延迟
更精确的方法——用 tshark 自动计算:
tshark -r capture.pcap \
-Y "http.request || http.response" \
-T fields \
-e frame.time_relative \
-e http.request.method \
-e http.request.uri \
-e http.response.code \
-e http.timehttp.time 字段是 Wireshark
自动计算的请求-响应间隔时间。
7.2 应用层延迟追踪
当确定瓶颈在服务端时,需要进一步分解:
服务端处理 = 框架开销 + 业务逻辑 + 数据库查询 + 外部调用 + 序列化
用 Go 实现一个延迟追踪中间件:
package middleware
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
type spanKey struct{}
type Span struct {
Name string
Start time.Time
Duration time.Duration
children []*Span
mu sync.Mutex
}
func NewSpan(name string) *Span {
return &Span{Name: name, Start: time.Now()}
}
func (s *Span) End() {
s.Duration = time.Since(s.Start)
}
func (s *Span) Child(name string) *Span {
child := NewSpan(name)
s.mu.Lock()
s.children = append(s.children, child)
s.mu.Unlock()
return child
}
func (s *Span) String() string {
var b strings.Builder
s.format(&b, 0)
return b.String()
}
func (s *Span) format(b *strings.Builder, depth int) {
indent := strings.Repeat(" ", depth)
fmt.Fprintf(b, "%s%s: %v\n", indent, s.Name, s.Duration)
for _, child := range s.children {
child.format(b, depth+1)
}
}
func LatencyTrace(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
root := NewSpan("request")
ctx := context.WithValue(r.Context(), spanKey{}, root)
next.ServeHTTP(w, r.WithContext(ctx))
root.End()
if root.Duration > 200*time.Millisecond {
fmt.Printf("[SLOW REQUEST] %s %s\n%s",
r.Method, r.URL.Path, root.String())
}
})
}
func SpanFromContext(ctx context.Context) *Span {
if s, ok := ctx.Value(spanKey{}).(*Span); ok {
return s
}
return NewSpan("unknown")
}
func TraceDB(ctx context.Context, query string) func() {
span := SpanFromContext(ctx).Child("db: " + query)
return func() { span.End() }
}使用示例:
func handleGetUser(w http.ResponseWriter, r *http.Request) {
// 数据库查询
done := middleware.TraceDB(r.Context(), "SELECT * FROM users")
user, err := db.QueryUser(r.Context(), userID)
done()
// 外部 API 调用
span := middleware.SpanFromContext(r.Context()).Child("external-api")
profile, err := fetchProfile(r.Context(), user.ID)
span.End()
// 序列化
span = middleware.SpanFromContext(r.Context()).Child("json-marshal")
data, _ := json.Marshal(response)
span.End()
w.Write(data)
}慢请求的输出:
[SLOW REQUEST] GET /api/users/12345
request: 523ms
db: SELECT * FROM users: 312ms ← 瓶颈在数据库
external-api: 156ms
json-marshal: 2ms
八、内核协议栈延迟追踪
8.1 用 bpftrace 追踪内核网络延迟
当延迟不在应用层时,可能藏在内核协议栈中。用 bpftrace 追踪关键路径:
#!/usr/bin/env bpftrace
// net-latency.bt — 追踪 TCP 收包到应用读取的延迟
kprobe:tcp_v4_rcv
{
@recv_time[arg0] = nsecs;
}
kretprobe:tcp_recvmsg
/@ recv_time[arg0]/
{
$latency_us = (nsecs - @recv_time[arg0]) / 1000;
@latency_hist = hist($latency_us);
if ($latency_us > 1000) {
printf("HIGH LATENCY: %d us, pid=%d comm=%s\n",
$latency_us, pid, comm);
}
delete(@recv_time[arg0]);
}
END
{
print(@latency_hist);
}8.2 软中断延迟
网络包从网卡到内核协议栈要经过软中断(softirq)处理。如果 softirq 被延迟,所有网络包的处理都会延迟:
# 查看 softirq 处理时间分布
sudo bpftrace -e '
tracepoint:irq:softirq_entry /args->vec == 3/ { @start[tid] = nsecs; }
tracepoint:irq:softirq_exit /args->vec == 3 && @start[tid]/ {
@us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'vec == 3 是
NET_RX(网络接收)软中断。如果直方图显示大量 > 100 μs
的值,说明 CPU 负载过高或中断亲和设置不当。
8.3 中断亲和与 RPS 检查
# 查看网卡中断分布
cat /proc/interrupts | grep eth0
# 查看 RPS 配置
for f in /sys/class/net/eth0/queues/rx-*/rps_cpus; do
echo "$f: $(cat $f)"
done
# 查看 softirq 在各 CPU 上的分布
awk '/NET_RX/ {for(i=2;i<=NF;i++) printf "CPU%d: %s ", i-2, $i; print ""}' \
/proc/softirqs如果所有 NET_RX softirq 集中在 CPU 0 上,其他 CPU 空闲,需要配置 RPS(Receive Packet Steering)或设置中断亲和:
# 为 eth0 的 rx-0 队列启用 RPS,分散到 CPU 0-7
echo "ff" > /sys/class/net/eth0/queues/rx-0/rps_cpus
# 设置中断亲和(将 eth0 的 IRQ 绑定到 CPU 2)
IRQ=$(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':')
echo 4 > /proc/irq/$IRQ/smp_affinity # 0x4 = CPU 2九、端到端延迟监控
9.1 Prometheus 延迟指标
建立持续的延迟监控,用直方图(Histogram)记录延迟分布:
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
)
var (
HTTPRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: []float64{
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10,
},
},
[]string{"method", "path", "status"},
)
DNSResolveDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "dns_resolve_duration_seconds",
Help: "DNS resolution duration in seconds",
Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5},
},
)
TCPConnectDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "tcp_connect_duration_seconds",
Help: "TCP connection establishment duration",
Buckets: []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.5, 1},
},
[]string{"remote_addr"},
)
TLSHandshakeDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "tls_handshake_duration_seconds",
Help: "TLS handshake duration in seconds",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1},
},
)
)
func init() {
prometheus.MustRegister(
HTTPRequestDuration,
DNSResolveDuration,
TCPConnectDuration,
TLSHandshakeDuration,
)
}9.2 Blackbox Exporter 端到端探测
Prometheus Blackbox Exporter 可以做完整的 HTTP 延迟分解探测:
# blackbox.yml
modules:
http_2xx:
prober: http
timeout: 15s
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
valid_status_codes: [200]
method: GET
preferred_ip_protocol: ip4
ip_protocol_fallback: false
tcp_connect:
prober: tcp
timeout: 10s
tcp:
preferred_ip_protocol: ip4
icmp_rtt:
prober: icmp
timeout: 5s
icmp:
preferred_ip_protocol: ip4Prometheus 配置:
# prometheus.yml
scrape_configs:
- job_name: 'blackbox-http'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://api.example.com/health
- https://web.example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115Blackbox Exporter 暴露的关键延迟指标:
| 指标 | 含义 |
|---|---|
probe_dns_lookup_time_seconds |
DNS 解析延迟 |
probe_ip_protocol |
使用的 IP 协议版本 |
probe_tls_version_info |
TLS 版本信息 |
probe_http_duration_seconds{phase="connect"} |
TCP 连接延迟 |
probe_http_duration_seconds{phase="tls"} |
TLS 握手延迟 |
probe_http_duration_seconds{phase="processing"} |
服务端处理延迟 |
probe_http_duration_seconds{phase="transfer"} |
响应传输延迟 |
probe_duration_seconds |
总延迟 |
9.3 延迟告警规则
# alerting-rules.yml
groups:
- name: latency_alerts
rules:
- alert: HighP99Latency
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket{path!~"/health.*"}[5m])
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "P99 延迟超过 2 秒"
description: "{{ $labels.path }} 的 P99 延迟为 {{ $value | humanizeDuration }}"
- alert: DNSResolutionSlow
expr: probe_dns_lookup_time_seconds > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "DNS 解析延迟超过 500ms"
description: "目标 {{ $labels.instance }} 的 DNS 解析耗时 {{ $value | humanizeDuration }}"
- alert: TLSHandshakeSlow
expr: |
probe_http_duration_seconds{phase="tls"} > 1
for: 5m
labels:
severity: warning
annotations:
summary: "TLS 握手延迟超过 1 秒"十、延迟分析实战案例
10.1 案例一:API 响应 P99 突然升高
现象:API 的 P99 延迟从 200 ms 升到 2 秒,P50 不变。
分析过程:
# 1. curl 延迟分解
curl -w "DNS:%{time_namelookup} TCP:%{time_connect} TLS:%{time_appconnect} TTFB:%{time_starttransfer} Total:%{time_total}\n" \
-o /dev/null -s https://api.example.com/data
# DNS:0.002 TCP:0.015 TLS:0.048 TTFB:0.065 Total:0.072
# → 正常请求没问题
# 2. 多次采样发现偶发慢请求
for i in $(seq 1 50); do
curl -w "%{time_starttransfer}\n" -o /dev/null -s https://api.example.com/data
done | sort -n | tail -5
# 0.065
# 0.071
# 0.068
# 1.856 ← 偶发
# 2.134 ← 偶发
# 3. 慢请求的 TTFB 高 → 瓶颈在服务端
# 4. 检查服务端日志
grep "duration_ms.*[0-9]\{4,\}" /var/log/app/access.log | tail -20
# → 发现慢请求都在查询同一个数据库表
# 5. 检查数据库
mysql -e "SHOW PROCESSLIST" | grep -v Sleep
# → 发现大量 Waiting for table metadata lock
# 6. 根因:一个长事务持有表锁
mysql -e "SELECT * FROM information_schema.INNODB_TRX ORDER BY trx_started"结论:P99 升高是因为数据库表锁导致部分查询被阻塞。网络层完全正常。
10.2 案例二:跨地域访问延迟高
现象:广州用户访问北京服务器,延迟 300 ms,预期 30 ms。
分析过程:
# 1. mtr 路径分析
mtr -rw -c 50 -T -P 443 bj-server.example.com
# HOST Loss% Snt Last Avg Best Wrst StDev
# 1. gw-gz.isp.net 0.0% 50 0.5 0.6 0.3 1.2 0.2
# 2. core-gz.isp.net 0.0% 50 2.1 2.3 1.8 4.5 0.5
# 3. backbone-gz.isp.net 0.0% 50 5.2 5.5 4.8 8.1 0.7
# 4. peer-sh.isp.net 0.0% 50 45.3 46.1 44.2 52.3 1.8 ← 广州→上海
# 5. backbone-sh.isp.net 0.0% 50 46.8 47.2 45.1 53.6 1.9
# 6. peer-us.transit.net 0.0% 50 185.2 186.8 183.1 195.4 2.8 ← 上海→美国!
# 7. peer-us2.transit.net 0.0% 50 186.1 187.5 184.2 196.8 2.9
# 8. peer-bj.isp.net 0.0% 50 278.3 280.1 275.4 295.2 4.2 ← 美国→北京
# 9. bj-server.example.com 0.0% 50 279.8 281.5 276.8 297.1 4.5
# 2. 发现流量从上海绕道美国再回北京!
# 这是 BGP 路由问题——ISP 的对等互联策略导致的
# 3. 验证:直接 traceroute 看路径
traceroute -T -p 443 bj-server.example.com结论:ISP 的 BGP 路由把流量绕到了美国。解决方案:联系运营商修正路由,或使用 CDN / 专线。
10.3 案例三:TLS 握手耗时 2 秒
现象:首次连接特别慢,后续请求正常。
# 1. curl 分解
curl -w "TCP:%{time_connect} TLS:%{time_appconnect}\n" \
-o /dev/null -s https://slow.example.com
# TCP:0.015 TLS:2.134
# TLS 握手耗时 2.119 秒!
# 2. openssl 诊断
echo | openssl s_client -connect slow.example.com:443 \
-servername slow.example.com -status 2>&1 | head -30
# Protocol: TLSv1.2 ← 还在用 TLS 1.2
# OCSP Response Status: ...没有响应... ← 没有 OCSP Stapling
# 3. 检查证书链
echo | openssl s_client -connect slow.example.com:443 \
-servername slow.example.com -showcerts 2>/dev/null \
| grep -c "BEGIN CERTIFICATE"
# 5 ← 证书链有 5 级,过长
# 4. 检查 OCSP 响应
openssl s_client -connect slow.example.com:443 \
-servername slow.example.com -status 2>&1 | grep "OCSP"
# OCSP response: no response sent ← 客户端需要自行查询 OCSP根因:TLS 1.2(2 RTT 握手)+ 无 OCSP Stapling(客户端查询 OCSP 超时 2 秒)+ 证书链过长。
修复:
- 升级到 TLS 1.3(1 RTT)
- 启用 OCSP Stapling
- 优化证书链(移除不必要的中间证书)
- 配置 Session Ticket 实现恢复握手
十一、延迟分析工具对比
| 工具 | 层级 | 适用场景 | 优势 | 限制 |
|---|---|---|---|---|
| curl -w | HTTP | 快速延迟分解 | 无需安装,分解精确 | 只能测 HTTP |
| ping | ICMP | 基础连通性和 RTT | 简单直接 | 被防火墙阻挡,不反映真实路径延迟 |
| hping3 | TCP/UDP/ICMP | 精确端口级 RTT | TCP SYN 穿透防火墙 | 需要 root |
| mtr | ICMP/TCP/UDP | 路径逐跳分析 | 持续监控,统计完整 | 某些跳对 ICMP 限速 |
| traceroute | ICMP/UDP/TCP | 路径发现 | 广泛可用 | 单次采样,不够精确 |
| Wireshark/tshark | 全栈 | 深度协议分析 | 逐包精确测量 | 需要抓包文件 |
| bpftrace | 内核 | 内核协议栈延迟 | 无额外开销 | 需要内核 BTF 支持 |
| Blackbox Exporter | HTTP/TCP/ICMP | 持续监控 | 集成 Prometheus | 需要部署 |
十二、总结:延迟分析方法论
延迟分析的核心方法论可以总结为五个步骤:
量化:不要接受”慢”这个模糊描述。用 curl、Prometheus 量化 P50/P95/P99 延迟,确认”多慢”和”哪些请求慢”。
分解:把端到端延迟拆成 DNS → TCP → TLS → Server → Transfer 五个阶段。用 curl -w 或 Blackbox Exporter 做自动分解。
定位:确定瓶颈阶段后,用对应工具深入。网络延迟用 mtr/hping3,服务端延迟用应用 trace,内核延迟用 bpftrace。
对比:与基线对比。当前的 P99 延迟相比昨天/上周是上升还是下降?什么时间点开始恶化?关联变更记录(部署、配置变更、网络变更)。
监控:修复后建立持续监控和告警。延迟问题往往会复发,告警规则比一次性排查更有价值。
最重要的经验:大多数”网络慢”的问题不在网络——而在 DNS、TLS 配置或服务端处理。先做延迟分解,再决定往哪个方向深入。盲目抓包是最低效的排查方式。
参考文献
- Grigorik, I. (2013). High Performance Browser Networking, O’Reilly Media
- RFC 9293: Transmission Control Protocol (TCP), IETF
- Mathis, M. et al. (1997). The Macroscopic Behavior of the TCP Congestion Avoidance Algorithm, ACM SIGCOMM
- Prometheus Blackbox Exporter Documentation (github.com/prometheus/blackbox_exporter)
- Brendan Gregg (2019). BPF Performance Tools, Addison-Wesley
上一篇:Wireshark 深度分析:流图、专家信息与协议解析
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】TCP 问题诊断实战:重传、RST 与窗口异常
TCP 问题排查是后端工程师的日常。本文系统梳理重传、RST、窗口异常三大类 TCP 问题的诊断方法,通过 ss、netstat、tcpdump、Wireshark 等工具链配合 Prometheus 指标采集,建立完整的 TCP 故障诊断体系。
【网络工程】网络性能基准测试:iperf3、netperf 与测试方法论
网络性能测试不是跑个 iperf3 就完了。本文系统讲解网络性能基准测试的方法论——带宽、延迟、丢包率、PPS 的正确测量方式,iperf3 的参数选择与结果解读,netperf 的请求-响应测试,hping3/sockperf 的延迟测量,以及基准测试的统计方法和常见陷阱。
【网络工程】零拷贝网络:sendfile、splice 与 MSG_ZEROCOPY
数据从磁盘到网卡的传统路径涉及 4 次拷贝和多次上下文切换。本文系统剖析 sendfile、splice、vmsplice、MSG_ZEROCOPY 四种零拷贝技术的内核实现、适用场景与性能差异,并以 Kafka 和 Nginx 为案例分析零拷贝在生产系统中的工程实践。
【网络工程】代理性能调优:缓冲、Keepalive 与连接复用
系统剖析反向代理层的性能瓶颈与调优方法——Proxy Buffer 内存控制、上下游 Keepalive 参数协调、HTTP/2 连接复用行为、代理层 CPU/内存/连接数监控与容量规划。