Linux 基础体系 · 第 64/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux TCP 诊断:ss、tcpdump、重传、SYN 队列、TIME_WAIT 和延迟

TCP 故障通常不是“端口通不通”这么简单。一个连接可能已经完成三次握手,却长期停留在 Recv-Q;应用看起来没有报错,但抓包显示数据不断重传;服务器监听端口正常,却因为监听队列或 accept() 处理不及时拒绝新连接;客户端请求耗时升高,根因却不是服务端处理慢,而是丢包后的 RTO(Retransmission Timeout,重传超时)退避。

诊断 TCP 时,需要同时观察四个层次:

  1. 内核 socket 状态:连接处于什么状态,发送和接收队列积压多少,TCP 当前估计的 RTT、RTO、拥塞窗口是什么。
  2. 线上报文:SYN、SYN-ACK、ACK、数据段和重传是否按预期出现。
  3. 队列与并发:SYN 队列、accept 队列、应用读取速度、发送缓冲区是否成为瓶颈。
  4. 时间关系:连接建立耗时、数据确认耗时、重传退避、TIME_WAIT 生命周期和应用自身处理延迟。

本文中的命令面向现代主流 Linux。部分字段依赖内核和 iproute2 版本,抓包通常需要 root 或 CAP_NET_RAWCAP_NET_ADMIN 权限;生产环境执行抓包、修改 sysctl 或终止连接前,应先确认影响范围。


一、先建立 TCP 诊断模型

1. TCP 连接由四元组标识

一个 TCP 连接通常由以下四元组唯一确定:

(源 IP,源端口,目的 IP,目的端口)(\text{源 IP}, \text{源端口}, \text{目的 IP}, \text{目的端口})

例如:

10.0.0.15:43120 -> 10.0.0.20:443

服务端监听的是本地地址和端口,例如 0.0.0.0:443。真正建立的连接则包含客户端的源 IP 和临时端口。

因此,以下现象必须区分:

  • 0.0.0.0:443 LISTEN:监听 socket 存在;
  • 10.0.0.15:43120 -> 10.0.0.20:443 SYN-SENT:客户端正在建立连接;
  • 10.0.0.15:43120 -> 10.0.0.20:443 ESTAB:连接已经建立;
  • 大量 TIME-WAIT:连接已经关闭,但内核仍保留短期状态。

“端口在监听”只能说明监听 socket 存在,不能证明新连接能及时完成握手,也不能证明应用及时读取或处理数据。

2. TCP 状态转换

简化后的主动连接和被动连接过程如下:

stateDiagram-v2
    [*] --> CLOSED
    CLOSED --> SYN_SENT: connect() / 发送 SYN
    SYN_SENT --> ESTABLISHED: 收到 SYN-ACK,发送 ACK
    SYN_SENT --> CLOSED: 超时或收到 RST

    CLOSED --> LISTEN: listen()
    LISTEN --> SYN_RECV: 收到 SYN,发送 SYN-ACK
    SYN_RECV --> ESTABLISHED: 收到最终 ACK
    SYN_RECV --> CLOSED: 超时、RST 或队列处理失败

    ESTABLISHED --> FIN_WAIT_1: 主动 close()
    FIN_WAIT_1 --> FIN_WAIT_2: 收到 ACK
    FIN_WAIT_2 --> TIME_WAIT: 收到对端 FIN,发送 ACK
    TIME_WAIT --> CLOSED: 等待结束

    ESTABLISHED --> CLOSE_WAIT: 收到对端 FIN
    CLOSE_WAIT --> LAST_ACK: 应用 close()
    LAST_ACK --> CLOSED: 收到 ACK

这张图省略了 CLOSINGLAST-ACK 等交叉关闭路径,但足以用于诊断:

  • 客户端大量 SYN-SENT:SYN 可能没有到达服务端,或者 SYN-ACK 没有返回;
  • 服务端大量 SYN-RECV:服务端收到了 SYN,但没有收到最终 ACK,或者半连接请求处理不过来;
  • 服务端大量 CLOSE-WAIT:对端已经关闭,服务端应用没有调用 close()
  • 客户端大量 TIME-WAIT:客户端通常是主动关闭方,短连接模式可能造成端口和内核状态压力。

二、ss:从内核视角观察 TCP

