TCP三次握手及其四次挥手详解:内核篇

TCP状态

根据原理篇的描述,在TCP三次握手以及四次挥手时,tcp会处于不同的状态,对应netstat -tun或者ss -tun那输出的各种状态,如下:

1
2
3
4
[root@xmxyk ~]#netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
SYN_RECV 78
ESTABLISHED 1
TIME_WAIT 4

那这个是什么意思呢?这里的各种状态其实就是对应tcp的不同阶段。一般有以以下几种状态:

  • LISTEN:首先服务端需要打开一个socket进行监听,状态为LISTEN。监听来自远方TCP端口的连接请求
  • SYN_SENT:客户端通过应用程序调用connect进行active open。于是客户端tcp发送一个SYN以请求建立一个连接。之后状态置为SYN_SENT。在发送连接请求后等待匹配的连接请求
  • SYN_RECV:服务端应发出ACK确认客户端的SYN,同时自己向客户端发送一个SYN。 之后状态置为SYN_RECV。在收到和发送一个连接请求后等待对连接请求的确认
  • ESTABLISHED: 代表一个打开的连接,双方可以进行或已经在数据交互了。
  • FIN_WAIT1:主动关闭(active close)端应用程序调用close,于是其TCP发出FIN请求主动关闭连接,之后进入FIN_WAIT1状态。FIN_WAIT1只出现在主动关闭的那一端,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态
  • CLOSE_WAIT:被动关闭(passive close)端TCP接到FIN后,就发出ACK以回应FIN请求(它的接收也作为文件结束符传递给上层应用程序),并进入CLOSE_WAIT。
  • FIN_WAIT2:主动关闭端接到ACK后,等待对端发送fin包之前,就进入了FIN-WAIT-2 。
  • LAST_ACK:被动关闭端一段时间后,接收到文件结束符的应用程序将调用CLOSE关闭连接。这导致它的TCP也发送一个 FIN,等待对方的ACK。就进入了LAST-ACK。
  • TIME_WAIT:在主动关闭端接收到FIN后,TCP就发送ACK包,并进入TIME-WAIT状态。主动发起FIN包的这一方才能会有这个状态。这时需要等2倍的MSL才能改为CLOSED的在状态,目前centos的这个等待时间为60秒。
  • CLOSED: 被动关闭端在接受到ACK包后,就进入了closed的状态。连接结束。

这几个状态在原始篇的结构图上面也有讲解过,这里权当做复习使用。

内核参数

很多时候,优化web服务器的内核参数就是在优化tcp这些状态的参数,让我们来看下有哪些内核参数可以优化。

三次握手阶段参数

三次握手主要是有如下参数:

1
2
3
4
5
[root@master ~]# sysctl -a 2>/dev/null |grep ipv4.tcp |grep syn
net.ipv4.tcp_max_syn_backlog = 128
net.ipv4.tcp_syn_retries = 6
net.ipv4.tcp_synack_retries = 5
net.ipv4.tcp_syncookies = 1
  • tcp_max_syn_backlog表示指定所能接受SYN包的最大客户端数量,即半连接上限,在第一步server收到client的syn后,把这个连接信息放到半连接队列中。
  • tcp_syn_retries 表示建立 TCP 连接时 SYN 报文重试的次数
  • tcp_synack_retries表示重传SYN+ACK的次数,就是在三次握手的第二步。这些就有一些恶意的人就会发起SYN Flood攻击,给服务器发了一个SYN后,就下线了,于是服务器需要重试5次,默认要等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。
  • tcp_syncookies当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。
  • tcp_abort_on_overflow 为0表示如果三次握手第三步的时候全连接队列满了那么server扔掉client 发过来的ack,但是server过一段时间再次发送syn+ack给client;为1表示第三步的时候如果全连接队列满了,server发送一个reset包给client,表示废掉这个握手过程和这个连接。
  • /proc/sys/net/core/somaxconn表示全连接队列,握手时第三步的时候server收到client的ack,如果这时全连接队列没满,那么从半连接队列拿出这个连接的信息放入到全连接队列中,否则按tcp_abort_on_overflow指示的执行。

补充几点:

  1. /proc/sys/net/ipv4/tcp_max_syn_backlog表示半连接的队列;/proc/sys/net/core/somaxconn是全连接的队列;2者的区别如下图所示,当握手成功之后,会从半连接转成全连接队列。

