网站时快时慢排查实战:用 tcpdump 与 ss -ti 分清网络丢包、出口瓶颈与后端慢

网站「有时快有时慢」:先用 tcpdump 分清是网络还是后端

个人站长最头疼的一类故障是「间歇性变慢」:不是全天慢,而是某几个时段某个页面莫名其妙要等七八秒;top 里 CPU 和内存都很正常,Nginx 的 access.log 里 $request_time 也确实高,但你看不出原因。这时候绝大多数人的做法是在应用层反复加日志、重启服务,结果转了一圈什么都没找到。

正确顺序是:先分层,再定位。而 tcpdump 就是在「网络层」这一层做判决的工具。它回答的问题很具体:这些慢请求的耗时,是消耗在「数据包在路上丢了反复重传」,还是「服务端收到了但后端处理慢」?分清了这一点,后面所有的排查方向都不会跑偏。

这篇文章不讲 tcpdump 的语法大全,只讲网站排障真正会用到的那几个过滤器和判读方法,以及 tcpdump 与 ss -ti 配合的实战套路。

开工前:确认你真的有抓包的权限和空间

两个前置条件经常被忽略,导致抓包本身制造出新的故障。

第一,确认有 CAP_NET_RAW 权限。非 root 用户抓包需要能力位,否则报 You don't have permission to capture on that device。最干净的做法是给 tcpdump 二进制加能力:

# 让普通用户也能抓包(比给 sudo 更细粒度)
setcap cap_net_raw,cap_net_admin=eip /usr/sbin/tcpdump
getcap /usr/sbin/tcpdump
# 输出:/usr/sbin/tcpdump cap_net_admin,cap_net_raw=eip

注意:每次 tcpdump 包升级(apt upgrade)之后能力位会被重置,需要重新执行。getcap 查一下就能确认。

第二,算好磁盘空间。这是最容易翻车的地方:在流量大的机器上不加限制地抓包,10 分钟就能写满几十 GB。永远用 -w 写文件而不是直接打在屏幕上,并且同时用 -c 限制包数或 -W/-G 做环形覆盖:

# 安全抓包:最多 5000 个包,单个文件 100MB,最多 5 个文件循环覆盖
tcpdump -i eth0 -w /tmp/cap.pcap -c 5000
# 或者按时间+大小滚动(生产环境推荐)
tcpdump -i eth0 -w /tmp/cap_%Y%m%d_%H%M%S.pcap -G 300 -W 5 -z gzip

-G 300 是每 300 秒滚动一个新文件,-W 5 是只保留 5 个文件循环覆盖,-z gzip 是抓完自动 gzip 压缩。这三个参数组合起来,就能保证抓包永远不会把磁盘写满——它们比单纯 -c 更实用,因为排障往往要抓一段时间的「间歇性」故障,包数不好预估。

四个真正有用的过滤器

网站排障不需要记住 tcpdump 的全部语法,掌握下面四个过滤器就够了。

1. 只抓某个端口的某个来源 IP:

tcpdump -i eth0 -nn -s0 'host 203.0.113.45 and port 443'
# -nn 不解析域名和端口名(快,且避免 DNS 反查拖慢抓包)
# -s0 抓完整包(老版本默认只抓 68 字节,看不到 payload)

-s0(或者 -s 0、-s 262144)非常重要。旧版 tcpdump 默认 snaplen 只有 68 字节,只够看 TCP 头,看不到 HTTP 内容。如果你要分析请求内容,必须显式指定。

2. 只看 TCP 标志位(比如只看 SYN 和 RST):

# 只看 SYN 包(分析连接是否被建立)
tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'

# 只看 RST 包(连接被拒绝/重置)
tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-rst != 0'

RST 是排障里最有价值的信号之一。大量的 RST 通常意味着三种情况:服务端 backlog 满了(somaxconn 太小,Nginx 全连接队列溢出会直接丢 SYN 或回 RST)、防火墙 REJECT 规则(而不是 DROP)、或者中间设备(WAF/CDN)主动切断。区分方法:如果是 REJECT,能抓到 RST;如果是 DROP,客户端只能看到 SYN 重传却收不到任何回应。

3. 直接看 HTTP 请求(明文端口用):