ss 来自 iproute2,读取内核 socket 信息,比传统 netstat 更适合现代 Linux。基础命令如下:

ss -ltn

常见输出:

State  Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0      4096   0.0.0.0:443       0.0.0.0:*

各列含义取决于 socket 状态:

  • LISTEN socket:
    • Recv-Q:当前已经完成握手、等待应用 accept() 的连接数;
    • Send-Q:监听队列允许的上限,实际值还受内核上限影响。
  • 对已建立连接:
    • Recv-Q:已经由内核接收、但应用尚未读取的数据;
    • Send-Q:应用已经写入内核、但尚未被对端确认或发送完成的数据。

这两个场景不能混用。例如,监听 socket 的 Recv-Q 不是“收到多少字节数据”,而是等待应用接收的已建立连接数量。

1. 查看监听端口和进程

sudo ss -ltnp

示例:

LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("myserver",pid=1842,fd=7))

-p 显示进程信息,通常需要权限。若没有权限,可能只能看到地址和状态。

检查 TCP、UDP 以及 Unix socket:

ss -lntup
ss -lx

ss -lntup 中:

  • -l:只显示监听;
  • -n:不做 DNS 和服务名解析;
  • -t:TCP;
  • -u:UDP;
  • -p:进程。

诊断时通常应使用 -n,否则反向 DNS 或服务名查询会增加等待并混淆现场。

2. 查看连接状态分布

ss -tan

按状态统计:

ss -tanH | awk '{print $1}' | sort | uniq -c | sort -nr

示例:

    812 TIME-WAIT
    124 ESTAB
     37 SYN-RECV
      6 FIN-WAIT-2
      2 LISTEN

这里的统计只能说明 socket 状态数量,不能直接证明故障原因:

  • SYN-RECV 多,可能是正常高并发,也可能是客户端 ACK 丢失、网络攻击或服务端队列不足;
  • TIME-WAIT 多,可能只是短连接客户端的正常结果;
  • ESTAB 多,可能是长连接设计,也可能是连接泄漏;
  • FIN-WAIT-2 多,常见于对端不发送 FIN,但还需要结合应用和网络行为判断。

查看特定状态:

ss -ant state syn-recv
ss -ant state time-wait
ss -ant state established

统计 TIME_WAIT 数量:

ss -tanH state time-wait | wc -l

按远端地址聚合:

ss -tanH state time-wait \
  | awk '{print $5}' \
  | sed 's/:[^:]*$//' \
  | sort | uniq -c | sort -nr | head

IPv6 地址包含多个冒号,简单的字符串切分可能不适用于所有格式;严谨分析时应使用结构化工具或明确限定地址族。

3. 查看单个连接的 TCP 内部信息

sudo ss -tinp

典型输出可能包含:

ESTAB 0 0 10.0.0.15:43120 10.0.0.20:443
 cubic wscale:7,7 rto:204 rtt:12.345/2.100 ato:40 mss:1448
 pmtu:1500 rcvmss:1448 advmss:1460 cwnd:15 bytes_sent:8320
 bytes_retrans:0 bytes_acked:8320 bytes_received:12000
 segs_out:18 segs_in:21 data_segs_out:9 data_segs_in:11

常见字段:

  • rtt:x/y:平滑 RTT 和 RTT 方差,单位通常为毫秒;
  • rto:当前重传超时估计;
  • cwnd:拥塞窗口,以 TCP 段为单位;
  • mss:最大报文段大小;
  • pmtu:路径 MTU;
  • bytes_retrans:重传字节数;
  • bytes_sentbytes_acked:发送和确认统计;
  • wscale:窗口扩大因子;
  • delivery_rate:部分内核和 iproute2 版本可见的传输速率估计;
  • app_limited:发送速率是否可能受应用供给不足限制。

字段不是所有版本都提供,不能把某个字段缺失解释为“没有该机制”。ss -i 展示的是内核当前采样结果,不是完整历史,也不等价于抓包。

查看 TCP 计时器:

sudo ss -ton

-o 会显示计时器信息,输出形式可能类似:

timer:(on,201ms,0)

它表示 socket 上存在计时器,但具体格式和含义应结合 iproute2 版本解释。不能仅凭一次计时器快照断定“马上要重传”。


三、tcpdump:从报文时间线验证内核推断

