一、TCP 连接的本质:从文件描述符到内核控制块

TCP 连接在 Linux 内核中并非一个"连接"对象,而是一个由struct sockstruct tcp_sockstruct inet_connection_sock三大结构体组成的控制块体系。理解这些结构的内存布局,是排查连接异常的基础。

每一个 TCP socket 在内核中都对应以下核心数据结构:

struct socket (用户态接口层)
  └── struct sock (网络层通用结构)
       └── struct inet_connection_sock (INET 连接层)
            └── struct tcp_sock (TCP 协议专用结构)
                 - snd_nxt    // 下一个发送序号
                 - rcv_nxt    // 下一个期望接收序号
                 - snd_wnd    // 发送窗口大小
                 - rcv_wnd    // 接收窗口大小
                 - snd_cwnd   // 拥塞窗口大小
                 - ssthresh   // 慢启动阈值
                 - rto        // 重传超时时间
                 - state      // 连接状态(enum tcp_states)

当应用调用 socket(AF_INET, SOCK_STREAM, 0) 时,内核会预分配一个struct inet_bind_bucket作为端口绑定的哈希表条目。理解这个细节,有助于诊断 EADDRINUSE(端口已被占用)错误。

服务端通过 listen(fd, backlog) 建立监听后,内核会维护两个队列:

  • 半连接队列(SYN Queue):存储处于 SYN_RECV 状态的连接请求
  • 全连接队列(Accept Queue):存储已完成三次握手、等待 accept() 取走的连接

backlog 参数实际上控制的是全连接队列的上限(struct request_sock_queue->rskq_accept_head)。如果队列满了,内核会丢弃新到达的 ACK,导致连接超时重传。这是高并发场景下 Connection refusedConnection timed out 的常见根因。

诊断命令:
# 监控半连接队列溢出
netstat -s | grep "SYNs to LISTEN"

# 监控全连接队列溢出
netstat -s | grep "times the listen queue of a socket overflowed"

# 使用 ss 查看当前 backlog 占用情况
ss -tnl | head -20

二、三次握手:不只是三个包的交换

三次握手是 TCP 可靠连接的基石。很多人只记"SYN → SYN+ACK → ACK",但每一步都涉及内核状态的精细变化。

2.1 客户端发起连接

客户端调用 connect() 后,内核执行以下操作:

  1. 从临时端口范围(ip_local_port_range,默认 32768-60999)选择一个可用端口作为源端口
  2. 生成初始序列号(ISN, Initial Sequence Number),现代 Linux 使用基于时钟和加密哈希的算法防止序列号预测攻击
  3. 分配 struct tcp_sock 控制块,状态设为 TCP_SYN_SENT
  4. 发送 SYN 包,启动重传定时器(RTO 默认 1 秒)

如果 SYN 包在 RTO 内未收到响应,内核会按指数退避重传:1s、2s、4s、8s...最多重试tcp_syn_retries次(默认 6,共 63 秒)。

2.2 服务端响应

服务端收到 SYN 后:

  1. 在半连接队列(SYN Queue)中创建 struct request_sock,将连接设为 TCP_SYN_RECV
  2. 生成服务端的 ISN
  3. 发送 SYN+ACK 包

关键点:在三次握手完成之前,服务端仅分配一个轻量级的 request_sock(约 128 字节),而非完整的 tcp_sock(约 1408 字节)。这是经典的 SYN Flood 攻击 利用的弱点——攻击者只发 SYN 而不完成握手,耗尽服务端的半连接队列内存。

2.3 最终 ACK 与连接建立

客户端收到 SYN+ACK 后:

  1. 确认服务端的 ISN,状态从 TCP_SYN_SENT 变为 TCP_ESTABLISHED
  2. 发送 ACK(此 ACK 可以携带应用数据,称为 "TCP Fast Open" 的扩展机制)

服务端收到 ACK 后:

  1. request_sock 从半连接队列移出,构建完整的 tcp_sock
  2. 将连接放入全连接队列,状态设为 TCP_ESTABLISHED
  3. 等待 accept() 取走连接

2.4 TCP Fast Open(TFO)

Linux 3.7+ 支持 TFO(tcp_fastopen),允许在第一个 SYN 包中携带数据,减少一个 RTT 延迟。服务端在首次连接时返回一个 TFO Cookie,客户端缓存后可在后续连接的 SYN 中附带数据。

# 启用 TFO(0=关闭, 1=客户端, 2=服务端, 3=都启用)
sysctl -w net.ipv4.tcp_fastopen=3

三、四次挥手:连接终止的完整状态机

TCP 连接是全双工的,每个方向的关闭需要独立的 FIN 交换。四次挥手看似简单,但每个状态转换都涉及内核定时器和内存释放策略。