img

  1. tcp_max_syn_backlog以及somaxconn都是系统内核管理的,半连接队列syns queue的大小取决于:max(64, tcp_max_syn_backlog);而全连接队列accept queue的大小取决于:min(backlog, somaxconn)这里的backlog是在socket创建的时候传入的,简单一点说,就是应用程序创建的,跟tcp_max_syn_backlog半毛线的关系都没有。可以使用ss -lt来查看应用程序的全连接,第二列Send-Q 值表示第三列的listen端口上的全连接队列最大值,第一列Recv-Q为全连接队列当前使用了多少。
1
2
3
4
5
6
[root@master ~]# ss -lt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:ssh *:*
LISTEN 0 100 127.0.0.1:smtp *:*
LISTEN 0 128 :::ssh :::*
LISTEN 0 100 ::1:smtp :::*

也可以使用netstat -s | grep -i listen来查看队列的溢出统计数据,如果出现xxx times the listen queue of a socket overflowed表示全连接队列溢出次:

1
2
3
[root@server ~]# netstat -s | egrep "listen|LISTEN" 
1641685 times the listen queue of a socket overflowed
1641685 SYNs to LISTEN sockets ignored

有时候,overflowed和ignored会出现一样多的情况,并且都是同步增加,overflowed表示全连接队列溢出次数,socket ignored表示半连接队列溢出次数。这是由于overflow的时候一定会drop++(socket ignored),也就是drop一定大于等于overflow。

也可以使用如下方法查看:

1
2
3
[root@xmxyk ~]# netstat -s |egrep "TCPBacklogDrop|SYN.*dropped"
35 SYNs to LISTEN sockets dropped //未进入syns queue的数据包数量
TCPBacklogDrop: 1 //未进入accept queue的数据包数量

netstat也是可以看到Recv-Q以及Send-Q的,但是这跟ss的意思完全不一样,这里的Recv-Q就是指收到的数据还在缓存中,还没被进程读取,这个值就是还没被进程读取的 bytes;而 Send 则是发送队列中没有被远程主机确认的 bytes 数。

1
2
3
4
[root@master ~]# netstat -tn
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 52 192.168.1.60:22 192.168.1.1:57969 ESTABLISHED
  1. 当受到半连接的攻击时,官方很良心地建议去修改三个参数:第一个是:tcp_synack_retries 可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大SYN连接数;第三个是:tcp_abort_on_overflow 设置为1,直接rst拒绝连接。不建议去开启tcp_syncookies,因为他严重违反 TCP 协议,如果开启,那tcp_max_syn_backlog的参数失效

  2. 如果client走完第三步在client看来连接已经建立好了,但是server上的对应连接实际没有准备好,这个时候如果client发数据给server,那server是不会给回复的,同时server会重新发SYN+ACK包,但是client认为连接已建立好了,所以不会发ack,就是重发首包的数据而也。后面超时之后client就直接FIN包了。

    img

四次挥手阶段参数

FIN-WAIT-1过多

1
2
[root@master ~]# sysctl -a 2>/dev/null |egrep tcp_fin_timeout
net.ipv4.tcp_fin_timeout = 60

出现fin-wait-1状态是本地自己发了fin包,其centos下默认的超时时间是60秒。如果系统上面出现了大量的fin-wait-1,可以先分析一下原因,如果找不出问题,就可以尝将tcp_fin_timeout值改小。

TIME_WAIT过多

如果在大并发的短链接下,TIME_WAIT 就会太多,这也会消耗很多系统资源。一般网上大部分的文章都是开启以下2个参数:

  • tcp_tw_reuse:设置为 1 时,就允许系统重用处于 TIME_WAIT 状态的 socket。如果 tcp_timestamp 没有设置为 1,只把 tcp_tw_reuse 设置为 1 是无效的。
  • tcp_tw_recycle:设置为 1 会开启系统对 TIME_WAIT 状态的 socket 的快速回收。开启这个功能,系统就会存下 TCP 连接的时间戳,当同一个 IP 地址过来的包的时间戳小于缓存的时间戳,系统就直接丢包,“回收”这个 socket。这个选项同样需要开启 tcp_timestamp 才生效。
1
2
3
[root@master ~]# sysctl -a 2>/dev/null |grep tcp_tw
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_tw_reuse = 0