ss 告诉我们内核如何看待连接,tcpdump 则观察接口上经过的报文。两者的观察点不同:

  • ss 看到的是内核 socket 和 TCP 控制块;
  • tcpdump 看到的是抓包点上的帧;
  • 中间可能存在网卡卸载、虚拟交换机、隧道、策略路由和容器网络等差异。

1. 基础抓包命令

抓取主机 443 端口的 TCP 流量:

sudo tcpdump -i any -nn -tttt -s 128 'tcp port 443'

参数含义:

  • -i any:抓取所有接口。Linux 支持,但链路层显示和方向信息与具体物理接口不同;
  • -nn:不解析主机名和服务名;
  • -tttt:显示可读时间;
  • -s 128:只抓取每个包前 128 字节,适合只看 TCP 头;
  • 过滤表达式:只抓 TCP 443。

若要保留完整数据供离线分析:

sudo tcpdump -i eth0 -nn -s 0 -w /tmp/tcp-443.pcap 'tcp port 443'

-s 0 会抓取完整包,可能包含敏感数据,且会增加磁盘和 I/O 压力。生产环境应限定接口、主机、端口和时间,并设置文件轮转,例如:

sudo tcpdump -i eth0 -nn -s 128 \
  -G 60 -W 5 -w '/tmp/tcp-%Y%m%d%H%M%S.pcap' \
  'host 10.0.0.20 and tcp port 443'

-G 按秒轮转,-W 限制文件数量;具体行为依赖 tcpdump 版本,执行前应验证生成文件和磁盘占用。

2. 观察三次握手

过滤 SYN:

sudo tcpdump -i eth0 -nn -tttt \
  'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'

更直接的过滤方式:

sudo tcpdump -i eth0 -nn -tttt \
  'tcp[tcpflags] & tcp-syn != 0'

正常握手应看到:

client > server: Flags [S], seq 1000, win ..., options [...]
server > client: Flags [S.], seq 5000, ack 1001, ...
client > server: Flags [.], ack 5001, ...

关键点:

  • SYN 消耗一个序列号,因此服务端确认客户端 SYN 时 ack = client_seq + 1
  • SYN-ACK 同样消耗一个序列号,因此客户端最终 ACK 为 server_seq + 1
  • 不能按字面把抓包中的 seq 1000 当成“第一个应用字节的序列号”,因为 SYN 和 FIN 也占用序列空间;
  • -S 可以显示绝对序列号,便于跨包追踪:
sudo tcpdump -i eth0 -nn -S 'host 10.0.0.20 and tcp port 443'

三次握手耗时可近似为:

ThandshakeRTT路径T_{\text{handshake}} \approx RTT_{\text{路径}}

客户端发送 SYN 到收到 SYN-ACK 通常需要一个往返路径中的主要部分;最终 ACK 通常不需要服务端再返回报文,所以应用能否立即发送数据还取决于 TCP 栈和应用行为。

3. 观察 FIN、RST 和连接异常终止

sudo tcpdump -i eth0 -nn \
  'tcp[tcpflags] & (tcp-fin|tcp-rst) != 0'
  • FIN:有序关闭,表示发送方不再发送数据;
  • RST:复位连接,通常表示异常终止、无监听端口、收到无法接受的报文,或应用使用了复位式关闭;
  • 看到客户端发出 RST,不等于服务端先出错;必须结合方向、时间和 socket 状态分析。

4. 抓包中的重复报文不一定都是 TCP 重传

抓包点可能位于发送端主机。网卡的 TSO/GSO 会让内核准备大段数据,网卡再分成多个线速报文;接收端的 GRO 可能将多个报文合并后交给内核。因此:

  • 主机抓包中可能看到大于链路 MTU 的“报文”;
  • 抓包工具显示的分段数量可能与线上实际数量不一致;
  • 校验和卸载会导致 tcpdump 报告“bad checksum”,但报文在线上可能是正确的;
  • 虚拟网卡、容器 veth 和物理网卡上的抓包结果可能不同。

如果怀疑卸载造成误判,可在实验环境或短时间窗口检查:

ethtool -k eth0

临时关闭部分卸载功能:

sudo ethtool -K eth0 tso off gso off gro off

这会改变性能和 CPU 消耗,生产环境不应无计划执行。诊断完成后恢复:

sudo ethtool -K eth0 tso on gso on gro on

