聊TCP,三次握手和四次挥手是绕不开的两个关。不管你是面后端、面网络,还是平时排查线上故障,这两个概念几乎必考,也几乎都是“背下来容易,讲明白很难”的代表。我第一次真正把它们弄懂,不是靠八股文,而是在一次线上偶发断连的排障里,抓包看到SYN、ACK、FIN的来回交错,才意识到协议栈的状态机设计得比我想象中严谨得多。
这篇文章不打算复述教科书,而是以应用开发和运维的视角,把TCP连接从建立到拆除的完整生命周期拆开讲清楚:为什么会是三次、为什么会是四次、状态机怎么转、线上异常怎么对应,最后用几张对比表格把知识点固化下来。不管你是刚入门的学生,还是被TIME_WAIT、CLOSE_WAIT困扰过的后端工程师,都能从中找到可以直接上手的结论。
1. 三次握手的系统性拆解
1.1 第一次握手到底在做什么
先说结论:三次握手的本质,是通信双方在正式传输数据之前,先完成两件事——确认对方在线,并且交换彼此的初始序列号。
第一次握手的报文长这样:客户端发送一个SYN报文,SYN标志位置1,同时携带一个随机生成的初始序列号,我们叫它seq=x。这个x不是从0开始的固定值,而是由操作系统根据时间、随机数等因子生成的,目的很简单,防止攻击者猜出序列号,也避免新旧连接的报文混淆。这个随机初始序列号,是很多人看抓包时容易忽略的细节。你去抓一次包,会发现同一个IP上新建的连接,seq基本都是不同的,这就是ISN(Initial Sequence Number)在起作用。
服务端收到这个SYN之后,不会立刻回复一个单独的ACK,而是会在内存里为这个半连接分配一个控制块,记录客户端的ISN,然后进入SYN_RCVD状态。这里有个容易踩的坑:很多人以为三次握手是“客户端SYN,服务端ACK,客户端再ACK”,但第三步的ACK其实不是对第二步的确认,而是对第一步SYN的确认,同时也可以看作对服务端“宣告自身序列号能力”的应答。严格讲,第二步是SYN+ACK,它是把“确认”和“同步”两个语义合并到了一个报文里。
抓包验证很简单,tcpdump过滤一下SYN标志:
tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'这时候你看到的基本都是SYN报文。如果把这个过滤条件改成syn和ack同时存在,你会看到握手阶段的第二个报文,几乎一模一样是46字节左右的包。这个“报文大小接近”的特点,在排查防火墙丢包时很有用——很多防火墙策略会把SYN和SYN+ACK当作两类流量分别控制。
1.2 为什么不能是两次握手:三个硬理由
这个问题面试必问,也是真正理解三次握手的关键。我见过很多人背答案说“因为要防止失效的连接请求”,但只答这一句是远远不够的,至少有三个层面要考虑。
第一,防止历史重复SYN干扰。假设客户端先发了一个SYN,因为网络拥塞卡了很久,客户端超时后重新发起新连接,这时旧SYN才姗姗来迟。如果是两次握手,服务端收到这个旧SYN就直接建立连接、分配资源,但客户端早已关闭了这个端口,根本不会理它。结果就是服务端白白维护一个“假连接”,直到超时才能回收。而在三次握手中,客户端收到服务端对旧SYN的响应后,会通过序列号判断这不是本次握手的回应,于是发送RST报文把这个假连接干掉,服务端就能及时释放资源。
第二,双向确认。TCP是全双工协议,数据要双向传输,所以双方的“发送能力”和“接收能力”都必须被验证。两次握手只能确认“客户端能发、服务端能收”这个单向通道是通的,无法确认“服务端能发、客户端能收”这个反方向是否正常。第三次握手时,服务端收到客户端发来的ACK,才能确定自己的发送通道对客户端是可达的。这个理由特别适合用来解释为什么挥手必须是四次——因为两个方向的通道要分别关闭。
第三,序列号同步。只有双方都确认了对方的初始序列号,后续数据包的编号才能对得上。客户端通过第二步SYN+ACK拿到服务端的ISN,服务端通过第三步ACK确认客户端已经收到了自己的ISN,这样双方才建立起一致的“数据编号上下文”。
我用一个生活中的例子来比喻:你约朋友去咖啡店碰头,不会刚喊一声“我到门口了”就直接开始点单。得先对方回应“我也到了”,你再确认一句“好,我看到你了”,双方才会开始交流。前两句只能证明你俩都在附近,第三句才确保双方都完成了信息对齐。
1.3 握手状态链与服务端半连接队列
聊完理论,看状态流转。三次握手过程中,客户端的连接状态变化是:CLOSED → SYN_SENT → ESTABLISHED。服务端则是:LISTEN → SYN_RCVD → ESTABLISHED。这里有个关键点,服务端的SYN_RCVD状态不是孤立存在的,它和内核里的半连接队列绑在一起。
在Linux上,服务端收到SYN后,内核会把这个连接放进两个队列之一:半连接队列(syn queue)和全连接队列(accept queue)。半连接队列里放的是已经收到SYN、还没完成三次握手的连接;全连接队列里放的是已经完成握手、等着应用层调用accept()取走的连接。很多“connection reset by peer”、“握手超时”问题,根源不在应用代码,而是这两个队列满了。
控制这两个队列的内核参数分别是:
# 半连接队列上限 net.ipv4.tcp_max_syn_backlog = 65535 # accept队列上限 net.core.somaxconn = 65535注意,应用层监听socket里的backlog参数也会限制accept队列大小,最终的有效值是min(somaxconn, backlog)。也就是说,你光在nginx配置里写了listen 80 backlog=65535,但内核的somaxconn还是默认的4096,队列照样上不去。这是我在排查Nginx高并发连接时踩过的坑,后面会展开说。
半连接队列最容易受SYN Flood攻击。攻击者不断发送SYN但不回应第三步ACK,服务端半连接队列很快被占满,正常用户的SYN就进不来了。Linux的应对机制是tcp_syncookies:不分配半连接队列资源,直接把“序列号+时间戳+源地址”等信息加密成一个cookie放在SYN+ACK里,等客户端回ACK时再重构连接信息。这个机制保证极端情况下正常用户仍能完成握手,代价是稍微增加CPU计算量。生产环境一般建议开启:
net.ipv4.tcp_syncookies = 1日常查看半连接队列积压情况,可以用:
ss -tan state syn-recv ss -lnt如果syn-recv状态的连接数长期居高不下,或者ss -lnt里某个监听端口的Recv-Q队列持续非零,就要警惕是不是有人在打SYN包,或者backlog配置太小。
2. 四次挥手的完整生命周期
2.1 为什么挥手是四次:半关闭设计
四次挥手比握手多一次,原因在于TCP是全双工协议,建立连接时一个SYN+ACK报文可以同时干两件事,但断开连接时不行——两个方向的关闭动作必须独立完成,而且不一定同时发生。
具体拆开看:客户端要关闭“自己发送数据”这个方向,发一个FIN报文;服务端收到后回ACK,表示“知道了”,但服务端可能还有数据没发完,所以不能立刻回FIN。等服务端把最后一个字节发完,它也要关闭“自己发送数据”的方向,于是发FIN;客户端收到后回ACK,整个连接才能关闭。
你可以类比成两个人打电话道别:先提出挂电话的人说“我要挂了”(FIN),对方应一声“好的我知道了”(ACK),但对方可能还有事没说完,于是继续说;等说完了,对方再说“我也说完了,挂吧”(FIN),先前提出挂电话的人再应一句“好,挂吧”(ACK)。这个过程天然就是四次交互,不是三次。
为什么不能像握手一样把两步合并成一步?因为在第三次挥手时,被动关闭方不一定能立刻close。它可能还在处理业务逻辑,或者还有积压数据要写。如果强行规定收到FIN就必须马上FINAL,那就要求应用层和内核协议栈完全同步关闭,这在现实中是不可能的。所以协议设计上必须允许“半关闭”状态,让一方先关闭发送,另一方继续发送。
2.2 四次挥手的四个报文与状态流转
挥手过程看起来比握手复杂,状态也多不少。我按主动关闭方(通常是客户端,但也可以是服务端)的视角走一遍:
第一步,主动方调用close(),内核发送FIN报文,主动方进入FIN_WAIT_1。这个状态很短暂,如果网络通畅,马上就会收到对方的ACK。
第二步,被动方收到FIN,内核立刻返回ACK,被动方进入CLOSE_WAIT状态。这时候被动方的应用程序会感知到EOF,如果程序逻辑写得没问题,它应该着手释放资源、关闭自己的套接字。主动方收到ACK后,从FIN_WAIT_1进入FIN_WAIT_2。
第三步,被动方处理完所有数据,调用close(),内核发送FIN,被动方进入LAST_ACK状态。
第四步,主动方收到FIN,发送最后一个ACK,然后进入TIME_WAIT状态。被动方收到这个ACK后,从LAST_ACK进入CLOSED。
这里有一个非常关键的细节,很多人忽略:如果最后一个ACK丢了,被动方会一直处于LAST_ACK状态,等待超时后重发FIN。而主动方因为处于TIME_WAIT,收到重发的FIN后还会再次回ACK。也就是说,TIME_WAIT状态的存在,就是为了兜底处理“最后一个ACK丢失”这种情况。我在抓包时见过几次这种重发FIN的现场,如果不理解状态机,很容易误判为“连接异常”。
顺手把状态流转用表格列出来,方便对照:
| 阶段 | 主动方状态 | 被动方状态 | 关键报文 |
|---|---|---|---|
| 挥手前 | ESTABLISHED | ESTABLISHED | 正常数据传输 |
| 第一次挥手 | FIN_WAIT_1 | CLOSE_WAIT | FIN |
| 第二次挥手 | FIN_WAIT_2 | CLOSE_WAIT | ACK |
| 第三次挥手 | TIME_WAIT | LAST_ACK | FIN |
| 第四次挥手 | TIME_WAIT | CLOSED | ACK |
2.3 TIME_WAIT的2MSL到底在等什么
TIME_WAIT是面试频率极高的考点。等2MSL的原因主要有两个,分开说。
第一个原因是确保最后一个ACK能够到达对端。ACK报文在网络中可能丢失,如果丢了,被动方会重发FIN。主动方只有在TIME_WAIT期间收到重发的FIN,才能重新回ACK。2MSL的时间足够一个报文在网络中“从一端传到另一端又传回来”,所以在这个时间内,即使ACK丢了,也能等来重发的FIN并再次回应。
第二个原因是让本次连接中所有“残留报文”在网络中彻底消失。TCP报文在网络里是有生存时间的,超过MSL就要被丢弃。如果连接立刻释放,端口立刻被新连接复用,理论上旧连接里延迟到达的报文可能被误认为新连接的报文,造成数据错乱。等待2MSL,等于是给所有旧报文留出足够的时间“死干净”。
TIME_WAIT状态下,这个连接的四元组(源IP、源端口、目标IP、目标端口)还不能被新的连接使用。线上如果大量产生短连接,会出现TIME_WAIT堆积,占用大量端口和内存。我在用Java写定时任务时遇到过这个问题:每5分钟new一个Socket连接,跑一天就报“Address already in use”,后面会专门讲怎么根治。
TIME_WAIT持续时间是2MSL,Linux上MSL默认30秒到60秒,所以TIME_WAIT一般持续60秒到120秒。网上有文章建议改小MSL来快速释放端口,短期能缓解端口耗尽,但可能增加数据错乱风险,不建议在生产环境乱调。更健康的方式是减少短连接、使用连接池、开启SO_REUSEADDR。
3. 核心对比与状态速查
3.1 三次握手和四次挥手的报文交互对照
很多教材把握手的三个报文和挥手的四个报文分开讲,但放到一起对比会更清晰。我用表格把两个过程的核心要素摆出来,方便记忆也方便复习:
| 对比维度 | 三次握手 | 四次挥手 |
|---|---|---|
| 报文数量 | 3个 | 4个 |
| 标志位组合 | SYN、SYN+ACK、ACK | FIN、ACK、FIN、ACK |
| 是否可能合并 | SYN+ACK必须合并成一步 | 第二次ACK和第三次FIN不能保证合并 |
| 参与方向 | 双向同步序列号 | 两个方向独立关闭 |
| 是否携带数据 | 标准握手不携带应用数据 | FIN之前的最后数据是合法的 |
| 建连/断开 | 从CLOSED到ESTABLISHED | 从ESTABLISHED到CLOSED |
| 关键状态 | SYN_SENT、SYN_RCVD、ESTABLISHED | FIN_WAIT_1、CLOSE_WAIT、LAST_ACK、TIME_WAIT |
| 何时主动发起 | 客户端主动 | 先关闭的一方主动 |
这个表格里值得强调的是,TCP允许在挥手阶段发送数据。比如被动方收到FIN后,还可以继续发送剩余数据,然后再发FIN。这一点和三次握手完全不同,握手中如果携带数据,很多协议栈会直接忽略或当作异常处理。
3.2 两端状态残留速查表
排障的时候,最基础的本事就是看到ss -tan里的一堆状态,能立刻知道连接处于什么阶段。我把常见状态和它们对应的阶段整理出来,直接照着对:
| 状态 | 属于哪一方 | 含义 | 如果大量出现表示什么 |
|---|---|---|---|
| SYN_SENT | 客户端 | 客户端已发SYN,正在等SYN+ACK | 服务端没响应或防火墙丢包 |
| SYN_RCVD | 服务端 | 已收SYN,正在等客户端ACK | 半连接队列积压、被SYN Flood |
| ESTABLISHED | 双方 | 连接建立成功,正常传输 | 正常,但也要关注数量是否超限 |
| FIN_WAIT_1 | 主动关闭方 | 已发FIN,等ACK | 通常瞬态,如果积压说明对方没回ACK |
| FIN_WAIT_2 | 主动关闭方 | 已收ACK,等对方FIN | 如果长时间不消失,说明对方一直不close |
| CLOSE_WAIT | 被动关闭方 | 已收FIN,应用还没闭 | 应用程序忘了close,必须查代码 |
| LAST_ACK | 被动关闭方 | 已发FIN,等最后一个ACK | 通常瞬态,积压说明最后的ACK丢了 |
| TIME_WAIT | 主动关闭方 | 等待2MSL确保旧包消失 | 短连接过密,端口可能不够用 |
| CLOSED | 双方 | 连接关闭 | 正常 |
记住一个规律:看到CLOSE_WAIT,先查应用代码;看到TIME_WAIT,先查连接的使用方式和端口规划;看到SYN_RCVD,先查队列参数和是否被攻击。这三个状态是线上排障中最常遇到,也最能反映问题的。
3.3 常见误区:SYN、ACK、FIN、RST别搞混
聊几个高频误区,都是我实际见过有人犯错的点。
第一个误区是分不清RST和FIN。RST不是正常的关闭方式,是一次异常重置。端口没有监听、接收缓冲区有未读数据但连接被强制关闭、超时被协议栈判定异常时,都会出现RST。线上看到大量RST,优先怀疑应用异常退出或防火墙策略。FIN是“好聚好散”,RST是“当场摔电话”,两者的语义完全不同。
第二个误区是以为断开一定是服务端先发FIN。其实谁先close,谁就先发FIN。客户端可以主动断开,服务端也可以。我们习惯上把“客户端—服务端”代入主动和被动,但真正决定权在应用层,谁先调用close,谁就是主动方。很多新手看到服务端出现TIME_WAIT就惊讶,其实如果服务端主动关闭连接,它照样会进TIME_WAIT。
第三个误区是不理解三次握手绝对不传应用数据。标准TCP握手第三个ACK报文里虽然可以带上数据(比如HTTP早期的一些实现),但绝大部分情况下,应用数据都是握手完成、ESTABLISHED之后才发送的。如果面试或者做协议解析时看到握手阶段就带负载,要么是TFO(TCP Fast Open)这种新特性,要么就是伪装的扫描工具,正常业务下少见。
4. 开发与运维中的TCP排查实录
4.1 案例一:Java客户端重连时报“地址已在使用”
这个案例几乎能映射到所有使用短连接的语言。现象是定时任务每隔几分钟就new一个Socket去连服务端,跑一段时间后开始报java.net.BindException: Address already in use。
根本原因很清晰:每次Socket close后,客户端端口进入TIME_WAIT状态,2MSL内(几十秒到两分钟)这个端口不能被复用。如果任务跑得密集,系统可用端口就越来越少,最终耗尽。Linux上临时端口的默认范围在/proc/sys/net/ipv4/ip_local_port_range里,一般是32768到60999,总共也就两万多个,短连接一多很容易见底。
解决思路分几层。第一层,能用长连接就尽量别用短连接,每次查询复用同一个连接或使用连接池;第二层,如果确实需要频繁创建连接,客户端Socket可以设置SO_REUSEADDR,允许端口在TIME_WAIT阶段被复用;第三层,如果端口还是不够,适当扩大ip_local_port_range,但这属于治标。Java里设置SO_REUSEADDR很简单:
Socket socket = new Socket(); socket.setReuseAddress(true); socket.connect(new InetSocketAddress(host, port), 3000);这里有个容易被忽略的技术细节:客户端的SO_REUSEADDR,作用是允许处于TIME_WAIT状态的本地端口被新的连接复用,并不是像某些文章说的那样“服务端必须加这个才能重启”。服务端监听socket加SO_REUSEADDR的意义在于,当服务进程重启、端口还在TIME_WAIT中时,可以立刻重新绑定监听端口,否则会直接绑定失败。所以说,客户端和服务端的SO_REUSEADDR解决的是不同问题。网上很多教程把它们混为一谈,实际排障时会误导人。
4.2 案例二:服务端CLOSE_WAIT堆积
CLOSE_WAIT堆积是后端开发最常遇到的连接泄漏。现象很典型:ss -tan一看,几千个CLOSE_WAIT状态的连接挂在同一个服务上,内存和文件描述符持续上涨,最终所有新连接都建不进来。
CLOSE_WAIT怎么产生的?前文讲过,被动方收到FIN后进入CLOSE_WAIT,如果应用层没有调用close,这个状态就永远停留在那里。Java里头,最常见的场景是用了HttpClient、Socket、InputStream但没在finally里关闭资源;或者业务线程阻塞在某个调用上,finally代码没执行到;或者连接池里的空闲连接被对端断开,但池本身不检测失效连接。
排障三板斧:
# 1. 看有多少CLOSE_WAIT ss -tan | grep CLOSE-WAIT | wc -l # 2. 看具体是哪些连接 ss -tan state close-wait # 3. 如果是Java进程,配合看线程栈 jstack <pid> | grep -A 20 "http-nio\|SocketRead"从经验看,CLOSE_WAIT爆炸往往是代码问题,不是网络问题。修复思路是:所有涉及连接、流、通道的代码,用try-with-resources或者try-finally保证close;设置合理的读超时,避免线程一直阻塞在read上;连接池要开启空闲检测和回收。
4.3 案例三:Nginx反向代理TCP最大连接数上不去
一个真实场景:内网某服务用Nginx做四层反向代理,压测到几千并发连接时,客户端开始大量报connection reset。当时第一反应是改Nginx配置,把worker_connections从1024调成10240,listen 80 backlog=8192也写了,结果还是不行。最后发现瓶颈不在Nginx,而在系统参数。
真正的限制链条是这样的:客户端发起SYN,经过Nginx时先经过全连接队列。accept队列的有效长度是min(net.core.somaxconn, backlog)。系统默认somaxconn只有4096,虽然listen backlog写了8192,但生效值还是4096。压力一上来,全连接队列溢出,内核直接丢包,客户端重试无果后报错。
正确的调整组合是:
# 内核层 net.core.somaxconn = 65535 net.ipv4.tcp_max_syn_backlog = 65535 # nginx层 worker_processes auto; events { worker_connections 65535; accept_mutex off; } listen 80 backlog=65535;另外还要看文件描述符上限,Nginx单进程承载的连接数不能超过ulimit -n:
ulimit -n 655350这个案例说明一个规律:TCP连接相关的坑,十有八九不是单一因素,而是应用配置、内核参数、资源限制三层堆叠造成的。排查的时候,不能只看自己熟悉的层面。
4.4 案例四:抓包看到大量tcp dup ack,怎么判断丢包还是乱序
TCP的Dup ACK机制属于传输可靠性的一部分,但线上看到它不代表一定出故障,要结合上下文判断。我曾经在半夜接到报警,说某个服务节点网络抓包出现大量TCP Dup ACK,服务本身却没有任何报错。后来分析发现,是上游设备短暂拥塞导致几个包乱序到达,属于网络中的正常现象,不用处理。
判断方法是看抓包时间线和序列号。正常的乱序表现为:某条连接里,对端连续回了几个“Dup ACK”后,缺失的包很快又到了,然后继续正常传输,整个过程RTT没有明显上升。真正的丢包则表现为:同一个序号的重传包反复出现,伴随RTT变长,而且Dup ACK持续不休,直到重传到达才恢复。
实战中,我习惯用Wireshark的Expert Information面板,它会自动归类Retransmission、Dup ACK、Out-of-Order。看到大量Out-of-Order但重传很少,基本可以判定是网络路径上的乱序问题;看到Retransmission和Dup ACK成对出现且频繁,就要去查对端网卡丢包、带宽打满或者防火墙策略了。
排查丢包还有几个很务实的命令:
# 网卡丢包统计 ip -s link show eth0 # 系统协议栈丢包统计 netstat -s | grep -E "segments retransmited|bad segments|resets received"网卡的RX dropped如果持续增长,基本可以锁定是物理层或驱动问题;协议栈统计里retransmited偏高,则大概率是网络拥塞或对端处理不过来。
4.5 Windows下查看TCP全局参数
不是所有环境都是Linux。Windows服务器也有对应的TCP参数查看方式,先记住这个命令:
netsh interface tcp show global这个命令能一次看到Windows的TCP全局配置:接收窗口自动调优级别、ECN能力、定时器设置、初始拥塞窗口等。我在维护Windows Server上的高并发长连接服务时,发现Windows默认的TCP设置和Linux差异很大,接收窗口的自动调优有时会限制大流量下载的吞吐。如果确认是Windows端带宽上不去,可以手动关闭自动调优级别或者固定为normal:
netsh interface tcp set global autotuninglevel=normalWindows上处理TIME_WAIT堆积,不像Linux那样有tcp_tw_reuse,通常要改注册表。两个关键值:TcpTimedWaitDelay(默认120秒,可调小)和MaxUserPort(限制用户可用的端口数量,默认很小)。改注册表有风险,建议在测试环境验证后再上生产,不要盲目调低TIME_WAIT,否则一样有旧包错乱风险。
5. 从背概念到真会用的三个落地建议
5.1 亲手抓一次包,胜过看十篇文章
学TCP协议最忌讳只看图,不动手。抓包工具用tcpdump加Wireshark就够。我建议在一台Linux测试机上,用一个简单的HTTP请求,同时抓包:
tcpdump -i lo -nn -w handshake.pcap 'tcp port 8080'然后用客户端发起一次HTTP请求。打开pcap文件后,你会在Wireshark里清晰地看到三握手两挥手:先是三条短的握手报文,长度几乎一样;中间夹着HTTP请求响应数据;最后是四条短的挥手报文。把鼠标放上去看标志位,SYN是0x002,SYN+ACK是0x012,ACK是0x010,FIN+ACK是0x011。这些十六进制标志位只要见过一遍,以后看任何抓包都不会发怵。
第二步是做一次“主动关闭”和“被动关闭”的对比实验,可以用nc -l 8080开个监听,然后从客户端连接后立刻关闭客户端,观察服务端的CLOSE_WAIT会不会出现。如果你能在抓包和状态机里亲眼看到这个状态,对TCP生命周期就建立了肌肉记忆。
5.2 把连接生命周期纳入应用设计
很多线上连接问题,根子不在协议栈,而在应用设计。连接什么时候建立、什么时候关闭、异常时怎么重试,这些都应该在写代码时想清楚。我总结几个靠谱的做法。
第一,用连接池而不是临时建连。无论Java的HttpClient还是MySQL的Connector,连接池都能避免大量TIME_WAIT堆积。第二,重试逻辑要带退避,不能无限快速重连,否则客户端端口和系统资源都会被打爆。第三,优雅关闭一定要实现。Java服务关闭时,先停止接收新请求,再等待存量请求处理完,最后调用close释放连接,不要直接在进程退出时强行断开,否则对端会看到大量RST。
第四,长连接要有心跳。TCP本身没有应用层心跳,如果中间设备静默丢弃空闲连接,业务方可能一直以为连接是好的。用TCP keepalive时建议调短默认时间,或者用应用层心跳包。心跳机制的本质,是让连接周期性地产生数据包,从而暴露那些“已死但未被发现”的连接。
5.3 几个值得记住的TCP参数清单
调优TCP连接,不需要背一堆参数,记住常用的几个就够了。我按使用频率列了一个清单:
| 参数 | 作用 | 适用场景 |
|---|---|---|
| net.core.somaxconn | accept队列上限 | 高并发服务、Nginx调优 |
| net.ipv4.tcp_max_syn_backlog | 半连接队列上限 | 抗SYN Flood、大量并发连接 |
| net.ipv4.tcp_syncookies | SYN Flood防护开关 | 推荐开启 |
| net.ipv4.tcp_tw_reuse | 允许TIME_WAIT端口复用(仅客户端出站) | 大量短连接场景 |
| net.ipv4.tcp_tw_recycle | 旧内核的TIME_WAIT快速回收 | 不推荐,4.12后已移除 |
| net.ipv4.tcp_keepalive_time | 空闲多久开始探测 | 调整长连接空闲踢除时间 |
| 端口范围ip_local_port_range | 本地临时端口范围 | 客户端大量短连接时调大 |
注意一个很容易被误导的点:tcp_tw_reuse只对客户端发起的新连接有效,对服务端主动关闭后的TIME_WAIT基本没用。另外,它依赖TCP时间戳选项(tcp_timestamps),如果系统里关了这个选项,tcp_tw_reuse也不会生效。网上很多文章把这些细节混在一起讲,用的时候容易踩坑。
还有一个常见误区是拿tcp_tw_recycle当优化手段。这个参数在旧内核里确实能快速回收TIME_WAIT,但它在NAT环境下会影响同NAT后多个客户端的时间戳校验,导致连接被随机丢弃,体验非常差。Linux 4.12内核已经把它移除了,如果你的系统还在用,建议尽早停掉。
这几年的排障经历让我有一个很深的体会:TCP的三次握手和四次挥手,绝不是面试背完就扔的八股文。它们背后的状态机、超时机制、资源管理逻辑,几乎每天都在和真实业务打交道。你在抓包里看到的每一个SYN、FIN、RST,背后都是一次业务的建立、关闭或异常。把这些状态真正和线上现象对应起来,比记住任何一张概念图都有用。