tcpdump -i eth0 -nn -s0 -A 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

这串看起来很吓人的过滤器作用是「只打印有 payload 的包」(排除纯 ACK),-A 表示用 ASCII 打印内容。用它可以快速确认某个请求到底有没有真正到达服务器。

4. 只抓「慢」的流:tcpdump 本身没有「只抓慢请求」的能力,因为它是无状态的。但你可以用 ss 先找出哪个连接慢,拿到端口后再用 tcpdump 定向抓:

# 列出所有已建立的连接及其 TCP 内部指标
ss -tin state established '( sport = :443 )' | head -40

ss -ti 的输出里包含 rtt、cwnd、retrans、bytes_retrans,这是判断「是不是网络重传导致慢」的关键。下面专门讲怎么读。

核心判读:重传率才是「网络慢」的铁证

抓包之后,不要手动一个个数包,用 tcpdump 的输出配合 ss 统计效率高得多。判读分两条线:

线一:用 ss -ti 看单条连接的 TCP 内部状态。

ss -tin state established | awk 'NR%2==1{print} NR%2==0{
  match($0,/rtt:([0-9.]+)/,a); match($0,/retrans:([0-9]+)\/([0-9]+)/,b);
  print "  rtt="a[1]" retrans="b[1]"/"b[2]
}'

关键字段解读:

  • rtt:往返时延。正常国内 10–50ms,跨洋 150–250ms。如果 rtt 突然出现 rtt:1200/300 这种格式,斜杠后面是 rttvar(时延波动),rttvar 大到接近 rtt 本身,说明网络抖动严重。
  • retrans:形如 retrans:0/12,斜杠前是「当前正在重传的包数」,斜杠后是「累计重传总数」。斜杠后的数字持续增长 = 有真实丢包。重传率 = 累积重传 / 总发送包,超过 1% 就值得警惕,超过 5% 就是故障级。
  • cwnd:拥塞窗口。一直很小(比如个位数)说明发不出去,通常伴随重传。
  • send / bytes_sent / bytes_retrans:发送队列积压和已发送字节数。如果 send 队列持续非零(形如 send 1234567),说明对端接收窗口小或者本机网卡发不出去,这是「本地出口带宽打满」的典型表现。

线二:用 tcpdump 抓到的包做重传判定。

# 统计抓到的包中「重传」的数量(tcpdump 会标注,但要配合 -tttt 看时间)
tcpdump -r /tmp/cap.pcap -nn 'tcp port 443' -tttt 2>/dev/null | head -50

更实用的是直接用 tshark(wireshark 的命令行版)做统计,如果机器上装了:

# 计算 TCP 重传率
tshark -r /tmp/cap.pcap -q -z io,stat,0,"COUNT(tcp.analysis.retransmission)tcp"

没有 tshark 也不影响。tcpdump 自身在启用 -v 时会标注 [TCP Retransmission]、[TCP Dup ACK]、[TCP Out-of-order] 之类的注释,把输出重定向到文件再 grep 统计就行:

tcpdump -i eth0 -nn -v 'tcp port 443' 2>&1 | grep -c 'Retransmission'

三个典型现象与它们的结论

把上面的工具组合起来,实战中会遇到三种典型模式,每种指向完全不同的根因:

模式 A:抓包显示大量重传,ss 里 rtt 抖动大。结论是网络质量问题——要么是链路本身(跨洋/跨运营商、走的路由绕路),要么是中间设备丢包。这种慢不是你服务器的问题,优化方向是换线路、上 BGP 机房、或者接个 CDN 把回源和用户接入分开。在服务器上做任何调优都是白费功夫。

模式 B:几乎没有重传,但 ss 显示 send 队列持续堆积、cwnd 很小。结论是本机出口带宽打满或者网卡有瓶颈。查法:

# 看网卡实时流量和历史累计
sar -n DEV 1 5
# 或者
ip -s link show eth0
# 重点看 dropped / overruns / fifo 三个计数是否增长

dropped 持续增长通常是 网卡环形缓冲区(ring buffer)太小——瞬间流量峰值时被丢弃,重传也随之而来。用 ethtool -S eth0 | grep -i drop 看具体是哪个队列丢的,ethtool -g eth0 看当前 ring 大小,适当调大能缓解。