不同驱动和虚拟设备支持的功能不完全相同,失败时应以 ethtool 的实际输出为准。


四、TCP 重传:为什么发生,如何从证据中确认

1. 重传的基本条件

发送方发送数据段后,只有在收到确认或其他可证明数据已到达的反馈后,才能安全地释放对应发送缓存。若数据段丢失,发送方需要重新发送。

常见触发机制有两类:

  1. 超时重传:发送段后,计时器达到 RTO 仍未得到足够确认;
  2. 快速重传:发送方收到多个重复 ACK,推断某个序列范围可能丢失,在超时前重传。

TCP 的确认是累计确认。假设发送端发送:

段 A:seq=1000,长度=1000
段 B:seq=2000,长度=1000
段 C:seq=3000,长度=1000

如果 B 丢失,接收端收到 A 后确认 ack=2000;收到 C 时仍无法连续交付 B,通常继续发送 ack=2000。重复 ACK 不表示 C 没到,而是表示接收端仍在等待从 2000 开始的数据。

启用 SACK 时,接收端可能在 ACK 中报告已经收到的非连续区间,例如“已收到 3000–4000”,发送端就能更精确地重传缺口。SACK 不能修复丢包本身,只能减少不必要的重传。

2. RTO 的直觉和形式化条件

一种经典的 RTO 估计使用平滑 RTT SRTT 和 RTT 方差 RTTVAR

RTO=SRTT+4×RTTVARRTO = SRTT + 4 \times RTTVAR

变量含义:

  • SRTT:历史 RTT 的平滑估计;
  • RTTVAR:RTT 波动程度;
  • 乘数 4:为突发排队和路径波动留出余量。

实际实现还会设置下限、上限,并在连续超时后进行指数退避。现代 Linux 的具体 TCP 实现包含更多算法和版本差异,不能把上式当成内核每个版本的完整源码逻辑,但它准确表达了一个关键因果:RTT 越大或抖动越大,发送方越不容易快速判定丢包;真正超时后,后续等待通常会快速变长。

假设某次连接的:

SRTT = 80 ms
RTTVAR = 10 ms

则初始估算为:

RTO=80+4×10=120 msRTO = 80 + 4 \times 10 = 120\text{ ms}

如果一次发送没有得到确认,首次超时可能在约 120 ms 量级发生;若仍未得到确认,退避可能接近:

RTO2240 ms,RTO3480 msRTO_2 \approx 240\text{ ms},\quad RTO_3 \approx 480\text{ ms}

具体值受内核边界、计时器粒度和实现影响。这个算例说明了为什么“偶发丢一个包”可能只增加几十毫秒,而连续丢包会造成几百毫秒甚至更长的延迟。

3. 快速重传不等于网络一定丢包

重复 ACK 可能由以下原因产生:

  • 中间某个数据段真实丢失;
  • 报文乱序,后续段先到达;
  • 接收端或路径发生队列重排;
  • 抓包点不完整,观察到的序列并非全貌。

因此,看到一个重传包只能说明发送方再次发送了相同序列范围,不能单独证明“物理链路丢包”。需要同时观察:

  • 是否出现重复 ACK;
  • 重传前后时间间隔;
  • 是否存在 SACK;
  • 发送端和接收端是否都能看到相同报文;
  • 网卡错误、接口丢包和中间设备计数器。

使用 tcpdump 保存数据后,可以用 Wireshark 的 TCP 分析标记辅助识别,但分析标记属于工具推断,不是 TCP 规范中的新报文类型。

4. 用 ss 验证重传状态

sudo ss -tin state established

重点比较:

bytes_sent
bytes_retrans
bytes_acked
rto
rtt
cwnd

例如:

bytes_sent:1000000 bytes_retrans:24000 bytes_acked:976000
rtt:35.2/8.1 rto:210

这说明内核统计中存在重传字节,但不能仅凭比例判断业务影响。若该连接长期 Send-Q 增长,同时 bytes_retrans 增长,较可能是对端确认不及时、路径丢包或接收窗口受限;若 Send-Q 为零但应用仍慢,问题可能位于请求处理、用户态队列或响应之外。


五、SYN 队列、accept 队列和监听积压

