ARTICLE DETAIL

资讯详情

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

TCP可靠传输机制深度解析:滑动窗口、重传与拥塞控制

TCP可靠传输机制深度解析:滑动窗口、重传与拥塞控制 TCP 协议这东西你在 Linux 下敲命令、写网络程序、排查线上故障几乎天天都要碰。但说实话很多搞了几年开发的兄弟对 TCP 的理解也就停留在“三次握手、四次挥手”这个层面。真要问到“为什么丢包了还能保证不重不漏”“滑动窗口到底怎么滑动”“拥塞窗口和接收窗口打架了听谁的”能讲清楚的人就不多了。这次我花了两天时间把 TCP 可靠传输的核心机制从头到尾捋了一遍结合 Linux 内核的实际行为写一篇尽量贴近实战的深度解析争取让你看完之后再遇到网络问题脑海里能有一个清晰完整的图景。这篇文章适合这些人看写网络服务的后端开发、做运维和 SRE 的兄弟、正在准备 Linux 和网络方向面试的同学以及那些用 TCP 调过接口但一直没搞明白底层原理的嵌入式工程师。我会尽量用生活化的类比把复杂机制讲明白再补上 Linux 环境下的实际验证手段和调优参数大家可以根据自己的基础选择跳读。1. TCP 可靠传输的整体设计思路1.1 不可靠的 IP 网络与 TCP 的目标要理解 TCP 为什么要设计得这么复杂得先认清它脚下踩的那片泥地——IP 网络。IP 协议提供的是“尽力而为”的传输服务它在转发数据包时不保证包一定到达不保证包按顺序到达甚至不保证包的内容不被篡改或者重复。你可以把 IP 网络想象成一个没有管理员、没有监控系统的快递中转站包裹扔进去之后能不能到、什么时候到、到了几件全都看运气。TCP 就是在这样一片“泥地”上靠一堆机制硬生生搭出一条“高速公路”。它的核心目标非常明确向应用层提供一个可靠的、面向连接的、基于字节流的传输服务。这里的关键词有三个可靠数据不丢、不重、不乱序。面向连接通信双方在传输数据前需要建立连接结束后需要释放连接。字节流TCP 不关心应用层的数据边界它把数据看作一串没有边界的字节按序交付给接收方。从架构上看TCP 位于传输层下面靠 IP 协议跨网络寻址上面为应用层提供 socket 接口。Linux 内核的 TCP/IP 协议栈实现得非常精巧它把 TCP 的复杂性全部封装在内核里应用层程序员只需要调用socket()、connect()、send()、recv()这几个 API完全不用关心序列号怎么管理、窗口怎么滑动、包怎么重传。1.2 可靠传输的四大支柱我在各种技术文章里反复看到过一句话TCP 的可靠传输靠的是“确认、重传、序号、窗口”这四招。这句话虽然简洁但确实是核心。我自己在实践中的理解是这四招分别解决不同的问题序列号Sequence Number解决“数据的顺序怎么保证”的问题。它给每个字节编号让接收方知道哪个字节先到、哪个字节后到还能顺带发现“中间是不是缺了一段”。确认应答ACK解决“对方收到没有”的问题。接收方收到数据后要告诉发送方“我收到哪了”发送方才能知道数据是否安全到达。重传机制Retransmission解决“丢了怎么办”的问题。发送方发现超时没收到确认或者收到重复 ACK就重新发送数据。滑动窗口Sliding Window解决“发太快收不下发太慢又浪费带宽”的问题。窗口大小由接收方的处理能力和网络拥塞程度共同决定让发送方动态调整发送速率。这四个机制并不是独立运作的它们之间咬合得非常紧密。比如序列号和 ACK 是重传的基础——发送方得知道“对方缺哪段数据”才能重传滑动窗口依赖 ACK 来推进窗口边界——没有 ACK窗口就永远不会滑动。理解了这个咬合关系你再看 TCP 协议栈的代码或者抓包结果就会觉得每个字段都顺理成章了。2. 连接管理三次握手与四次挥手的底层逻辑2.1 三次握手为什么必须是三次关于 TCP 三次握手网上有一堆“为什么不是两次”的解释但很多都停留在表面。我自己在排查问题后的体会是三次握手最核心的目的是让通信双方就“起始序列号”达成一致。回忆一下握手过程客户端发送 SYN携带初始序列号client_isn。服务端回复 SYNACK携带自己的初始序列号server_isn同时确认客户端的序列号。客户端发送 ACK确认服务端的序列号。为什么需要交换序列号因为 TCP 的可靠传输完全依赖序列号来排序和去重。如果通信双方不能确认对方的初始序列号后面接收到的每个字节都无法判定顺序可靠传输就无从谈起。为什么必须是“三次”我们可以做个假设——如果只握两次手客户端发出了 SYN服务端回复了 SYNACK此时服务端就认为连接已建立。但如果客户端发出的 SYN 因为网络问题延迟了很久连接建立后客户端已经放弃并重新发起了一个新连接那么这个迟到的 SYN 会在网络中形成一个“僵尸连接”请求。服务端收到它并回复确认后会白白分配资源等待一个永远不会来的客户端。三次握手的设计中客户端需要再发一次 ACK本质上是让服务端确认“客户端确实还活着并且确实收到了我的序列号”。如果这个 ACK 丢了服务端虽然进入了 ESTABLISHED 状态但客户端会因迟迟收不到 SYNACK 而放弃连接最终由服务端的超时机制清理掉这个半死不活的连接。所以三次握手是“建立可靠双向通信”的最小次数少一次都不行。在 Linux 下你可以用tcpdump观察握手的完整过程。我的习惯是先在终端跑一条抓包命令tcpdump -i eth0 tcp port 8080 -S -nn再另开一个终端发起连接请求curl http://127.0.0.1:8080/抓包里会出现三个包标志位分别是[S]、[S.]、[.]序列号和确认号的交替非常清晰。-S参数的关键性容易被忽略——它让 tcpdump 显示原始序列号否则你会看到经过相对转换后的序列号难以理解三次握手的推进逻辑。2.2 四次挥手与握手的不对称性断开连接采用四次挥手也是我在面试里经常被问到的一个点。TCP 是全双工协议意味着数据可以同时在两个方向传输。断开连接时每个方向都必须独立关闭所以需要四次交互主动关闭方发送 FIN。被动关闭方回复 ACK。被动关闭方也发送 FIN。主动关闭方回复 ACK。这里最值得注意的状态是TIME_WAIT。主动关闭方在发送最后一个 ACK 后不会立即进入 CLOSED 状态而是要等待 2MSLMaximum Segment Lifetime报文最大生存时间后才关闭。MSL 在 Linux 内核中配置为 30 秒所以 TIME_WAIT 持续约 60 秒。很多高并发的服务端程序都会遇到 TIME_WAIT 连接过多的问题因为它们经常作为主动关闭方。TIME_WAIT 状态残留的连接会占用端口和内存资源如果量太大可能导致端口耗尽。我在处理生产环境问题时常用的排查命令是netstat -nat | awk {print $6} | sort | uniq -c | sort -rn或者用ss命令替代已经有些过时的netstatss -s如果发现 TIME_WAIT 数量异常大需要检查连接是否被频繁建立和关闭。对于服务端来说更优雅的做法是尽量设计长连接避免反复握手、挥手带来的开销和 TIME_WAIT 堆积。2.3 Linux 下的连接状态观察与内核调优Linux 内核提供了完备的工具来观察 TCP 连接状态。我最常看的是/proc/net/tcp和ss命令的输出。ss比netstat更推荐使用因为它直接从内核的 inet_diag 模块读取信息速度快、信息全而且不会因为文件描述符数量多而卡顿。ss -t -a -n这条命令列出所有 TCP 套接字的状态。如果你发现大量连接卡在 SYN_SENT说明本机发出的连接请求没有得到响应可能是对端端口未监听、防火墙拦截或者网络路由不通。如果你看到大量 SYN_RECV那就要高度警惕 SYN Flood 攻击了——服务端收到了 SYN 但没有收到后续的 ACK一直处于半连接状态占着半连接队列。对于半连接队列和全连接队列的使用情况Linux 也提供了统计信息netstat -s | grep -i listen这个命令会给出“times the listen queue of a socket overflowed”之类的数据。如果 overflow 数值在持续增长说明 accept 队列满了应用层无法及时接收新连接通常需要调整应用的处理能力或者适当加大net.core.somaxconn和 socket 的 backlog。3. 可靠传输核心机制逐层拆解3.1 流水线传输窗口思想的价值在介绍滑动窗口之前我想先讲一个看似不起眼但影响深远的设计——流水线传输。如果 TCP 采用“发送一个包等一个 ACK再发一个包”的停等协议网络利用率会惨不忍睹。假设一个往返时间RTT是 100ms带宽是 10Mbps那么一个包的传输时间大约只有 1ms剩下 99ms 都在空等 ACK线路利用率不到 1%。TCP 的做法是引入窗口——允许发送方在未收到 ACK 的情况下连续发送多个包。窗口内部的逻辑可以类比工厂里的流水线工人发送方不断往传送带网络上放零件数据包只要传送带末端的质检工接收方还没反馈“这批零件合格ACK”工人就持续按顺序放。一旦收到反馈工人就知道前面的零件已经安全入库可以继续放新的零件。这个“窗口大小”就决定了同一时刻可以有多少包“在途”。窗口太小流水线会频繁阻塞窗口太大又可能导致大量包积压在网络里一旦出现丢包重传的代价会很高。3.2 确认号与累计确认用最少的反馈确认最多的数据TCP 的 ACK 机制是“累计确认”的。接收方发送的 ACK 号表示“我已经成功收到这个序号之前的所有字节请从这个序号继续发送”。举个例子发送方发送了序列号 1、101、201 三个包。接收方成功收到 1 和 201但 101 丢了。因为 101 没到即使 201 先到接收方也不能确认超过 101 的任何内容。接收方会连续发送 ACK 号为 101 的确认包表示“我还在等 101”。这种累计确认的好处是接收方无需对每个包都单独发送 ACK减少了网络开销。代价是它无法精确告诉发送方“201 到了但 101 丢了”发送方只能猜测丢包的具体位置。为了弥补这个不足TCP 引入了 SACKSelective Acknowledgment选择性确认选项它允许接收方在 ACK 中额外携带一段或多段“已收到的不连续数据范围”。这样发送方就能精确知道哪些包需要重传而不是盲目地从丢包点之后全部重发。Linux 下默认开启 SACK用ss -t -i能看到当前 socket 是否启用了 SACK 选项。sysctl net.ipv4.tcp_sack为 1 表示开启。我在实际排查中遇到大文件传输慢的案例时经常会先确认一下链路两端的 SACK 是否都开启因为一端关闭 SACK 会导致传输效率显著下降尤其是链路丢包率稍高的时候。3.3 重传机制从超时重传到快速重传可靠传输的核心底线是“丢了我再发”。TCP 的重传分为两大类第一类是超时重传RTO。发送方给每个发送的包设置一个计时器如果超过 RTORetransmission Timeout时间还没有收到对应 ACK就重新发送该包。RTO 的设定不是拍脑袋定的它必须基于当前网络的 RTT 动态计算。Linux 内核会维护一个平滑后的 RTT 估计值和偏差值公式大致为RTO SRTT 4 × RTTVAR。如果网络波动大RTO 会自动拉长避免不必要的重传。第二类是快速重传。发送方收到同一个序列号的重复 ACK 超过一定次数通常是 3 次时可以断定该包已经丢失直接重传不必傻等 RTO 超时。这个机制能显著缩短丢包后的恢复时间。打个比方你和朋友约好每天汇报进度如果某天你说到下午 5 点还没收到朋友的“收到”回复但连续听旁边的人说他也没收到你就应该主动再发一次而不是干等到晚上 12 点。快速重传之后TCP 还会进入快速恢复阶段。经典实现是 Reno 算法把拥塞窗口减半然后执行拥塞避免而不是重新从慢启动开始。这样既对丢包做出了反应又尽量保持了网络的吞吐量。3.4 滑动窗口与流量控制别让接收方不堪重负滑动窗口这个机制很多人会跟拥塞窗口搞混。它们虽然都叫“窗口”但服务的对象完全不同。接收窗口rwnd接收方根据自身缓冲区剩余空间通告给发送方的窗口目的是流量控制——防止发送方发得太快把接收方的缓冲区打爆。拥塞窗口cwnd发送方根据网络拥塞程度自己维护的窗口目的是拥塞控制——防止发送方一次性灌太多的数据把网络设备打爆。发送方实际能够发送的数据量取决于min(rwnd, cwnd)。这就像家里请客吃饭厨房能同时做多少道菜拥塞窗口和餐厅能坐多少人接收窗口你实际一次能端的菜数量取决于两者中较小的那个。接收窗口的调整过程非常直观。接收方在每份 ACK 中都会携带窗口大小字段Window Size告诉发送方“我的缓冲区还剩多少”。如果接收方处理速度跟不上窗口就会缩小甚至变成 0。窗口为 0 时发送方必须停止发送直到收到接收方发来的“窗口更新”消息。但这里有一个容易被忽略的细节窗口更新消息本身是可能丢失的。为了防止发送方和接收方相互等待陷入死锁TCP 规定发送方在窗口为 0 时仍然需要定期发送一个“窗口探测包”来触发接收方重新通告窗口大小。3.5 拥塞控制慢启动、拥塞避免、快重传、快恢复拥塞控制是 TCP 协议里最复杂、也是影响传输性能最大的部分。我在优化跨机房数据传输时被拥塞控制的几个阶段折磨过很久这里把核心脉络梳理一下。TCP 连接刚建立时拥塞窗口非常小一般只有 10 个 MSSMaximum Segment Size最大报文段长度。为了快速探测网络的可用带宽TCP 采用慢启动算法每收到一个 ACK拥塞窗口就增加一个 MSS。这个过程是指数增长的——收到 1 个 ACK 变 22 变 44 变 8。看似段时间内窗口就变得很大但这是有代价的一旦网络支撑不住就会大量丢包。为了避免指数增长到崩溃TCP 设置了一个慢启动阈值ssthresh。当拥塞窗口达到或超过 ssthresh 时进入拥塞避免阶段窗口从指数增长改为线性增长每经过一个 RTT窗口才增加一个 MSS。这样网络可以慢慢地、尽量无损地被填满。当发生丢包时根据丢包的类型不同拥塞控制策略也不同如果是超时丢包说明网络已经非常拥塞TCP 会直接把 ssthresh 减半拥塞窗口降为 1重新开始慢启动。如果是快速重传收到 3 次重复 ACK说明网络可能只是轻微拥塞TCP 进入快速恢复拥塞窗口减半然后从减半后的窗口继续线性增长。这样恢复速度更快吞吐量损失更小。这四种算法——慢启动、拥塞避免、快速重传、快速恢复——合称 TCP Tahoe/Reno 时代的经典拥塞控制体系。虽然现在 Linux 默认的拥塞控制算法已经换成了 BBR 或 Cubic取决于内核版本和发行版但这套经典体系仍然是理解后来一切拥塞控制算法的基础。Cubic 针对高带宽长链路场景做了改进BBR 则彻底换了一套思路——不再以丢包作为拥塞信号而是通过测量瓶颈带宽和最小 RTT 来建模网络效果在弱网和长肥链路下尤其明显。在 Linux 下查看当前系统的拥塞控制算法sysctl net.ipv4.tcp_congestion_control如果需要临时切换可以sysctl -w net.ipv4.tcp_congestion_controlbbr不过需要内核模块支持先检查sysctl net.ipv4.tcp_available_congestion_control里面没有 bbr 的话需要先加载模块。生产环境调整拥塞控制算法会影响所有新建立的 TCP 连接建议在业务低峰期操作并且先在测试环境验证效果。4. Linux 内核中的 TCP 实现与关键参数4.1 socket 与内核协议栈的数据流动路径Linux 中 TCP 协议栈的实现是分层的应用层通过系统调用send()把数据从用户态拷贝到内核态内核在 TCP 层进行分段、序号分配、窗口管理等操作然后交给 IP 层封装成 IP 包最后通过网卡驱动发送出去。接收路径反过来网卡收到数据包后经过中断处理、IP 层校验、TCP 层排序重组最终把数据从内核缓冲区拷贝到用户态缓冲区唤醒正在recv()中等待的应用进程。理解这条数据路径后你会发现一个关键点TCP 的可靠传输离不开内核缓冲区。发送缓冲区存着已发送但未确认的数据接收缓冲区存着已到达但尚未被应用读取的数据。rwnd的大小就是接收缓冲区剩余空间的直接反映。如果应用进程读数据读得慢接收缓冲区就会被占满窗口随之收缩最终形成“反压”——接收应用的处理速度会通过 TCP 的流量控制传递回发送端。这块缓冲区的大小可以通过 socket 选项调整。我在压测高吞吐服务时经常用到sysctl net.core.rmem_max sysctl net.core.wmem_max分别表示接收和发送缓冲区的最大值。应用也可以直接用setsockopt()设置SO_RCVBUF和SO_SNDBUF但内核会对这两个值做一些边界调整所以设置后记得用getsockopt()确认生效值。4.2 影响可靠传输的关键内核参数整理Linux 内核把所有 TCP 参数都暴露在/proc/sys/net/ipv4/下用sysctl命令可以动态调整。我在不同项目中反复验证过下面这几个参数对可靠传输的影响最大值得专门列出来参数默认值作用与调整建议tcp_sack1开启选择性确认大幅提升高丢包率链路的传输效率建议保持开启。tcp_tw_reuse0允许在 TIME_WAIT 状态下复用端口缓解高并发客户端端口耗尽问题但对服务端无效。tcp_tw_recycle0快速回收 TIME_WAIT 连接但 NAT 环境下会引发严重问题强烈建议不要开启。tcp_max_syn_backlog128半连接队列长度SYN Flood 或高连接并发时适当加大。tcp_keepalive_time7200空闲探活间隔默认 2 小时。服务端应用可以调短到 60-120 秒尽早感知失效连接。tcp_retries13丢弃连接前的最小重传次数低于此值不通知应用层。tcp_retries215主动放弃连接前的最大重传次数这个值决定了断线检测的总耗时。tcp_fin_timeout60等系统自动关闭 FIN_WAIT_2 状态连接的超时时间。这些参数不建议盲目照搬网上的“优化大法”一定要结合业务场景来判断。比如tcp_tw_recycle在 NAT 环境下的问题特别严重因为 NAT 设备后面的多台机器可能共享一个公网 IP 和端口如果内核因为时间戳乱序而丢弃连接用户会诡异掉线排查起来非常痛苦。我踩过这个坑之后对这类“为了省资源而牺牲兼容性”的参数一律保持谨慎。4.3 粘包、拆包与 TCP 的字节流本质很多刚开始写网络程序的开发者都会遇到“粘包”问题发了两条数据对方收数据的时候却一次收到了一大段。这其实是 TCP 的字节流特性决定的。TCP 不管应用层的数据边界它只负责把一串字节可靠地按序交付。应用层调用两次send()写入的数据可能被 TCP 合并为一个报文段发出也可能被拆散成多个报文段。接收方调用一次recv()读了 1000 字节不代表这 1000 字节一定来自同一次send()。粘包问题的解决思路不是让 TCP 去改行为而是在应用层设计消息边界。常见方案有三种固定长度消息每条消息定长不足补零。简单粗暴适合协议特别简单的场景。分隔符协议比如用\n或自定义特殊符号分隔消息像 HTTP 的 header 部分就是这样。如果消息体里也可能出现分隔符需要做转义处理。长度前缀法每条消息前面加上 4 字节的长度字段接收方先读长度再读完整消息。这是目前最通用的方案gRPC、Redis 协议都采用了类似思路。我在实际项目中最推荐的是长度前缀法。它解析效率高而且不容易出错。如果你用 Go 写服务encoding/binary包里的BigEndian/LittleEndian可以方便地完成长度字段的读写。用 C 的话要注意字节序问题——网络字节序是大端千万别拿本机的小端序直接写入包。5. 性能调优与线上问题排查实操5.1 抓包分析的基本姿势排查 TCP 问题最重要的技能不是背参数而是会抓包、会看包。我的习惯是先用tcpdump抓原始包存成 pcap 文件再用 Wireshark 分析。命令行下轻量查看时tcpdump配-S和-A参数已经足够。抓包时机很关键。很多问题不是持续出现的而是突发的所以最好在问题可能发生的时间段提前抓包。比如怀疑是重传导致的延迟可以这样抓tcpdump -i eth0 tcp and host 10.0.0.5 and port 8080 -S -w /tmp/tcp_problem.pcap等复现后CtrlC停止抓包把 pcap 文件拖到 Wireshark 里。Wireshark 有一个特别方便的功能右键任意 TCP 包选择“Follow TCP Stream”就能看到完整的数据流还能通过菜单的 Statistics - TCP Stream Graph 画出时序图、吞吐图、RTT 图。抓包时一个常见的坑是在应用所在的本机上只抓到了发出的包没抓到对端回复的包导致看不出 RTT 和重传。这种情况下要么在链路两侧都抓包做对比要么在中间交换机的镜像端口上抓包。单纯看一端的包有时会把问题归错方向。5.2 常见问题速查表我把这些年排查过的 TCP 问题整理成了一个速查表按现象、可能原因、排查命令的思路来组织供大家参考现象可能原因排查命令与方法连接建立超时卡在 SYN_SENT对端未监听、防火墙拦截、路由不通ss -t -a -ntelnet ip porttcpdump连接建立后马上被重置RST对端端口无进程监听、应用主动关闭抓包观察 RST 包来源netstat -ap查进程TIME_WAIT 堆积过多短连接场景频繁、主动关闭方处理不完ss -s观察应用层改用长连接必要时调tcp_tw_reuse大量 SYN_RECV半连接队列溢出、SYN Floodnetstat -s看 SYN 丢弃计数调tcp_max_syn_backlog传输吞吐低、CPU 占用高小包过多、中断频繁、缓冲区过小抓包看包大小和 PSH 标志调wmem_max/rmem_max延迟高、频繁超时重传链路拥塞、RTO 配置不合理、丢包率高Wireshark 查看 TCP 重传率ping测 RTT 和丢包率应用层读到半包消息边界设计不合理应用层协议加长度字段或分隔符这些常见问题里最容易被忽视的是小包问题。网络编程新手容易频繁调用send()发送少量数据导致一个 TCP 报文段只承载几十个字节加上 IP 头和 TCP 头网络有效载荷率极低。Nagle 算法在一定程度上会把小包合并但它同时会引入延迟——尤其是在开启延迟 ACK 的情况下。对于实时性要求高的消息可以关闭 Nagle 算法设置TCP_NODELAY对于批量数据上传保持默认通常更合适。5.3 一个真实延迟问题的排查复盘去年帮一个客户排查过一次跨机房数据传输延迟高的问题过程很有代表性分享一下。现象业务方反馈两个机房之间同步数据平均延迟从原来的 50ms 涨到了 800ms而且带有明显的周期性。第一反应是检查网络 RTTping一下发现延迟只有 20ms说明基础网络没问题问题大概率出在 TCP 传输层。在发数据的主机上抓包用 Wireshark 打开后发现两个关键现象一是大量 TCP 包出现 Dup ACK二是每隔一段时间就有一连串的重传。这说明链路存在比较高的丢包率正触发快速重传甚至超时重传。但ping的丢包率并不高网络设备监控也显示一切正常这就比较诡异了。继续深挖发现重传的包总是集中在某一条固定路径而且这些报文段的长度都比正常数据包大不少。最终定位到问题根源中间路由设备的 MTU 设置不一致导致大包被丢弃。TCP 没有自动发现路径 MTU 的能力——准确地说PMTUDPath MTU Discovery机制在 ICMP 被防火墙过滤后就失效了。解决方法是把 TCP 的 MSS 手动调小避免超过链路最小 MTUip link set eth0 mtu 1400或者更精准地在应用层设置TCP_MAXSEGsocket 选项让 TCP 段长不要超过 1400 字节。调整后重传立刻消失延迟恢复到正常水平。这个案例给我的经验有两点第一排查 TCP 问题不能只盯着应用层链路层的 MTU、中间设备的过滤规则都要纳入考虑第二抓包分析时Dup ACK 和重传的出现是有规律的弄清楚模式才能定位到具体环节。5.4 性能调优的合理边界与反思关于性能调优我最想强调的一点是不要为了调优而调优。很多开发者喜欢一上来就改一堆内核参数结果网络不但没有变快反而更不稳定。我从实际项目中总结出来的调优原则有这几点优先从应用层优化比如减少不必要的连接、选择合适的消息大小、批量读写数据。应用层的合理设计和高效编码往往比调整内核参数带来的提升更大。内核参数调整要有基准。调之前记录吞吐量、延迟、CPU 占用、丢包率等数据调完后再对比形成闭环验证。没有数据支撑的调优都是玄学。涉及连接复用和回收的参数比如tcp_tw_reuse和tcp_tw_recycle要格外谨慎。前者在客户端场景可以理解后者建议直接不要碰。调整完参数后要确认连接确实在按预期行为运行。比如调大缓冲区后用ss -t -i查看实际窗口大小是否变大开启 BBR 后检查 cwnd 增长曲线是否明显跟之前的 Cubic 不同。真实世界里TCP 的传输性能往往受限于网络链路的质量和应用的写码质量内核参数反而是最后才需要动的地方。一个设计良好、读写了合理大小数据的程序用默认内核参数同样能跑出很高的吞吐量。6. 个人体会与最后提醒把这些机制全部串起来之后我最大的感受是TCP 可靠传输的本质不是在网络完美的时候表现完美而是在网络不完美的时候还能维持一个可用的状态。序列号和 ACK 构建了“对账”的基础重传机制是“纠错”的手段双窗口是“限速”和“防拥堵”的策略。整个协议栈像是一套精密配合的交通管制系统每一个机制都在为同一个目标服务在有损环境下尽可能高效地把数据送到对端。如果你是在 Linux 下做网络相关开发的我非常建议你找一个测试环境自己用 Python 写两个 socket 程序一个发一个收配合tcpdump和 Wireshark 观察整个传输过程中的细节变化。你也可以用ss -t -i实时观察一个 socket 的 cwnd、rtt 和重传情况。纸上得来终觉浅亲手抓过几次包之后你对 TCP 的理解会有一个质的提升。最后再分享一个小技巧写网络程序时建议把 TCP 相关参数和连接状态暴露到监控系统里。哪怕只是在服务里定时打一条日志记录当前 socket 的重传次数、平均 RTT、发送窗口大小都能在问题排查时省下大量时间。TCP 的问题是慢性的等用户报障时往往已经持续了很久有了实时数据积累才能把问题定位从“事后猜”变成“事前看”。
返回列表