默认情况下是不会开启的,虽然网上大部分的文章在time_wait过多的情况下开启这二个选项,但是开启这个功能是有很大风险的,如前面所说,会根据同一个 IP 来的包得时间戳来判断是否丢包,而时间戳是根据发包的客户端的系统时间得来的,如果服务端收到的包是同一出口 IP 而系统时间不一样的两个客户端的包,就有可能会丢包,可能出现的情况就是一个局域网内有的客户端能连接服务端,有的不能。这在nat环境下就会有很多问题。所以建议修改如下2个内核参数:

1
2
3
[root@master ~]# sysctl -a 2>/dev/null |egrep "tcp_max_tw_buckets|ip_local_port_range"
net.ipv4.ip_local_port_range = 32768 60999
net.ipv4.tcp_max_tw_buckets = 8192
  • ip_local_port_range:可用端口范围,默认情况下,其端口范围是32768~60999
  • tcp_max_tw_buckets:表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。当TIME_WAIT太多时,通过dmsg可以看到,会出现TCP: time wait bucket table overflow这样的错误。

TCP keepalive

出现time_wait的原因很大的一部分原因是使用了短连接的方式,可以使用长连接的方式来处理。

因为 TCP 的上层调用是 Socket,客户端和服务端都会启动 Socket。如果客户端关闭了 Socket,而服务端不知道,一直会为客户端保持着连接,这样是很浪费资源的。为了解决这个问题,TCP协议规定,当超过一段时间之后,TCP自动发送一个数据为空的报文给对方,如果对方回应了这个报文,说明对方还在线,连接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为连接丢失,应该关闭连接。

Linux 内核包含了对 keepalive 的支持,使用下面三个参数:

1
2
3
4
5
6
7
8
9
10
11
# 表示TCP链接在多少秒之后没有数据报文传输时启动探测报文(发送空的报文)
cat /proc/sys/net/ipv4/tcp_keepalive_time
7200

# 表示前一个探测报文和后一个探测报文之间的时间间隔
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75

# 表示探测的次数
cat /proc/sys/net/ipv4/tcp_keepalive_probes
9

以上是tcp层的keepalive的三个参数,还有http层的,有兴趣的朋友 ,可以查看:https://kiswo.com/article/1018

TCP缓冲参数

1
2
3
4
[root@master ~]# sysctl -a 2>/dev/null |egrep tcp_.*mem
net.ipv4.tcp_mem = 22959 30614 45918
net.ipv4.tcp_rmem = 4096 87380 6291456
net.ipv4.tcp_wmem = 4096 16384 4194304
  • tcp_rmem:TCP读取缓冲区,可以理解为收到的数据包缓存,单位为字节,默认值为87380 byte ≈ 86K,最小为4096 byte=4K,最大值为4064K。
  • tcp_wmem:TCP发送缓冲区,默认情况下,三个数值是跟tcp_rmem一模一样的。
  • tcp_mem:表示TCP的内存大小,其单位是页,1页等于4096字节。其三个值表示如下:
    • low:当TCP使用了低于该值的内存页面数时,TCP不会考虑释放内存。4096*4K=16M,即TCP低于16M是不会释放内存的。
    • pressure:当TCP使用了超过该值的内存页面数量时,TCP试图稳定其内存使用,进入pressure模式,当内存消耗低于low值时则退出pressure状态。16384*4k=64M
    • high:允许所有tcp sockets用于排队缓冲数据报的页面量,当内存占用超过此值,系统拒绝分配socket,后台日志输出“TCP: too many of orphaned sockets”。4194304*4K=16G

还有一些其他的一些内核参数如下:

img

文件数参数

文件数设置