TCP 服务端收到客户端 SYN 后,并不会立刻把连接交给应用。至少有两个逻辑阶段:

  1. 半连接阶段:服务端发送 SYN-ACK,等待客户端最终 ACK,通常表现为 SYN-RECV
  2. 已完成连接阶段:三次握手完成,连接等待应用调用 accept(),之后才成为应用可用的连接。

可以把路径画成:

flowchart LR
    C[客户端 connect] -->|SYN| L[服务端 LISTEN]
    L --> H[半连接请求/SYN-RECV]
    H -->|最终 ACK| A[accept 队列]
    A -->|应用 accept()| E[应用持有的 ESTABLISHED socket]

这两个队列的故障表现不同。

1. SYN 队列:握手尚未完成

查看半连接:

ss -ant state syn-recv

若服务端看到大量 SYN-RECV

  • 客户端最终 ACK 可能没有到达;
  • 服务端发送的 SYN-ACK 可能在回程丢失;
  • 客户端可能根本没有真正发起连接,而是 SYN flood;
  • SYN backlog 可能不足;
  • 防火墙或负载均衡设备可能只允许单向流量。

SYN 队列中的对象通常不是完整的已建立 socket,而是等待握手完成的请求状态。Linux 可能启用 SYN cookies,在队列压力或相关条件下不保存完整请求状态,而把部分状态编码进 SYN-ACK 的序列号。SYN cookies 能缓解资源耗尽,但并不等于队列无限,也可能限制某些 TCP 选项;是否启用、何时触发和具体行为属于内核实现。

检查相关参数:

sysctl net.ipv4.tcp_syncookies
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.core.somaxconn

tcp_max_syn_backlog 与半连接请求相关;somaxconn 是监听 backlog 的内核上限。应用传入 listen(fd, backlog) 的值还会受到内核上限和内核版本行为影响,不能简单认为把应用 backlog 写成很大就能无限扩大队列。

2. accept 队列:握手完成但应用尚未接收

监听 socket 的 ss 输出:

LISTEN 120 4096 0.0.0.0:8080 0.0.0.0:*

这里的 Recv-Q=120 表示大约有 120 个已完成握手、等待应用 accept() 的连接。若这个数持续接近 Send-Q=4096,常见原因是:

  • 应用线程没有及时调用 accept()
  • 事件循环被耗时任务阻塞;
  • 应用进程 CPU 饱和或发生 stop-the-world 暂停;
  • accept() 返回 EMFILEENFILE,文件描述符不足;
  • 应用虽然调用 accept(),但随后没有及时处理连接;
  • 多进程或多线程竞争监听 socket,实际调度不均衡。

检查进程文件描述符限制:

cat /proc/$(pgrep -n myserver)/limits | grep -i 'open files'

检查系统级打开文件数:

sysctl fs.file-max
cat /proc/sys/fs/file-nr

这些指标只能定位资源方向,不能取代应用日志。应用必须记录 accept() 失败的错误码,特别是 EMFILEENFILEECONNABORTED 等。

3. 两种队列积压的反例

以下判断经常出错:

SYN-RECV 很多,所以应用 accept 太慢。

不一定。SYN-RECV 表示握手尚未完成,与应用是否调用 accept() 没有直接等价关系。若客户端到服务端的 ACK 被防火墙丢弃,即使应用完全空闲,SYN-RECV 也会积累。

相反:

监听 socket 的 Recv-Q 很大,所以网络丢包严重。

也不一定。握手可能全部正常,真正问题是应用线程没有及时 accept()。应同时比较抓包中的握手完成情况、监听 socket 的 Recv-Q,以及进程系统调用和资源状态。


六、TIME_WAIT:关闭后的安全状态,而不是“僵尸连接”

1. TIME_WAIT 为什么存在

主动关闭的一方通常进入 TIME_WAIT。它的作用主要有两个:

  1. 确保连接中迟到的旧报文不会误伤后续复用的相同四元组;
  2. 确保主动关闭方能够重传最后的 ACK,使对端有机会完成关闭。

传统 TCP 描述中常用 2MSL 表示等待时间。MSL 是 Maximum Segment Lifetime(最大报文生存时间),协议模型中的概念;Linux 的实际 TIME_WAIT 持续时间由内核实现和版本决定,不能仅根据某个 RFC 算出精确秒数。常见 Linux 环境中它通常约为一分钟量级,但应通过实际系统和内核行为验证。

