资讯动态

TCP三次握手ACK丢失:从故障现场到内核调优实战

发布时间:2026/10/20 23:28:26 来源:尧图企业网站定制
1. 从一个真实故障场景说起聊这个话题之前先讲一个我亲身经历过的线上故障。某次大促期间监控系统突然报警说一台后端服务的并发连接数暴涨但实际业务请求量并没有明显增加。登录机器一看netstat里满屏的SYN_RECV状态连接而且这些连接来自同一个客户端集群。第一反应是怀疑有人在搞事情但抓包分析之后发现事情比想象中有意思得多。抓包结果显示客户端明明已经发出了第三次握手的 ACK服务端却像没收到一样反复重传SYNACK。而客户端这边呢认为自己已经完成了连接建立开始往这条“已建立”的连接上发数据了。结果就是服务端一直在等第三次握手客户端却在发数据两边状态完全对不上。这个现象持续了大概十几秒直到客户端因为收不到服务端的确认而超时重传最终连接才恢复正常。这个故障的根因就是TCP三次握手中第三次握手的ACK丢失。听起来像是一个很基础的问题但真正在复杂网络环境下遇到时它的表现和教科书上写的完全不是一回事。今天我就把这个话题彻底拆开从协议原理、内核行为、实际抓包、排查手段到优化建议一次性讲透。这篇文章适合所有做后端开发、运维、SRE、网络工程的同学阅读。不管你是刚入行的新手还是已经工作多年的老手相信都能从中找到一些之前没注意到的细节。我会尽量用大白话把内核里的行为讲清楚同时配上实际抓包和参数调优的实操内容让你看完就能用到实际工作中。2. 三次握手到底在交换什么信息2.1 握手过程的本质双向确认很多人背过“SYN、SYNACK、ACK”这个流程但未必真正理解每一步在干什么。我用一个生活化的类比来解释两个人打电话A拨号说“喂能听到吗”B回答“能听到你能听到我吗”A再说“能听到”。这三句话的核心目的是确认双方的发送能力和接收能力都正常。TCP的握手也是同样的逻辑。第一次握手客户端发SYN告诉服务端“我想跟你建立连接我的初始序列号是X”。第二次握手服务端回SYNACK意思是“我收到了你的请求我同意建立连接我的初始序列号是Y同时我确认收到了你的序列号X”。第三次握手客户端发ACK告诉服务端“我收到了你的序列号Y确认号是Y1”。关键在于前两次握手已经让客户端确认了服务端的收发能力但服务端还不知道客户端是否收到了自己的SYNACK。第三次握手的核心作用就是让服务端确认“客户端确实收到了我的SYNACK并且客户端的接收和发送能力都正常”。2.2 第三次握手的ACK里有什么从协议格式上看第三次握手的报文很简单TCP头部里SYN标志位为0ACK标志位为1确认号字段填的是服务端初始序列号加1序列号字段填的是客户端初始序列号加1。这个报文通常不携带任何应用层数据就是一个纯粹的控制报文。但这里有一个容易被忽略的细节第三次握手的ACK是可以携带数据的。在Linux内核中如果客户端在调用connect()之后立即调用send()内核有可能把数据和第三次握手的ACK合并到一个报文里发出去。这个行为叫做“数据捎带”在协议上是完全合法的。不过大多数情况下应用层还没来得及发数据ACK就已经单独发出去了。2.3 服务端在等什么服务端在发出SYNACK之后连接进入SYN_RECV状态。此时内核会把这个连接放入一个叫做半连接队列也叫SYN队列的地方。服务端在等什么呢它在等一个满足以下条件的ACK报文确认号等于自己发出的SYNACK中的序列号加1并且ACK标志位为1。一旦收到这个ACK连接就会从SYN_RECV状态迁移到ESTABLISHED状态同时从半连接队列移到全连接队列也叫Accept队列等待应用层调用accept()把连接取走。如果一直收不到这个ACK服务端就会重传SYNACK重传次数由内核参数控制超过次数后连接被丢弃。3. ACK丢失后两端到底发生了什么3.1 客户端的视角我已经连上了这是整个问题里最反直觉的地方。客户端在发出第三次握手的ACK之后就认为连接已经建立了。在客户端的视角里它已经完成了自己的所有动作发了SYN收到了SYNACK发了ACK。按照状态机连接应该进入ESTABLISHED状态。所以客户端会立刻开始往这条连接上写数据。如果应用层有数据要发内核会把数据封装成报文发出去。此时客户端完全不知道服务端有没有收到那个ACK。如果ACK丢了客户端发出的数据报文到达服务端时服务端会怎么处理呢服务端此时还在SYN_RECV状态它期待的是一个纯ACK报文。收到一个带数据的报文序列号是客户端初始序列号加1确认号是服务端初始序列号加1。从协议角度看这个报文其实完全满足第三次握手的条件——它有ACK标志确认号也正确。所以服务端会把这个数据报文当作第三次握手同时接收其中的数据。连接进入ESTABLISHED状态数据被放入接收缓冲区。这就是为什么很多情况下ACK丢失并不会导致连接建立失败——因为客户端后续的数据报文“顺便”完成了第三次握手。但如果客户端没有数据要发呢那服务端就只能干等直到超时重传。3.2 服务端的视角我还在等确认服务端发出SYNACK后启动了一个重传定时器。这个定时器的初始值通常是1秒由TCP_TIMEOUT_INIT控制实际值可能因内核版本和配置而异。如果1秒内没收到ACK服务端会重传SYNACK同时把定时器翻倍变成2秒、4秒、8秒……直到达到最大重传次数。在Linux中tcp_synack_retries参数控制SYNACK的重传次数默认值是5。这意味着服务端会尝试重传5次总等待时间大约是12481631秒实际会略有偏差因为定时器有抖动。如果5次重传都没有收到ACK服务端会发送RST报文并丢弃这个连接。这里有一个非常重要的细节服务端在重传SYNACK时如果收到了客户端的数据报文会立即完成握手并接收数据。所以实际生产中ACK丢失导致连接建立失败的情况并不多见更多时候表现为连接建立延迟增加。3.3 中间网络的视角报文去哪了ACK丢失的原因可能有很多种。最常见的是网络拥塞导致报文在队列中被丢弃。还有一种情况是中间设备如防火墙、负载均衡器的状态表满了新连接的报文无法通过。另外如果客户端和服务端之间的路径发生了变化比如路由切换ACK可能被发到了一个不再转发该连接的中间设备上。我在实际排查中遇到过一种比较隐蔽的情况客户端的ACK发出去了但被中间的一个安全设备拦截了。这个设备在做深度包检测时认为这个ACK报文“不完整”因为它没有携带数据直接丢弃了。这种问题非常难排查因为从两端的抓包来看客户端确实发了ACK服务端确实没收到但中间发生了什么完全看不到。4. 内核参数与超时机制详解4.1 半连接队列与全连接队列理解ACK丢失的影响必须搞清楚Linux内核中两个队列的行为。半连接队列syn_queue存放的是收到SYN但还没完成三次握手的连接也就是处于SYN_RECV状态的连接。全连接队列accept_queue存放的是已经完成三次握手、等待应用层accept()的连接。半连接队列的大小由tcp_max_syn_backlog参数控制默认值通常是1024或2048取决于内存大小。全连接队列的大小由listen()系统调用的backlog参数和somaxconn参数共同决定取两者中的较小值。当ACK丢失时连接会一直停留在半连接队列中占用队列空间。如果大量连接的ACK都丢失了半连接队列可能被占满导致新的SYN请求无法被处理。这就是我在文章开头提到的那个故障场景SYN_RECV连接暴涨就是因为大量连接的第三次握手ACK丢失连接卡在半连接队列里出不去。4.2 SYNACK重传机制Linux内核中SYNACK的重传由tcp_synack_retries参数控制。这个参数的默认值是5意味着重传5次。每次重传的间隔是指数退避的第一次1秒第二次2秒第三次4秒第四次8秒第五次16秒。总共大约31秒。你可以通过以下命令查看和修改这个参数# 查看当前值 cat /proc/sys/net/ipv4/tcp_synack_retries # 临时修改为3次 echo 3 /proc/sys/net/ipv4/tcp_synack_retries # 永久修改写入配置文件 echo net.ipv4.tcp_synack_retries 3 /etc/sysctl.conf sysctl -p把重传次数调小可以加快连接释放但也会增加在网络抖动时连接建立失败的概率。我一般建议在稳定的内网环境中可以调到3在公网环境中保持默认的5或者调到4。4.3 客户端的数据重传如果客户端在发出ACK后立即发送了数据但服务端因为没收到ACK而处于SYN_RECV状态客户端的数据报文会触发服务端的重传吗答案是不会。服务端收到数据报文后会正常处理并进入ESTABLISHED状态然后给客户端回ACK。但如果这个数据报文也丢了呢客户端的数据报文有自己独立的超时重传机制。如果数据报文丢失客户端会重传数据。重传的数据报文同样可以触发服务端完成握手。所以实际上只要客户端有数据要发ACK丢失几乎不会导致连接建立失败最多就是延迟增加。真正的问题场景是客户端发出ACK后没有数据要发同时服务端的SYNACK重传也丢了。这种情况下双方就会陷入互相等待的死锁直到服务端重传次数耗尽发送RST客户端收到RST后才知道连接建立失败。5. 抓包实战如何确认ACK是否丢失5.1 抓包点的选择要确认ACK是否丢失抓包点的选择非常关键。最理想的情况是在客户端和服务端同时抓包然后对比两边的报文。如果客户端抓到了ACK发出但服务端没抓到那说明ACK在中间网络丢了。如果两边都抓到了ACK但服务端仍然重传SYNACK那可能是服务端内核的问题。在实际生产环境中往往只能在一边抓包。这时候就需要根据报文的行为来推断。比如在服务端抓包看到反复重传SYNACK但收不到ACK同时客户端已经在发数据了那基本可以确定是ACK丢失或者被中间设备拦截了。5.2 用tcpdump过滤关键报文以下命令可以在服务端抓取与某个客户端的所有握手相关报文tcpdump -i eth0 -nn -tttt host 192.168.1.100 and (tcp[tcpflags] (tcp-syn|tcp-ack) ! 0) -w handshake.pcap这个过滤条件的含义是抓取与192.168.1.100相关的、TCP标志位中包含SYN或ACK的报文。抓完之后用Wireshark打开可以看到完整的握手过程。在Wireshark中你可以用tcp.flags.syn1 and tcp.flags.ack1过滤出所有的SYNACK报文观察是否有重复的SYNACK发往同一个客户端端口。如果有说明服务端在重传间接说明ACK没有到达服务端。5.3 关键指标重传次数与时间间隔在分析抓包结果时重点关注两个指标重传次数和重传时间间隔。正常情况下SYNACK的重传间隔应该是指数退避的。如果你看到重传间隔是固定的那可能不是内核在重传而是中间设备在伪造响应。另外如果重传次数远小于tcp_synack_retries设置的值连接就被重置了那可能是中间设备主动发送了RST。这种情况在云环境中比较常见因为云平台的负载均衡器或安全组可能会对半开连接进行清理。6. 常见问题与排查技巧实录6.1 为什么客户端显示连接已建立服务端却显示未建立这是最典型的症状。客户端netstat看到连接是ESTABLISHED服务端看到的是SYN_RECV。原因就是客户端发出ACK后立即进入ESTABLISHED状态而服务端还在等ACK。如果此时客户端没有数据要发这个状态差异会一直持续到服务端超时。排查方法在两端同时执行ss -tnp或netstat -tnp对比同一组IP和端口的连接状态。如果状态不一致基本可以确定是第三次握手的ACK出了问题。6.2 大量SYN_RECV连接堆积怎么处理当半连接队列被大量SYN_RECV连接占满时新的连接请求会被丢弃。这时候可以采取以下应急措施临时调大tcp_max_syn_backlog给队列更多空间开启tcp_syncookies在队列满时用SYN Cookie机制处理新连接调小tcp_synack_retries让卡住的连接更快释放# 应急调优 echo 4096 /proc/sys/net/ipv4/tcp_max_syn_backlog echo 1 /proc/sys/net/ipv4/tcp_syncookies echo 3 /proc/sys/net/ipv4/tcp_synack_retries但这些都是治标不治本。根本的解决方法是找到ACK丢失的原因可能是网络设备问题也可能是客户端的行为异常。6.3 常见问题速查表症状可能原因排查手段解决方向客户端ESTABLISHED服务端SYN_RECV第三次握手ACK丢失两端同时抓包对比检查中间网络设备服务端反复重传SYNACK未收到ACKtcpdump观察重传间隔确认客户端是否发出ACK半连接队列满新连接被丢弃大量ACK丢失ss -s查看队列统计调大队列开启syncookies连接建立延迟高但最终成功ACK丢失后靠数据报文补上抓包看是否有数据捎带优化网络质量客户端收到RST服务端重传超时抓包看RST的序列号检查网络稳定性6.4 一个容易被忽略的坑NAT设备的老化时间在客户端和服务端之间存在NAT设备时NAT设备会为每个连接维护一个映射表。如果第三次握手的ACK丢失NAT设备可能认为这个连接没有完全建立从而不创建映射表项。当客户端后续发送数据时NAT设备因为没有映射表项可能会丢弃数据报文或者发送RST。这个问题的隐蔽之处在于从客户端和服务端的抓包来看双方的行为都是正常的但数据就是不通。排查时需要在NAT设备上查看会话表确认是否有对应的表项。解决方法是调整NAT设备的老化时间或者确保第三次握手的ACK能够可靠传输。7. 优化建议与个人经验7.1 内核参数调优建议根据我多年的运维经验以下参数组合在大多数场景下表现比较均衡# 半连接队列大小根据并发量调整 net.ipv4.tcp_max_syn_backlog 4096 # SYNACK重传次数内网可以调小公网保持默认 net.ipv4.tcp_synack_retries 3 # 开启SYN Cookie防止半连接队列被打满 net.ipv4.tcp_syncookies 1 # 全连接队列大小 net.core.somaxconn 32768 # SYNACK重传的超时基数适当调小可以加快失败检测 net.ipv4.tcp_syn_retries 3需要注意的是tcp_syncookies虽然能缓解半连接队列满的问题但它会导致一些TCP扩展选项如窗口缩放、SACK无法使用可能影响传输性能。所以它更适合作为应急手段而不是长期开启的配置。7.2 应用层的心跳与超时设计从应用层的角度不要完全依赖TCP的握手超时。建议在应用层设计合理的心跳机制和连接超时。比如客户端在connect()成功后如果一段时间内没有收到服务端的响应应该主动断开重连。服务端在accept()之后如果一段时间内没有收到客户端的数据也应该主动关闭连接。这样即使TCP层面的ACK丢失导致连接状态不一致应用层也能通过自己的超时机制快速发现并恢复。7.3 监控与告警配置建议对以下指标配置监控和告警半连接队列使用率ss -s中的SYN-RECV数量SYNACK重传率通过netstat -s | grep -i syn查看连接建立失败率应用层统计connect()失败次数连接建立延迟统计从connect()到accept()的时间分布这些指标可以帮助你在问题影响扩大之前及时发现异常。我在实际工作中发现很多ACK丢失的问题在早期只表现为连接建立延迟轻微增加如果不监控这些指标很难察觉到。7.4 我踩过的一个坑最后分享一个我踩过的坑。有一次在容器环境中服务的SYN_RECV连接异常增多我按照经验调大了tcp_max_syn_backlog但问题依然存在。后来发现容器的net.ipv4.tcp_max_syn_backlog是命名空间级别的参数在宿主机上修改并不影响容器内部。需要在容器的启动参数中单独设置或者通过sysctl在容器内修改。这个坑让我意识到在容器化环境中排查网络问题时一定要确认参数的作用范围。很多内核参数在容器中有独立的命名空间宿主机上的配置不一定对容器生效。排查时可以先在容器内执行cat /proc/sys/net/ipv4/tcp_max_syn_backlog确认实际生效的值。另外一个经验是不要盲目相信“默认值就是最优值”。Linux内核的很多网络参数默认值是基于通用场景设计的在高并发、低延迟的特定场景下适当调整可以带来明显的性能提升。但调整之前一定要理解参数的含义和影响范围最好在测试环境中验证后再上生产。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