ARTICLE DETAIL

资讯详情

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

TCP可靠性机制深度解析:三次握手、重传机制与拥塞控制

TCP可靠性机制深度解析:三次握手、重传机制与拥塞控制 1. TCP到底在解决什么问题1.1 不要把TCP想象成一条管子很多人学TCP时有个非常顽固的误解觉得TCP就是一根管子数据从一头灌进去另一头按顺序流出来。这个认知会阻碍你理解TCP几乎所有的重要机制包括三次握手、滑动窗口、重传、拥塞控制。我建议你先换一个模型TCP不是管子而是一个**寄信回执系统**。发送方把数据切成一段一段的信件也就是报文段扔进网络接收方收到后必须回一封信说我收到了确认ACK。网络本身是不可靠的——信件可能丢、可能乱序到达、可能重复到达、可能在路上被卡很久。TCP要做的就是在这个完全不可靠的IP网络上靠着发信回执超时重发这套规则给上层应用制造出一种可靠有序字节流的假象。为什么必须这样因为IP层只负责尽力送达best effort它不承诺任何可靠性。数据包在路由器之间穿梭时可能因为队列满了被丢弃可能因为走了不同路径导致后发的先到可能因为网络拥塞被延迟很久。UDP的做法是我发出去就不管了而TCP把所有脏活累活都揽在自己身上这才是理解它所有复杂机制的总纲领。1.2 可靠传输的三个难点TCP要解决的核心难题可以拆成三个不丢报文段在网络上丢失了发送方怎么知道答案是超时。发送方发出数据后启动定时器如果一段时间内没收到ACK就重发。但问题来了如果报文没丢只是ACK在路上堵车了重发就会造成重复数据。于是需要给每个字节编号序列号接收方根据序列号去重。不重同一份数据到达接收方多次接收方必须能识别并丢弃重复部分。这靠序列号和确认号配合完成。不乱序IP包可能走不同的路由器路径后发出的先到达很正常。接收方收到乱序数据后不能立刻交给应用层要先在缓冲区里排序凑齐了再按顺序向上交付。这三点说起来简单实际操作里全是细节。比如不丢这个目标如果重传太激进会导致网络中塞满重复报文反而加剧拥塞如果太保守延迟会大到应用无法接受。于是又衍生出RTO计算、快速重传、拥塞控制等一堆机制。1.3 一次真实故障乱序重传怎么拖垮下载速度我讲一个线上案例这个案例能让你直观感受到上面三个难点有多要命。某天业务方反馈从内网服务器下载一个文件速度从平时的80MB/s掉到2MB/s但ping延迟正常链路也没有丢包。在服务器上抓包后发现TCP重传率高达15%而且大量报文是乱序到达的。当时用的网卡开启了接收侧缩放RSS多队列收包但某个驱动版本在特定流量模式下会出现哈希不均导致同一连接的报文被分发到不同CPU核心处理而核心之间同步不当造成报文到达顺序错乱。接收方的TCP协议栈发现序列号跳变以为是丢包了就拼命发dup ACK请求重传发送方收到3个dup ACK触发快速重传浪费了大量带宽在处理重复数据上。这个故障说明什么TCP的可靠性机制本质上是在用确认和重传对抗网络的不完美但如果网络本身或主机协议栈出现乱序TCP的容错机制会产生大量的额外开销。工程上排查TCP性能问题最先看的就是重传率、乱序率这些指标不是ping延迟。2. 三次握手为什么必须是三次2.1 握手的本质是确认四件事教科书上画的三次握手图大多数学生是背下来的——SYN、SYNACK、ACK。但我建议你换一个角度问自己握手到底在确认什么答案是对通信双方而言需要确认四件事客户端确认自己的发送能力正常服务器的接收能力正常客户端确认服务器的发送能力正常自己的接收能力正常服务器确认自己的发送能力正常客户端的接收能力正常服务器确认客户端的发送能力正常自己的接收能力正常第一次握手客户端发SYN服务器收到。此时服务器确认客户端的发送能力没问题、我的接收能力没问题但客户端还不知道自己的发送能力是否正常也不知道服务器的收发能力。第二次握手服务器回SYNACK。客户端收到后确认了我的发送没问题、服务器接收没问题、服务器发送没问题、我的接收没问题——对客户端来说四件事全齐了。但此时服务器还不知道自己的发送能力是否正常、客户端接收是否正常。第三次握手客户端回ACK。服务器收到后才确认我的发送没问题、客户端的接收没问题。至此双方对四个能力项达成共识连接建立。2.2 为什么不能是两次如果有人问你为什么不能两次握手不要只背会建立历史连接导致资源浪费你要能讲清楚场景。假设只需要两次握手客户端发SYN服务器回SYNACK就算建立。问题在于网络中有一个迟到的旧SYN报文客户端上一次连接留下的到达服务器服务器以为是新连接立刻建立连接并分配资源。客户端收到SYNACK后发现这个连接根本不是自己想要的序列号对不上直接丢弃——但服务器已经为这个幽灵连接浪费了资源而且会一直等着客户端发数据直到超时。有了第三次握手服务器收到ACK时才知道哦客户端确实在线、这个连接是合法的。如果服务器等的ACK一直不来它就明白这个连接有问题可以释放资源。三次握手本质上解决的是服务器如何确认客户端确实收到了我的SYNACK这个问题。2.3 握手队列溢出一个真实运营事故很多人以为三次握手只是打个招呼但在高并发场景下握手阶段就是一场攻防战。Linux内核为每个监听的socket维护两个队列半连接队列SYN Queue和全连接队列Accept Queue。我处理过一个线上事故某个服务在流量高峰时突然大面积超时大量客户端报Connection timed out但CPU、内存、带宽都还有余量。最后用ss -lnt一看Recv-Q列显示全连接队列溢出很多连接的握手已经完成但应用层还没accept新来的连接直接被内核丢弃。原因很简单应用是单线程accept处理请求太慢全连接队列满内核开始丢SYN。这个问题的排查思路和解决方式扩容应用线程数、调大backlog参数先不谈重点是想说明三次握手不是纯理论它涉及内核队列、应用accept速度、超时重传等多层配合。tcpdump抓包能看到一个经典现象服务器不回SYNACK客户端重复发SYN重传间隔是1秒、2秒、4秒……指数退避。这就是半连接队列满了或者防火墙丢包的典型表现。2.4 抓包看握手序列号的意义用tcpdump -i eth0 host 1.2.3.4 and port 8080抓一次完整握手你会看到1 客户端 → 服务器 SYN seq1000 2 服务器 → 客户端 SYNACK seq5000 ack1001 3 客户端 → 服务器 ACK seq1001 ack5001注意seq都是相对值Linux内核有个net.ipv4.tcp_timestamps和随机化机制实际序列号是随机的。序列号初始值为什么要随机如果可预测攻击者可以伪造RST包切断连接或者伪造SYN造成连接欺骗。这也是为什么很多安全基线要求启用tcp_timestamps——它不仅提供RTT采样还让序列号更难预测。每次握手时SYN会消耗一个序列号所以第二次握手的ack是客户端seq1第三次握手的ack是服务器seq1。这里消耗一个序列号很多人会忽略但理解了这个你后面理解累计确认时就不会卡壳。3. 四次挥手与TIME_WAIT关闭连接的代价3.1 为什么挥手要四次三次握手只需要三次挥手却要四次这个不对称让很多人困惑。根本原因在于建立连接时双方同步状态都是closed没有数据凭空飞来而关闭连接时双方可能都还有数据要发送需要各自独立地宣布发送完毕。TCP是全双工的A和B可以同时发数据也可以各自独立关闭自己的发送方向。四次挥手的过程A发FIN表示A不再发送数据B回ACK表示收到你的FINB继续把自己没发完的数据发完然后发FIN表示B也不发了A回ACK表示收到你的FIN第2步和第3步不能合并因为B在收到A的FIN后大概率还有自己的数据要发比如处理完请求再关闭。只有在B恰好没有任何数据要发、而且能立刻确认关闭的情况下2和3才可能合并成一次——这就是三次挥手的由来。顺便说一句最后一步如果丢了B会超时重发FINA必须能再次响应所以A的TIME_WAIT状态需要持续一段时间。3.2 TIME_WAIT为什么是2MSL主动关闭连接的一方通常是客户端也可能是服务端在发出最后一个ACK后会进入TIME_WAIT状态时长2MSLMSL是Maximum Segment Lifetime报文在网络上存活的最长时间Linux里默认是60秒所以TIME_WAIT要等120秒。这两个MSL的含义第一个MSL是等着处理可能重发的FIN。你想如果A最后一个ACK丢了B会重新发FINA必须还在场才能回应。第二个MSL是让网络中所有旧报文消失——如果TIME_WAIT太短前一个连接的旧报文还在网络里漂而被同一个四元组源IP、源端口、目的IP、目的端口新建的连接收到了就会把数据搞混。这里有个常见的工程矛盾高并发短连接场景下大量TIME_WAIT会让服务器端口被占满五元组或让客户端端口耗尽四元组。我之前调过一台压测机默认端口范围是32768到60999约2.8万个可用端口客户端瞬间发起几万个短连接后报Cannot assign requested address。解法有几个但不建议一上来就调短TIME_WAIT那是掩耳盗铃——旧报文混淆的风险真实存在。一个相对安全的思路是开启net.ipv4.tcp_tw_reuse它只对客户端角色生效允许在端口紧张时复用处于TIME_WAIT的连接前提是打开的tcp_timestamps能判断这个连接足够老。对服务端更建议调整架构减少短连接比如用连接池、HTTP持久连接。我这里说得比较细是因为很多人百度到tw_reuse1就乱设其实内核版本、角色、双方时间戳选项都对它有约束。3.3 连接假死与保活机制如果一方断电、崩溃、拔网线另一方在TCP层是感知不到的。TCP没有类似于心跳的义务机制但你可以开启SO_KEEPALIVE选项。默认内核参数是7200秒2小时后开始探测每隔75秒探测一次连探9次没回应就判定连接死亡。2小时对大多数业务来说太久了。我在做网关时会把keepalive时间调到10秒级别让NAT会话在网关上超时之前就能被清理掉。这里有个容易踩的坑很多云厂商的负载均衡器、NAT网关的会话超时时间是60秒左右你如果不开SO_KEEPALIVE长连接业务一会儿就被中间设备静默杀死了——从业务侧看就是连接断了但谁也没报错。4. 确认、重传与dup ACKTCP的容错机制4.1 累计确认一把分段尺子的设计TCP确认报文的格式是下一个期望收到的字节序号不是我收到了哪些段。也就是说ack1001表示序列号1000及之前的所有字节我都收到了你从1001开始发吧。这种累计确认有个巨大好处接收方不需要对每个报文单独回复发送方也只需要记住一个基准点。但它也有个副作用——如果中间的某个段丢了即使后面的段都到了接收方也只能确认到丢包处。比如发了seq1000、2000、3000三个段2000丢了哪怕3000到了接收方也只能回ack1001因为你缺2000。发送方看到ack还是1001就知道中间丢了。这里顺便解释一个热搜词tcp dup ack机制——接收方每收到一个失序报文期望之外的seq都会重复确认当前期望的序列号这个重复的ACK就叫dup ACK。比如上例中收到的第一个失序段触发ack1001收到第二个失序段再次触发ack1001于是出现了重复确认。4.2 超时重传与RTO计算发送方发一个段启动一个计时器如果超时RTO还没收到ACK就重发。问题是网络延迟是动态变化的RTO设置成固定值会出问题——设短了稍有抖动就重传浪费带宽设长了真丢包时恢复太慢。所以TCP用自适应算法根据实测的RTT往返时间动态调整RTO。经典算法是RTO SRTT 4×RTTVAR前者是平滑后的RTT后者是RTT的抖动程度。网络越波动RTO就自动拉大避免误判。每次超时重传后RTO会指数退避——第一次超时等1秒再超时等2秒、4秒……这是为了在拥塞时别火上浇油。我曾经在弱网环境下测过一个上传功能丢包率5%时吞吐直接掉到原来的十分之一。原因就是超时重传的等待时间占了大量时间而快速重传又没有触发因为丢的是尾部报文不会产生dup ACK。这种情况下调整TCP参数没用唯一靠谱的做法是应用层做前向纠错或者干脆接受低带宽。4.3 快速重传3个dup ACK触发重传如果丢包后傻等超时效率太低尤其在延迟高如跨太平洋几百毫秒的链路上。所以TCP有一种快速重传机制发送方收到3个连续的dup ACK即连续4个相同的ACK就不再等待超时立即重传丢失的段。为什么是3次不是1次因为网络乱序也会导致dup ACK——接收方收到失序段就会回确认。如果只收到一个dup ACK就重传网络稍微乱序就会触发大量无用重传反而加重拥塞。3次是经典实现里的一个折中它假设3次重复确认意味着不是乱序而是真的丢了。我在排查一个存储同步问题时看过抓包目标机器显示大量suspected dup ack说明接收方认为收到了重复确认。最终定位到是网卡驱动在中断合并时重复提交了同一报文。抓包软件看到的dup ACK不一定都是网络丢包也可能是主机协议栈自己的问题——这也是我在1.3节提到的故障的另一个变种。4.4 用ss和tcpdump判断重传问题工程上判断TCP是否在重传别靠感觉。两个命令最常用ss -ti可以显示socket的retrans计数比netstat好用太多。输出里有retrans:3/10这种表示当前有3次重传未确认累计10次重传。tcpdump抓包后可以用-d、-S看序列号和ACK也可以把抓包文件导入Wireshark它的Expert Info会直接标出Duplicate ACK、Retransmission、Out-of-Order、Spurious Retransmission等事件。我一直认为做网络排查的人应该先学会看Expert Info这一栏——它能帮你秒级判断链路质量而不是盯着十六进制报文一头雾水。5. 流量控制与拥塞控制TCP不是越快越好5.1 滑动窗口接收方的额度控制流量控制的目标发送方的发送速度不能超过接收方的处理能力。实现方式是接收方在ACK里带上自己的剩余缓冲区大小也就是窗口值。发送方只能发送已确认字节数 窗口值范围内还没发出去的数据。想象你是快递发件员对方每次告诉你我仓库还能放1000件你就只在允许的范围内发货等下次说仓库又腾出800件你再接着发。这个额度就是滑动窗口。一个常见的误用是盲目调大socket缓冲区。有人觉得接收窗口越大性能越好就sysctl -w net.core.rmem_max16777216往大了调。但接收窗口大不代表不丢包如果应用读取速度跟不上数据都在内核缓冲里堆积TCP会自动缩小通告窗口实际上并不会提高吞吐。真正的瓶颈常常在应用消费速度上。另外要知道TCP窗口字段只有16bit最大就是65535字节要支持更大的窗口必须开启窗口缩放选项Window Scaling。我见过一台老机器没启用窗口缩放文件传输莫名其妙慢其实是因为接收窗口被限制在64KB——要知道在百G链路上64KB窗口的单位RTT吞吐上限就是64KB/RTT乘以8算下来一望便知有多低。5.2 零窗口与糊涂窗口综合征接收方缓冲区满了会通告窗口0发送方收到后进入零窗口探测状态不断发1字节的探测报文。这个机制本身是必要的但有几个老问题。糊涂窗口综合征是这么一种状态接收方每次只腾出几十字节就通告一个极小的窗口发送方一看能发一点就发一点于是一个报文段只携带几十字节的有效数据网络被小包填满吞吐暴跌。解决办法有两个方向一是接收方推迟通告小窗口至少等到窗口显著增大再通告二是发送方用Nagle算法合并小数据。5.3 Nagle算法与TCP_NODELAY的取舍Nagle算法的规则是发送方在还有未确认数据时不能发送小的数据段必须攒到一个满段或所有数据都ACK了再发。它解决的是小包太多的问题在交互式场景SSH、Telnet里如果不开启每个按键都会触发一个小包效率极低。但Nagle算法和延迟ACK机制会形成经典的死锁式延迟Nagle等ACK来合并数据延迟ACK等收到两个满段或200ms超时才回ACK。于是常常出现几百毫秒的假延迟。遇到这种问题关闭Nagle即设置TCP_NODELAY通常能解决但如果关闭后你的应用又疯狂发小包就要靠应用层自己合并数据了。我在写网关转发逻辑时深有体会如果只是逐包转发TCP_NODELAY是必须开的如果能把多个逻辑消息合并成一个大报文反而应该开着Nagle收获更大的吞吐收益。没有哪个更好只有是否符合你的消息模式。5.4 拥塞控制网络不是你家开的流量控制管的是发送速度别超出接收方能力但网络中间设备的处理能力也有限。路由器缓冲一旦塞满就开始丢包丢包又会触发重传导致更多包进入网络进一步加剧拥塞——如果不控制网络就会崩溃。拥塞控制的目的是让发送方试探性地探测网络可用带宽。经典的四阶段慢启动初始拥塞窗口cwnd很小通常是10个MSS约14KB左右每收到一个ACK翻倍实际是每RTT翻倍。增长速度是1、2、4、8……指数上升直到达到ssthresh或发生丢包。拥塞避免cwnd达到ssthresh后改为线性增长每RTT只增加1个MSS。指数变线性是为了不把网络打爆。快速重传3个dup ACK触发重传这是阶段切换的触发器之一。快速恢复发生快速重传后cwnd减半进入拥塞避免的线性增长阶段而不是回到慢启动。因为收到dup ACK说明数据还在网络中流动有痕迹部分链路仍可用不必归零重启。这里有个反直觉的点TCP的拥塞控制初衷是控制自身的发送速率但它在带宽很大的链路上会让发送方长时间试探性增长导致吞吐率爬坡很慢。对于短连接、大文件传输的场景慢启动阶段可能占了整个传输的大部分时间。我在这类场景里会把初始拥塞窗口调大ip route可以通过initcwnd设置但只能在受控内网这么干公网上乱调容易挨骂——因为你等于让流量抢占别人的公平份额。6. 工程里那些看似简单、实则折腾的TCP问题6.1 一次Nginx连接数激增的复盘有一回线上Nginx报警ESTABLISHED连接数从几千冲到几万负载升高但不至于挂。当时第一反应是是不是有刷流量攻击用ss -s看连接状态分布发现大量SYN_RECV也就是半连接堆积。进一步查发现是后端某个服务hang了不accept新连接Nginx作为反向代理一直在往后端发连接请求后端的内核Accept队列满了直接丢弃SYN。Nginx这边表现为后端连接超时用户请求大量502。那次复盘给我的教训排查TCP问题先分清楚是连接建立不起来连接半路上断了还是连接建立了但没数据流动三个方向用的工具和思路完全不同。建立不起来多半看SYN/ACK、队列溢出半路断大概率看超时、保活、中间设备NAT表项建立了没数据则是应用层问题居多别在TCP参数上瞎找。6.2 常用TCP参数速查长期调TCP参数后我总结了一张个人速查表贴出来供参考。切记先理解参数含义再动手改改了要压测验证。场景参数建议值说明高并发短连接服务端tcp_max_syn_backlog4096以上增大半连接队列容量高并发短连接服务端somaxconn4096以上配合应用listen的backlog客户端端口耗尽tcp_tw_reuse1仅客户端角色需tcp_timestamps开启延迟敏感小包TCP_NODELAY应用层设置关闭Nagle算法长连接保活tcp_keepalive_time60010分钟按NAT超时设计大BDP链路吞吐低rmem/wmem调大且检查窗口缩放需同步检查tsq等机制丢包敏感业务无应用层改造前向纠错或冗余发送TCP参数救不了物理丢包这张表里每一项背后都有一段踩坑史比如tcp_max_syn_backlog调了但net.core.somaxconn没调导致全连接队列依旧溢出——这两个队列是分开的得一起看。6.3 判断瓶颈的经验法则最后分享几个经验法则适合快速定位TCP性能问题第一看重传率。ss -ti里retrans比例超过千分之一先怀疑链路丢包或乱序超过百分之一基本可以断定网络出问题了。第二看RTT和RTO。RTT暴涨伴随重传说明网络拥塞或丢包RTT正常但吞吐上不去重点检查窗口大小和发送缓冲区。第三看连接状态分布。大量SYN_RECV看半连接队列和防火墙大量TIME_WAIT看连接复用和架构大量FIN_WAIT_2看应用是否忘记关闭连接。第四不要只盯着TCP。很多TCP慢其实是慢在应用——数据包发到网卡了应用线程在等锁、在等数据库查询表现为连接保持、数据不流动。抓包确认数据到底卡在哪一端比盲目调参数高效得多。写到这里也该收个尾了。说实话TCP这个协议我学了十多年、排查了无数现场问题每次以为彻底懂了总会被某个边缘场景教育一顿。但正是这种基础协议却有无限深度的感觉让我建议每一个做网络、做后端、做运维的同行都把TCP当成一门需要长期咀嚼的功课来学而不是考完试就扔。真到了线上故障复盘的那天你会发现对TCP理解得越深你能走直线的路就越多。
返回列表