查看数量:

ss -tanH state time-wait | wc -l

查看本机临时端口范围:

sysctl net.ipv4.ip_local_port_range

如果客户端频繁建立短连接,可能出现:

connect: Cannot assign requested address

这通常表示可用的本地地址/临时端口组合不足,或者已有连接占用了可复用的四元组。它不是“服务端端口被 TIME_WAIT 占满”的简单问题。

2. tcp_fin_timeout 不等于 TIME_WAIT 时长

sysctl net.ipv4.tcp_fin_timeout

这个参数主要与孤儿 socket 的 FIN-WAIT-2 处理有关,不能把它当作控制 TIME_WAIT 生命周期的通用开关。修改它来“减少 TIME_WAIT”通常是错误方向。

3. SO_REUSEADDR 的边界

SO_REUSEADDR 主要影响本地 bind 行为,例如服务进程重启时是否能在某些旧连接仍存在的情况下重新绑定监听地址。它不意味着可以无条件复用所有正在 TIME_WAIT 的四元组,也不等于 SO_REUSEPORT

SO_REUSEPORT 主要用于多个 socket 共享同一监听地址和端口,涉及负载分发和内核实现行为,不能作为清理 TIME_WAIT 的手段。

Linux 提供 net.ipv4.tcp_tw_reuse,但其含义、默认值和适用范围具有内核版本差异;过去的 tcp_tw_recycle 已从 Linux 中移除,不应照搬旧文章建议。生产环境不应为了压低 TIME_WAIT 数字盲目开启相关参数,因为错误复用可能引入旧报文污染、NAT 兼容性和连接语义问题。

更直接的解决方式通常是减少不必要的短连接,例如:

  • HTTP keep-alive;
  • HTTP/2 或其他多路复用;
  • 数据库连接池;
  • 合理的空闲连接回收;
  • 将主动关闭责任放在更适合的一侧。

但这不是绝对规则:协议和业务可能要求服务端主动关闭,不能为了减少 TIME_WAIT 改变连接语义。


七、延迟:RTT、握手延迟、应用延迟和排队延迟不是同一个量

1. TCP 延迟的分解

一次请求的总耗时可以粗略拆成:

Ttotal=TDNS+Tconnect+Tqueue+Trequest+Tserver+Tresponse+TretransT_{\text{total}} = T_{\text{DNS}} + T_{\text{connect}} + T_{\text{queue}} + T_{\text{request}} + T_{\text{server}} + T_{\text{response}} + T_{\text{retrans}}

其中:

  • T_DNS:名称解析时间;
  • T_connect:TCP 建连时间,HTTPS 还要加 TLS 握手;
  • T_queue:本机、网卡、路由器和服务端队列等待;
  • T_request:请求数据发送时间;
  • T_server:服务端应用处理时间;
  • T_response:响应传输和确认时间;
  • T_retrans:丢包和重传引入的额外时间。

ping 测到的是 ICMP 往返时间,不等于 TCP 应用请求耗时;ss 中的 rtt 是 TCP 对确认报文的估计,也不等于服务端业务处理时间。

2. 一个完整算例

假设:

DNS:8 ms
TCP 握手:24 ms
请求发送:2 ms
服务端处理:40 ms
响应传输:24 ms

则无重传时:

Ttotal8+24+2+40+24=98 msT_{\text{total}} \approx 8 + 24 + 2 + 40 + 24 = 98\text{ ms}

若响应中的一个关键数据段发生超时,当前 RTO 为 200 ms,那么额外延迟可能接近 200 ms:

Ttotal98+200=298 msT_{\text{total}} \approx 98 + 200 = 298\text{ ms}

如果连续超时,退避可能变为约 400 ms、800 ms,延迟会出现明显的长尾。

但如果数据段只是乱序并触发快速重传,额外等待可能远小于一次 RTO。于是“RTT 只有 24 ms”与“请求偶尔 300 ms”可以同时成立:平均路径延迟不高,但少量丢包触发了 RTO。

3. 小请求为何可能被 Nagle 和延迟 ACK 影响

Nagle 算法倾向于避免在已有未确认数据时频繁发送很小的 TCP 段;接收端的 delayed ACK 则可能等待短时间,把确认与后续数据合并。两者在“应用频繁写入小片段、同时等待对端响应”的交互式协议中可能互相放大等待。

