TCP协议(二)状态详解
SYN_RCVD
Server端在收到Client的SYN后回复ACK,状态变更为SYN_RCVD,因此时TCP连接还未建立,这个状态称为半连接 状态。
SYN连接超时
在半连接状态下,如果一直收不到Client回复的ACK,或者Client已经下线怎么办?
为保证TCP的可靠性,Server端会重发SYN+ACK,在Linux下默认重发5次,每次重发的间隔为1s,2s,4s,8s,16s,且最后一次重发还需等待32s才能确认超时,因此SYN_RCVD的默认超时时间是63s,超时后连接断开。
SYN Flood攻击
半连接在异常情况下需要63s才能断开,恶意攻击者利用这个特性即可发起 SYN Flood攻击 ,通过伪造IP向服务器发起大量SYN后下线,服务器端无法完成三次握手只能维持半连接状态,待服务器的SYN连接队列耗尽后,就无法处理正常的连接请求。
在Linux服务器中有几个TCP内核参数可以应对这种情况。
- tcp_synack_retries 服务器端SYN+ACK的重试次数,可适当降低
- tcp_max_syn_backlog SYN队列长度,可适当增加
- tcp_abort_on_overflow 当确实遭受攻击时可以打开开关,拒绝新的连接
- tcp_syncookies 可以处理一定的攻击但不属于标准的协议实现,不推荐使用
Linux下修改TCP内核参数
上述提到了几个与tcp相关的内核参数,修改内核参数有两种方式。
- 在/proc/sys目录下每个参数都保存为一个文件,修改对应参数的文件内容即可生效,但是临时的,重启后恢复原值
# 修改SYN+ACK重试次数为3次
$ cd /proc/sys/net/ipv4
$ cat tcp_synack_retries
5
$ echo 3 > tcp_synack_retries
$ cat tcp_synack_retries
3- 修改/etc/sysctl.conf文件,并使用
sysctl -p重新加载参数,永久生效
# 查看所有内核参数
$ sysctl -a | grep tcp
# 调整SYN队列长度
$ vi /etc/sysctl.conf
新增一行:net.ipv4.tcp_max_syn_backlog = 1024
$ sysctl -p
net.ipv4.tcp_max_syn_backlog = 1024FIN_WAIT_1/FIN_WAIT_2
FIN_WAIT_1和FIN_WAIT_2都是TCP主动关闭的一方需要经历的状态,两者的区别是:
- 主动关闭的一方发出FIN数据段后进入FIN_WAIT_1,通常对方会立即响应一个ACK,状态接着变更为FIN_WAIT_2,因此保持在FIN_WAIT_1状态的时间很短
- FIN_WAIT_2状态表示要等待对方的FIN,对方需要把数据全部发送完毕后才会发送FIN,因此保持在FIN_WAIT_2状态的时间会略长(只是相对FIN_WAIT_1而言)
- Linux中有一个TCP内核参数
tcp_fin_timeout用于控制FIN_WAIT_2状态的超时时间,默认60s。
TIME_WAIT
在上一章TCP概述中提到,TIME_WAIT是个很重要的状态,也是主动关闭连接的一方必须经历的状态,需要等待2MSL后才会关闭连接。如果在大并发的短链接下,TIME_WAIT就会太多,占用大量连接数。到网上查找解决办法,通常会给出如下建议,我们来客观审视一下:
- 把tcp_fin_timeout 参数调小,目的是降低MSL,也就是降低TIME_WAIT的时间。但实际上这个参数和MSL 没有任何关系 !!!它表示的是FIN_WAIT_2状态的超时时间。
- 打开tcp_tw_reuse ,允许对TIME_WAIT状态的连接进行重用,一般会同时打开tcp_timestamps 。
- 另一个开关参数叫tcp_tw_recycle ,允许对TIME_WAIT进行快速回收。但一般情况下,这两个参数都不建议使用。因为之所以存在TIME_WAIT,是TCP协议基于可靠性的考量(上一章有详细解释),如果强行对TIME_WAIT进行回收或重用,可能会遇上一些意料之外的问题(例如前后两个连接的数据段可能会混在一起)。
- 打开长连接。通过复用TCP长连接可以避免频繁三次和四次握手,降低TIME_WAIT数量,但TCP协议本身并未对连接的时间做限制,通常长连接是在应用层进行控制。后续讲到HTTP协议时会对长连接做详细说明。(注:在Linux内核参数中有几个关于TCP长连接的配置,由于不常用,感兴趣可自行查阅相关资料)
CLOSE_WAIT
CLOSE_WAIT是TCP连接被动关闭的一方,在收到对方FIN,且自身未发出FIN时的状态,因此如果服务端存在大量的CLOSE_WAIT,一定是服务端没有关闭socket连接,至于为什么没有关闭,可能会有以下情况:
- 粗心的程序员忘记了要close socket
- 程序不够健壮,例如需要等待数据处理完毕后再关闭连接,但数据处理的性能较慢,导致迟迟无法关闭
- 异常情况导致服务器崩溃等
总之服务端如果存在大量CLOSE_WAIT,其根本原因是代码中存在bug,需要排查修复。网上说可以通过调整tcp参数来解决,事实上治标不治本。
TCP状态统计
ESTABLISHED状态将在后续章节做详细说明,SYN_SENT和LAST_ACK在概述中已有介绍,在此不做详述。
最后介绍两个netstat的“高级”用法:
查看状态的倒计时
命令:netstat -o
说明:当状态为TIME_WAIT时,后面的倒计时即为TIME_WAIT的超时时间,默认从60s开始;当状态为ESTABLISHED且Timer中有keepalive时,后面的时间即为TCP长连接倒计时,默认从7200s开始。
查看TCP状态统计
命令:netstat -nat | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
说明:可以统计服务器的每个TCP状态的连接个数,具体排查问题时可以使用grep过滤出特定端口后再统计