以客户端主动关闭为例,状态变化为:TCP_ESTABLISHED →(发送 FIN)→ TCP_FIN_WAIT_1 →(收到 ACK)→ TCP_FIN_WAIT_2 →(收到服务端 FIN)→ TCP_TIME_WAIT →(等待 2MSL)→ TCP_CLOSE。

服务端收到 FIN 后回复 ACK,状态变为 TCP_CLOSE_WAIT。如果此时应用没有调用 close() 关闭连接,就会出现 CLOSE_WAIT 堆积,这是最常见的连接泄漏问题。

TIME_WAIT 存在的两大理由

理由一:可靠地终止连接。如果最后一个 ACK 丢失,被动关闭方会重传 FIN。主动关闭方必须在 TIME_WAIT 中保持足够时间以处理重传的 FIN 并重发 ACK。

理由二:防止旧连接的延迟报文干扰新连接。TCP 报文在网络中可能延迟到达(最大 MSL,通常 60 秒)。TIME_WAIT 期间,端口不会被立即复用,防止上一个连接的数据串入新连接。

四、流量控制:滑动窗口与零窗口探测定时器

TCP 使用滑动窗口实现接收端流量控制,防止发送端过载接收端。

接收方在每个 ACK 中包含 Window Size 字段(16 位,最大 65535 字节),告知发送方自己还能接收多少数据。

窗口缩放(Window Scale):16 位窗口字段无法支撑高带宽延迟积(BDP)场景。TCP 在三次握手的选项中协商 Scale Factor(0-14),实际窗口 = 通告窗口 × 2^Scale。Linux 默认启用 tcp_window_scaling=1

零窗口测定时器(ZWP):当接收方通告窗口为 0 时,发送方停止发送数据,启动零窗口探测定时器(默认 1 秒),定期发送 1 字节的探测包检测接收方窗口是否恢复。

糊涂窗口综合征(SWS):接收端使用 Clark 方案(不更新窗口直到可接收 MSS 或缓冲区一半空闲),发送端使用 Nagle 算法(不允许发送小于 MSS 的数据,除非所有已确认数据都已 ACK)。

Nagle 算法在交互式场景(如 SSH、Redis)中可能导致微小延迟,可通过 TCP_NODELAY 选项关闭。

五、拥塞控制:从 Reno 到 BBR 的演进之路

TCP 拥塞控制算法的目标:在不压垮网络的前提下最大化吞吐量。

5.1 经典 Reno 算法

Reno 定义了四个核心机制:慢启动(Slow Start)、拥塞避免(Congestion Avoidance)、快速重传(Fast Retransmit)、快速恢复(Fast Recovery)。

慢启动:cwnd 从 10 MSS 开始,每收到一个 ACK 增加 1 MSS(每 RTT 翻倍),直到达到 ssthresh。拥塞避免:cwnd 达到 ssthresh 后线性增长,每 RTT 增加 1 MSS。

Reno 的缺陷:在高带宽延迟积(BDP)网络中效率低下,无法区分拥塞丢包和传输错误丢包。

5.2 CUBIC:Linux 默认算法

CUBIC 使用三次函数窗口增长模型:W(t) = C * (t - K)^3 + W_max。其中 K = cbrt(W_max × β / C)。β 为乘法减少因子(默认 0.7),C 为缩放常数(默认 0.4)。

CUBIC 的优势:窗口增长与 RTT 解耦,不同 RTT 的流具有相似的窗口增长速率,对高 BDP 网络更加友好。

5.3 BBR:基于模型的新范式

BBR(Bottleneck Bandwidth and RTT)由 Google 开发,完全不依赖丢包作为拥塞信号。它通过连续测量估计瓶颈带宽(BtlBw)和往返传播时间(RTprop),发送速率 = BtlBw × RTprop = BDP。

BBR 的工作循环(Startup → Drain → Probe BW → Probe RTT),每 10 秒为一个周期。

# 查看可用拥塞控制算法
sysctl net.ipv4.tcp_available_congestion_control

# 设置 BBR 为默认
sysctl -w net.ipv4.tcp_congestion_control=bbr

# 对特定连接设置
# Python
import socket
TCP_CONGESTION = 13
sock.setsockopt(socket.IPPROTO_TCP, TCP_CONGESTION, b"bbr")

5.4 算法选型指南

数据中心内部网络推荐 CUBIC + ECN;广域网/跨洋链路推荐 BBR;长肥网络(卫星、跨国专线)推荐 BBR v2;无线/移动网络(可变丢包)推荐 BBR;需要公平共享带宽场景推荐 CUBIC。

六、TIME_WAIT 与端口耗尽:生产调优全景