这并不表示 Nagle 或 delayed ACK 本身是错误。它们减少小包数量,但应用协议若要求每次小写入都立即得到响应,就需要评估:

  • 是否能在用户态合并写入;
  • 是否应该使用 TCP_NODELAY
  • 是否因关闭 Nagle 而造成大量小包和 CPU 消耗;
  • 服务端是否批量发送响应。

TCP_NODELAY 解决的是发送端小段聚合策略,不会修复丢包、拥塞、服务端排队或 DNS 延迟。


八、从一次“请求很慢”开始的诊断流程

第一步:确认名称解析和路由

不要把 DNS 问题误认为 TCP 问题:

dig +stats example.com

确认实际连接地址和路由:

getent ahosts example.com
ip route get 203.0.113.20

ip route get 显示内核针对特定目的地址选择的路由、出口接口和源地址。多网卡、策略路由、容器和 VPN 场景下,必须确认抓包接口与实际路径一致。

第二步:确认监听和连接状态

ss -ltnp 'sport = :443'
ss -tan state syn-recv '( sport = :443 )'
ss -tan state established '( sport = :443 )'

如果监听不存在,问题尚未进入 TCP 传输阶段;如果监听存在但 SYN-RECV 持续增长,应检查握手回程、队列和攻击流量;如果 accept 队列 Recv-Q 高,应转向应用 accept() 和文件描述符。

第三步:从客户端和服务端同时抓包

客户端:

sudo tcpdump -i eth0 -nn -tttt -s 128 \
  'host 203.0.113.20 and tcp port 443'

服务端:

sudo tcpdump -i eth0 -nn -tttt -s 128 \
  'host 198.51.100.10 and tcp port 443'

比较同一个 SYN 的四元组和序列号:

  • 客户端看到 SYN 发出,服务端看不到:中间路径或抓包接口有问题;
  • 服务端看到 SYN 并发出 SYN-ACK,客户端看不到:回程路径或中间设备问题;
  • 客户端重发 SYN,说明没有收到可接受的 SYN-ACK;
  • 三次握手完成后才开始长时间等待:问题已不再是“端口是否开放”,应检查应用响应、窗口和数据重传。

第四步:观察窗口和队列

已建立连接:

sudo ss -tinp state established

重点看:

  • Recv-Q 是否持续增长:应用读取慢;
  • Send-Q 是否持续增长:对端确认、接收窗口或路径发送受阻;
  • bytes_retrans 是否增长:存在重传;
  • rttrto 是否异常升高;
  • cwnd 是否很小:可能受到拥塞控制或丢包影响;
  • 是否显示 rwnd_limitedsndbuf_limited 等版本相关信息。

Recv-Q 增长,而抓包显示数据已正常到达,网络可能没有问题,应用读取速度才是瓶颈。若 Send-Q 增长但抓包没有继续发出数据,可能是接收窗口关闭、拥塞窗口受限或本机发送缓冲区问题;若抓包持续重传,则应继续检查路径丢包和对端处理。

第五步:区分 RST、超时和应用处理慢

三类现象的时间线不同:

  • RST:通常能在抓包中直接看到复位报文,连接快速失败;
  • 握手超时:重复 SYN 或 SYN-ACK,间隔逐步拉长;
  • 应用处理慢:握手成功、数据也可能发送成功,但响应迟迟不出现,没有必要的 TCP 重传。

因此不能只看最终的客户端错误。例如客户端报告“connection reset by peer”,首先应找哪个方向发出了 RST;客户端报告“i/o timeout”,则要看是 DNS、SYN、数据确认还是应用读取阶段超时。


九、常见误判和反例

1. TIME_WAIT 多就是故障

反例:一个代理每天处理大量短连接,主动关闭方自然会积累大量 TIME_WAIT。只要临时端口、内存和 CPU 没有耗尽,数量本身可能完全正常。

应观察:

ss -s
ss -tanH state time-wait | wc -l
sysctl net.ipv4.ip_local_port_range

同时检查连接建立速率、端口耗尽错误和应用连接复用策略。

2. SYN-RECV 多就是 SYN flood

反例:跨地域链路发生回程丢包时,合法客户端也会让服务端留下大量 SYN-RECV。需要比较源地址分布、握手重传、接口丢包和防火墙统计,不能只看状态数量。