模式 C:重传不多,但每个请求的 TCP 三次握手就很慢(SYN 到 SYN-ACK 之间隔了几百毫秒)。结论是DNS 解析慢或者 accept 队列满。查 DNS 用 dig +stats,查队列用 ss -lnt 看 Recv-Q:

ss -lnt
# State  Recv-Q  Send-Q  Local Address:Port
# LISTEN    129     511    0.0.0.0:443
#  ↑ Recv-Q 在 LISTEN 状态下代表「全连接队列已满、尚未被 accept 的连接数」
#    持续不为 0 = 应用 accept 不过来,需要调大 somaxconn 和 nginx backlog

这个 Recv-Q 的语义非常反直觉(在 LISTEN 状态下它不是你熟悉的数据接收队列,而是「已完成三次握手但应用还没 accept 的连接数」),它是判断「连接排队」最直接的一个数,很多人查了半天 Nginx 配置却没看这个。调优方式是在 /etc/sysctl.conf 里加大内核队列,并在 Nginx 的 server 块配置 backlog:

# /etc/sysctl.conf
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
net.core.netdev_max_backlog = 8192

# nginx.conf
listen 443 ssl backlog=4096;

注意内核参数改了要 sysctl -p,而且 somaxconn 是上限——Nginx 里 backlog 写得再大,超过 somaxconn 也没用,两个要一起改。

抓包排障的正确姿势:先想假设,再抓包

最后说一个方法论上的坑。不要在没有任何假设的情况下就 tcpdump -i eth0 全量抓。大流量机器上这样抓,几秒内就是几百万个包,你打开 Wireshark 也看不出任何东西,纯粹浪费时间和磁盘。

正确流程是「先分层假设 → 定向抓包 → 用 ss 交叉验证」:

  1. 先看 Nginx 的 $request_time 和 $upstream_response_time。如果请求总时间高但 upstream 时间低,说明慢在网络传输或客户端;如果 upstream 时间也高,说明慢在后端(PHP/MySQL),根本不用抓包,直接去查慢日志。这一步就能筛掉一大半「其实是后端慢」的情况。
  2. 确认确实要抓网络,就限定 host + port,配合 ss -ti 找到可疑连接,只抓那条流。
  3. 抓完立刻用 -r 读文件分析(而不是边抓边看),并且只在 /tmp 或专用目录操作,做完删掉。
  4. 抓包期间不要用 -w 写到大文件系统上,避免抓包本身把磁盘写满,制造出第二个故障。用前面说的 -G/-W/-z gzip 滚动方案最安全。

还有一点:如果站点在 CDN 后面,直接在源站抓包看到的都是 CDN 回源节点的 IP,不是真实用户。这时候「网络慢」很可能是「用户到 CDN 那一段慢」,源站抓包是抓不到的。要先在源站判明「回源这一段是否正常」,再决定要不要联系 CDN 服务商看边缘数据。

小结

把这篇的要点压成一张清单,下次遇到「时快时慢」可以照着走:

  1. 先用 $request_time vs $upstream_response_time 分层,别一上来就抓包。
  2. 抓包三件事:-nn 不反解、-s0 抓全包、-G/-W/-z gzip 防磁盘写满。
  3. 用 ss -ti 读 rtt/retrans/cwnd/send 四项,重传率 > 1% 就是网络问题。
  4. 用 ss -lnt 看 LISTEN 状态的 Recv-Q,它代表「握手完成但未 accept」的连接数。
  5. 结论分三类:重传多 = 网络质量;send 堆积 = 出口带宽/网卡 ring;握手慢 = DNS 或 accept 队列。
  6. CDN 后面的站点,源站抓包只能看到回源流量,用户侧问题要另找数据源。

tcpdump 的价值不在于它多强大,而在于它能把「慢」这个模糊的体感,变成一个可证伪的假设——是网络丢包,还是本地瓶颈,还是后端处理慢。分清了这三者,你才知道力气该往哪使,而不是在服务器上反复重启服务碰运气。

Last modification:September 26th, 2026 at 09:26 pm

Leave a Comment