TIME_WAIT 是生产环境中最常见的连接问题之一。关键参数包括:

  • tcp_fin_timeout(默认 60 秒 = 2 × MSL)控制 TIME_WAIT 前的 FIN_WAIT_2 超时
  • tcp_tw_reuse = 1允许 TIME_WAIT 状态的连接被安全重用(仅适用于连接发起方)
  • tcp_max_tw_buckets限制 TIME_WAIT 套接字的最大数量

CLOSE_WAIT 堆积排查:长时间停留在 CLOSE_WAIT 状态说明应用没有正确调用 close() 关闭连接。常见原因包括 HTTP 客户端未关闭 response body、数据库连接池未正确释放。

端口耗尽排查ss -tan state time-wait | wc -l 监控 TIME_WAIT 连接数;cat /proc/sys/net/ipv4/ip_local_port_range 查看可用端口范围。

解决方案包括扩大临时端口范围、多 IP 地址绑定、使用连接池、SO_REUSEPORT 等。

七、内核 TCP 参数:sysctl 调优模板

cat <<'EOF' >> /etc/sysctl.d/99-tcp-tuning.conf

# 连接队列
net.ipv4.tcp_max_syn_backlog = 65535
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535

# 缓冲区
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728

# TIME_WAIT
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_tw_buckets = 262144

# Keepalive
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 5

# 重传
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_synack_retries = 2

# 拥塞控制
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_notsent_lowat = 16384

# SYN Cookie 防护
net.ipv4.tcp_syncookies = 1
EOF

sysctl -p /etc/sysctl.d/99-tcp-tuning.conf

八、延迟确认与 Nagle 算法的交织问题

TCP 延迟确认(Delayed ACK,默认延迟最多 200ms)和 Nagle 算法可能在同一连接上同时生效,导致 40ms 的交互延迟。

解决方案:在发送端禁用 Nagle(TCP_NODELAY);在接收端禁用延迟确认(TCP_QUICKACK);或使用 writev() 合并小数据包。

九、TCP 重传与 RTO 计算

RTO 通过 RFC 6298 标准自适应计算,使用 Jacobson 算法平滑 RTT 测量:

RTTVAR = (1 - β) × RTTVAR + β × |RTTs - R|     β = 0.25
RTTs = (1 - α) × RTTs + α × R                    α = 0.125
RTO = RTTs + max(G, K × RTTVAR)

Karn 算法规定不基于重传报文更新 RTT 采样。Linux 4.0+ 引入 RACK-TLP 替代传统快速重传。

十、epoll 与 TCP 的非阻塞 I/O

高性能网络服务器使用 epoll(O(log n) 增删 + O(1) 获取就绪事件)替代 select/poll。epoll 支持 Level-Triggered(默认)和 Edge-Triggered(ET,性能更高)两种模式。

惊群效应可通过 EPOLLEXCLUSIVE(Linux 4.5+)和 SO_REUSEPORT 缓解。

十一、eBPF 与 XDP:TCP 网络加速

传统工具 ss/netstat 通过读取 /proc/net/tcp 存在延迟。eBPF 内核探针可实时跟踪 TCP 事件:

# 跟踪 TCP 连接建立
bpftrace -e 'kprobe:tcp_connect { printf("connect from %s to %s\n", ntop(...)); }'

# 使用 BCC 工具
sslat              # socket 延迟
tcpconnect         # TCP 连接跟踪
tcplife            # TCP 连接生命周期
tcpdrop            # TCP 丢包事件

Linux 5.13+ 允许 eBPF 程序介入 TCP 拥塞控制,实现自定义算法。

十二、生产故障排查全景

故障1:TIME_WAIT 大量堆积
启用 tcp_tw_reuse=1;减小 tcp_fin_timeout=15;扩大端口范围;长连接替代短连接。

故障2:CLOSE_WAIT 堆积
应用收到 FIN 后未调用 close(),连接泄漏。检查 HTTP 响应体是否正确关闭,数据库连接池是否释放。

故障3:SYN Flood 攻击
启用 tcp_syncookies=1;增大 tcp_max_syn_backlog;iptables 限速 SYN。

故障4:连接超时
tcpdump 抓包检查 SYN 是否被丢弃;mtr 检查路径 RTT;ss 检查 backlog。

故障5:TCP 重传率高
ss -ti 查看拥塞控制状态;netstat -s 查看全局重传;切换 BBR;启用 ECN;检查网卡驱动。

十三、总结

  • 连接复用:用连接池替代短连接
  • 缓冲区适配:根据 BDP 调整收发缓冲区
  • 拥塞算法选型:广域网 BBR,数据中心 CUBIC
  • Keepalive 探活:及时检测僵尸连接
  • epoll ET 模式:高并发服务端优先使用
  • eBPF 观测:实时掌握协议栈行为

TCP 协议栈经过 40 多年演进,仍是互联网最可靠的传输层协议。理解其内部机制是解决生产网络问题的必备技能。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部