linux下,一切皆文件,这个很多运维只理解了一半,很多时候都没有完全理解透。如一个已经建立好的socket连接,这其实也是一个文件。默认情况下,使用ulimit -n可以看到同时打开文件数量是1024个,这意味着只能建立1024个连接,这是不够用的。centos要修改这个值就需要修改2个地方:

  1. 用户级别限制ulimit

    可以通过ulimit -SHn 1048576 命令来修改该限制,但这个变更只对当前的session有效,当断开连接重新连接后更改就失效了。要永久保存的话,就需要在编辑/etc/security/limits.conf文件添加:

    1
    2
    * soft nofile 1048576
    * hard nofile 1048576
    • soft是一个警告值,而hard则是一个真正意义的阀值,超过就会报错。
    • soft 指的是当前系统生效的设置值。hard 表明系统中所能设定的最大值
    • nofile 打开文件的最大数目
    • 星号表示针对所有用户,若仅针对某个用户登录ID,请替换星号

    这只是修改用户级的最大文件描述符限制,也就是说每一个用户登录后执行的程序占用文件描述符的总数不能超过这个限制。

  2. 系统级的限制

    1
    2
    3
    4
    [root@master ~]# sysctl -a 2>/dev/null|grep fs.file
    fs.file-max = 201948
    fs.file-nr = 896 0 201948
    fs.xfs.filestream_centisecs = 3000
    • fs.file-max:限制所有用户打开文件描述符的总和,一般情况下系统内核会设置为内存总量的10%左右。
    • file-nr:第一个数表示表示自引导以来分配的文件描述符的数量;第二个数表示已使用文件句柄的数目;第三个数表示文件句柄的最大数目

最大连接数的问题

我们知道一个tcp端口的一共有65536个,所以我们对外的说法,一台服务器上面的最大连接数就只有65535个,如果机器上面有2个IP,那连接数就是65535*2wh 。其实这个观点算是错误的。如果是正常业务的服务器,如web服务器,这个最大连接数跟上一小节说的文件数有关系;如果是代理服务器,那就是说法是正确的,因为代理服务器是需要回源的。以下说明一下client以及正常业务的服务器的最大连接数的问题。

系统用一个4元组来唯一标识一个TCP连接:{local ip, local port,remote ip,remote port}。client每次发起tcp连接请求时,除非绑定端口,通常会让系统选取一个空闲的本地端口(local port),该端口是独占的,不能和其他tcp连接共享。tcp端口的数据类型是unsigned short(2字节),因此本地端口个数最大只有65536,端口0有特殊含义,不能使用,这样可用端口最多只有65535,所以在全部作为client端的情况下,最大tcp连接数为65535,这些连接可以连到不同的server ip

server通常固定在某个本地端口上监听,等待client的连接请求。这样子remote port就是固定了,不能修改了。因此最大tcp连接为客户端ip数×客户端port数,对IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数)。为什么是这么算呢?请参考数据包的结构图。但是实际上是达不到这么多的连接的。因为每个tcp连接都要占用一定内存,每个socket就是一个文件描述符,在默认2.6内核配置下,经过试验,每个socket占用内存在15~20k之间。请参考:https://blog.csdn.net/lanyang123456/article/details/79834998

其他参数

  • tcp_max_orphans:系统所能处理不属于任何进程的TCP sockets最大数量,默认为8192。假如超过这个数量,那么不属于任何进程的连接会被立即reset,并同时显示警告信息。之所以要设定这个限制﹐纯粹为了抵御那些简单的 DoS 攻击﹐千万不要依赖这个或是人为的降低这个限制,更应该增加这个值(如果增加了内存之后)。
  • nf_conntrack_max:系统默认值为65536,用来追踪本机上的请求和响应之间的关系,有NEW、ESTABLISHED、RELATED以及INVALID。当nf_conntrack模块被装置且服务器上连接超过这个设定的值时,系统会主动丢掉新连接包,直到连接小于此设置值才会恢复。当系统出现nf_conntrack: table full, dropping packet说明系统已经出现异常或者负载过大的问题了,详细说明请参考:https://clodfisher.github.io/2018/09/nf_conntrack/
  • 其他参数可以参考https://blog.csdn.net/weixin_33698823/article/details/91767090

参考资料

TCP SOCKET中backlog参数的用途是什么?http://www.cnxct.com/something-about-phpfpm-s-backlog/

TCP 三次握手原理,你真的理解吗?http://jm.taobao.org/2017/05/25/525-1/

对 Linux TCP 的若干疑点和误会:https://fixatom.com/some-doubts-and-misunderstandings-of-tcp/

100万并发连接服务器笔记之1M并发连接目标达成:http://www.blogjava.net/yongboy/archive/2013/04/11/397677.html

linux中最大文件描述符数: https://leokongwq.github.io/2016/11/09/linux-max-fd.html

linux调优:https://xulizhao.com/blog/linux-tuning/

Linux 常用内核网络参数与相关问题处理:https://help.aliyun.com/knowledge_detail/52868.html

0%