ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TCP 握手第三次 ACK 丢失怎么办?服务端重试机制与连接半打开状态排查

TCP 握手第三次 ACK 丢失怎么办?服务端重试机制与连接半打开状态排查 TCP 握手第三次 ACK 丢失怎么办服务端重试机制与连接半打开状态排查在所有计算机网络面试和底层排错中“TCP 三次握手”是出镜率最高的八股问题。近乎所有的技术博客都会贴出一张规整的时序图客户端发 SYN服务端回 SYNACK客户端再回 ACK双双步入ESTABLISHED状态开启数据传输。然而在公网传输、移动弱网、机房跨地域专线抖动的真实生产环境里网络丢包是常态。一个极具杀伤力的硬核追问是如果在握手的最后一步客户端发出的第三次 ACK 报文在网络路由器中被静默丢弃了双端的状态机会发生什么裂变服务端会如何自救客户端何时才会意识到连接出了问题如果只靠背课本你可能会以为双方会永远卡死在半路上。但实际上Linux 内核协议栈内部设计了精妙的容错与状态补偿机制。握手末尾丢包双端状态机的剧烈撕裂我们先还原第三次 ACK 丢失瞬间的微观时序状态sequenceDiagram autonumber actor Client as 客户端 (Client) actor Server as 服务端 (Server) Client-Server: 1. SYN (seqx) Note over Client: SYN_SENT Server-Client: 2. SYNACK (seqy, ackx1) Note over Server: SYN_RCVD (进入半连接队列) Client-Server: 3. ACK (seqx1, acky1) [网络中途丢失!] Note over Client: ESTABLISHED (客户端认为建连成功!) Note over Server: 仍然处于 SYN_RCVD !在这个致命的时间切片上双端的状态认知发生了严重的结构性撕裂客户端视角它已经成功收到了服务端的 SYNACK并按照协议规范回复了纯 ACK 确认。根据 RFC 793 状态机定义客户端在发出这个 ACK 的瞬间本地的 socket 状态就已经不可逆地跃迁到了ESTABLISHED服务端视角由于第三次 ACK 在线路上丢失服务端压根不知道客户端有没有收到自己的 SYNACK。因此服务端的该 socket 依然被死死卡在SYN_RCVD半连接状态驻留在内核的**半连接队列SYN Queue**中迟迟无法进入全连接队列Accept Queue供应用层accept()唤醒。接下来的系统行为将严格取决于客户端在发送完第三次 ACK 之后是否会立刻发送应用层业务数据。分流推演两种完全不同的命运走向场景一客户端发完 ACK 后立刻发送应用层业务数据绝大多数 HTTP 请求这是 Web 应用最普遍的场景。例如浏览器或移动端 App 在建立连接后立刻紧接着发送一个HTTP GET /index.html报文。此时会发生非常神奇的协议自愈根据 TCP 报文规范除握手第一个纯 SYN 包外后续所有正常传输的 TCP 数据段其 TCP Header 上的ACK标志位都必须被强制置为 1。客户端发出的第一个 HTTP 数据包其头部不仅携带了请求载荷而且其ack_seq字段精确地包含了服务端期望确认的y 1当服务端内核网卡收到这个携带数据的报文时虽然之前那个纯 ACK 丢了但协议栈在解析报文头时发现ACK标志置位且确认序号正是自己等待的y 1。服务端内核协议栈会立刻“原谅”之前丢失的纯 ACK顺水推舟将 socket 状态从SYN_RCVD推进到ESTABLISHED将连接移入全连接队列同时将这个数据包写入接收缓冲区结论在客户端“抢先说话”的模式下第三次纯 ACK 的丢失对上层业务几乎是完全透明且无感的TCP 凭借其报文头部常态化携带 ACK 的特性完成了被动自愈。场景二客户端发完 ACK 后保持沉默长连接等待服务端先发或心跳空闲如果业务协议设计为“服务端先打招呼”如早期的 FTP、某些非标准私有 RPC或者客户端建连后只是作为空闲连接池备用、暂时没有任何数据发送那么局势将变得极其恶劣。1. 服务端的重传自救指数退避由于服务端迟迟等不到合法的第三次确认服务端的重传定时器Retransmission Timer超时触发对SYNACK 的主动重传。在 Linux 系统中服务端的重传次数由内核参数net.ipv4.tcp_synack_retries决定默认通常为 5$ sysctl net.ipv4.tcp_synack_retries net.ipv4.tcp_synack_retries 5重传间隔遵循经典的二进制指数退避Binary Exponential Backoff第 1 次重传在发出首个 SYNACK 约 1 秒后第 2 次重传再等待 2 秒后累计 3 秒第 3 次重传再等待 4 秒后累计 7 秒第 4 次重传再等待 8 秒后累计 15 秒第 5 次重传再等待 16 秒后累计 31 秒最终等待 32 秒累计总共耗时约 63 秒。在这一分钟的重传窗口期内只要客户端收到了任何一次重传的 SYNACK由于客户端早已处于ESTABLISHED状态它会感到“疑惑”为什么服务端还在向我发 SYNACK客户端会本能地再次重发一个纯 ACK给服务端。只要这次重发的 ACK 成功送达服务端握手成功闭环连接被挽救。2. 服务端彻底放弃TCB 销毁与 RST如果网络持续恶化耗尽了 5 次重传后服务端依然没有收到任何回应服务端内核彻底判定该客户端不可达服务端将该半连接从半连接队列中剔除彻底释放其传输控制块TCB / Transmission Control Block内存资源部分 Linux 内核版本在释放资源前甚至会向客户端的 IP:Port 发送一个RST复位报文尝试告知对方切断幻想。此时的客户端处于极度危险的**“半打开Half-Open状态”**客户端仍然天真地认为连接是健康的如果此时客户端终于准备发送数据数据包打到服务端服务端协议栈一查根本没有这个连接的上下文会直接粗暴地向客户端回递一个RST报文客户端在读取或写入数据时瞬间捕获到 Java 的java.net.SocketException: Connection reset by peer异常或 C 语言的ECONNRESET错误。生产环境全连接与半连接排查实战在遇到大批连接异常重置或建连延迟抖动时如何通过 Linux 工具链实锤是否卡在第三次握手或半连接溢出1. 监控全连接与半连接队列指标使用现代 Linux 标配的ss命令检查监听端口# 查看 8080 端口的监听队列状态 $ ss -lnt ( sport :8080 ) State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 0.0.0.0:8080 0.0.0.0:*在LISTEN状态下Send-Q表示该端口全连接队列Accept Queue的最大容量由系统的somaxconn和应用层listen(backlog)的较小值决定。Recv-Q表示当前全连接队列中已经堆积了多少个完成了三次握手、但应用层如 Tomcat/Netty尚未调用accept()取走的连接数。如果Recv-Q长期接近或等于Send-Q说明全连接队列被打满此时新来的第三次 ACK 会被直接无情丢弃2. 统计丢包计数器通过netstat -s观察内核 TCP 统计计数$ netstat -s | grep -E listen|SYNs to LISTEN 8423 times the listen queue of a socket overflowed 19205 SYNs to LISTEN sockets droppedlisten queue of a socket overflowed全连接队列溢出次数。每一次溢出都意味着一个原本合法的第三次 ACK 被服务端内核主动丢掉。SYNs to LISTEN sockets dropped半连接队列或全连接队列满导致的主动丢包。3. tcpdump 抓包抓现行在服务端执行针对性抓包抓取所有的纯 ACK 与重传报文tcpdump -i eth0 -nn tcp[tcpflags] (tcp-syn|tcp-ack) ! 0 and port 8080如果抓包结果中频繁看到服务端向同一个客户端 IP 重复发送[SYN, ACK]而客户端只有零星的纯[ACK]或完全静默便可 100% 断定链路正在遭遇握手末端丢包。核心内核参数调优指南要化解握手末尾丢包与半连接雪崩在网关和高并发服务器上需要针对性调优以下内核参数内核参数 (/etc/sysctl.conf)推荐值调优核心逻辑net.ipv4.tcp_synack_retries2或3(默认5)在不可靠的公网环境中适当调小重试次数促使服务端更快释放悬挂的半连接资源防止 SYN 队列耗尽。net.ipv4.tcp_max_syn_backlog4096~16384扩大半连接队列容量增强抵抗突发流量和握手挂起的能力。net.core.somaxconn4096~32768提高系统级全连接队列上限防止应用层 GC 停顿引发的 Accept 堆积丢包。net.ipv4.tcp_abort_on_overflow0(默认0)保持为 0。当全连接满时只丢弃 ACK寄希望于客户端超时重发若设为 1 则直接回 RST 杀死连接对业务冲击过大。总结TCP 三次握手绝不是一个简单的三次来回问答它是分布式系统面对“两军问题”和不可靠网络通信时所能做出的最具工程智慧的妥协ACK 载荷捎带是最好的容错机制数据报文自带 ACK 序号的特性让绝大多数弱网丢包在真实数据交互面前不攻自破指数退避是最后的防线服务端有限次数的重传和最终主动释放阻止了恶意或悬挂连接无限消耗操作系统有限的物理内存半打开是分布式一致性的代价理解半打开状态的存在才促使我们在应用层坚定不移地构建基于业务心跳Keepalive与超时重试的防御闭环。
返回列表