3. Recv-Q 大就是内核网络坏了

对已建立连接,Recv-Q 大通常表示应用没有及时读取;对监听 socket,Recv-Q 大通常表示应用没有及时 accept()。它们都可能是应用调度或资源问题,而不是 TCP 栈故障。

4. tcpdump 看到 checksum 错误就是链路损坏

若抓包发生在校验和计算前的发送路径,网卡卸载会使 tcpdump 看到尚未填充的校验和。应在实际出口、对端或关闭卸载后复核;不能凭单个主机抓包直接判定线上校验和错误。

5. ssrtt 就是用户感知延迟

rtt 只描述 TCP 确认路径的估计。服务端可能在确认请求后花费数秒执行数据库查询,此时 TCP RTT 正常,而应用延迟很高。必须结合应用日志、请求时间戳和抓包中的数据方向判断。


十、生产环境中的风险控制

1. 抓包的风险

完整抓包可能包含:

  • HTTP 明文请求和响应;
  • Cookie、令牌、用户数据;
  • TLS 握手元数据;
  • 大量磁盘文件和 I/O。

优先使用窄过滤器和较小 snaplen:

sudo tcpdump -i eth0 -nn -s 128 \
  'host 10.0.0.20 and tcp port 443'

若需要内容分析,再在受控窗口使用 -s 0,并限制轮转文件数量。抓包文件应按敏感数据处理。

2. 修改 sysctl 的风险

以下参数都可能影响全局或大量连接:

net.ipv4.tcp_syncookies
net.ipv4.tcp_max_syn_backlog
net.core.somaxconn
net.ipv4.ip_local_port_range
net.ipv4.tcp_tw_reuse

修改前应记录当前值:

sysctl net.ipv4.tcp_syncookies \
       net.ipv4.tcp_max_syn_backlog \
       net.core.somaxconn \
       net.ipv4.ip_local_port_range \
       net.ipv4.tcp_tw_reuse

临时修改只适合经过验证的实验或明确的应急方案:

sudo sysctl -w net.core.somaxconn=8192

这不会自动改变已经创建的监听 socket 队列,也不能替代应用重新调用 listen() 或重启。永久配置还涉及 /etc/sysctl.d/,应通过配置管理系统发布,并保留回滚值。

3. 不要用终止连接代替诊断

sudo ss -K dst 203.0.113.20 dport = :443

ss -K 可用于杀掉匹配连接,但会主动破坏业务连接,并非所有协议状态和内核版本都支持相同过滤字段。生产环境使用前必须确认命令实际匹配范围,优先在单连接、维护窗口和可回滚场景执行。


十一、把证据连成因果链

一个可靠的 TCP 结论应至少包含“现象、时间线、内核状态、报文证据”四部分。例如:

现象:客户端 P99 从 50 ms 升到 300 ms
内核:bytes_retrans 持续增长,rto 约 200 ms
抓包:请求数据段发送后约 200 ms 再次出现,期间没有有效 ACK
队列:服务端监听 Recv-Q 正常,已建立连接 Recv-Q 不增长
结论:更符合数据段丢失触发 RTO,而不是 accept 队列积压或服务端应用读取慢

另一个不同的证据链可能是:

现象:新连接偶发超时
服务端:SYN-RECV 不多,监听 Recv-Q 接近 Send-Q
抓包:三次握手已完成
进程:accept() 返回 EMFILE
结论:连接已完成握手,但应用因文件描述符耗尽无法及时接收

这两个故障的客户端表现都可能是“连接超时”,但处理方向完全不同:前者应查链路丢包和重传,后者应查进程资源限制与连接泄漏。

TCP 诊断的核心不是记住某个命令,而是建立一条可验证的时间链:

DNS/路由
  -> SYN 是否发出
  -> SYN-ACK 是否返回
  -> 最终 ACK 是否到达
  -> 连接是否进入 accept 队列
  -> 应用是否 accept/read/write
  -> 数据是否被确认
  -> 是否发生快速重传或 RTO
  -> 关闭后由哪一方进入 TIME_WAIT

ss 的状态、tcpdump 的报文、队列计数和应用日志能在同一条时间线上相互印证时,TCP 问题才从“网络很慢”变成了可定位的具体故障。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。