当我们在浏览器上点开一个网页时,我们的客户端就会和服务器建立一个socket连接。Soket是什么,什么是Socket 五元组,常见的TCP优化参数的依据又是什么,这里我们将来分析一下。


Socket 五元组

我们通常所说的五元组就是我们熟悉的 源IP,源端口,目的IP,目的端口和协议组成。以http协议为例,当我们访问网站时,使用三层协议的是TCP协议,源IP为本地电脑的IP,源端口为随机端口,目的IP为网站所在服务器IP,目的端口为80。

下图是TCP数据的封装结构:

wKioL1mmQ7mh20S7AAC0TV0IFN0830.jpg


tcp的源端口和目的端口都占用16bit,也就是2的16次方,这样一共可用的端口就是65536个,端口号是从0开始,而且0是保留端口(使用0端口分配一个随机端口),实际可用的端口范围是1-65535,Unix系统中的保留端口是0-1024,在1024以内的端口都需要超级用户特权才能启用。


优化端口范围

在我们的系统中,有很多保留的端口,如http的80,ftp的20,21,ssh的22等等,这些端口都是特定协议的特定端口。当我们从本机访问服务器时,系统会随机分配一个端口,在linux系统中这个随机端口的范围默认是 32768到61000,可以通过如下命令查看:

# cat /proc/sys/net/ipv4/ip_local_port_range
32768 61000

当然,我们可以通过优化主机的端口范围来提高并发系统的并发能力,这个一般配置在我们的代理服务器和需要向其他主机发起请求的主机上,如web服务器等。在指定端口范围的时候,我们需要将10000以下的端口保留,很多服务默认会使用10000以内的端口。

# echo "10000 65535" > /proc/sys/net/ipv4/ip_local_port_range

 由于Linux系统中,一切皆文件,在系统发起一个socket连接的时候会自动创建一个socket文件,如果同时有很多个socket请求,本地的Openfile数量就会是一个瓶颈,下面我们看一下Linux系统默认的open-file参数:

# ulimit -n
1024

默认的open files数量远远低于我们的随机端口数量,我们需要修改open files参数:

 vim /etc/security/limits.conf
*  soft nofile  50000   # soft表示为默认的限制范围,可以超过,但是最大不能超过hard中的配置
*  hard nofile  60000   # hard表示不能超过此范围

这里指定了所有用户,如果是对特定用户或者用户组可以将'*'替换为用户名。

提示:使用ulimit -n 60000这条命令,只是在当前的shell生效。普通用户使用此命令不能超过hard nofile中的设定值,且使用之后,只能缩小,不能增大。root用户可任意设置,不受限制。


由于系统支持的端口范围有限,所以在有些高并发的场景,即使优化了端口范围也会是一个瓶颈,所以在这种场景下,一般使用多IP的方式来实现。


TCP三次握手四次挥手

这里既然提到了TCP协议,就不得不说下TCP的三次握手和四次挥手。附图:

wKioL1mmX_jQ6gteAAFEP8DIAV0016.jpg

在描述这个流程图前,结合上面的TCP报头结构,可以更加容易理解这个过程:

wKioL1mmc12j3gToAABMhns7_r8126.jpg


简单的描述下这个流程图(结合上面的三张图理解):


三次握手

1、在起始点服务端和客户端都是无应用状态,没有TCP连接.

2、客户端有应用需要发起TCP连接,此时向服务端发送SYN的TCP连接请求,并将自身状态转换为SYN_SENT。此处的SYN信息保存在TCP报头的32位序列号中(图3,SEQ No),Code Bits区域的SYN位设置为1(图1)。

3、服务端服务启动后,进入LISTEN状态,当服务端收到客户端发来的SYN后,立即回复一个新的SYN序列号,同时带上ACK,ACK也是一个32bits的序列号(图3,ACK No),并且是在客户端发送的SYN序列号上+1,Code Bits区域的SYN位和ACK位都设置为1。服务端进入SYN_RCVD状态。

4、客户端接收到服务端发送的SYN,ACK报之后,会回复服务端一个ACK,此时ACK标识位置为1,客户端进入ESTABLISHED状态。

5、服务端接受ACK后进入ESTABLISHED状态。


四次挥手

1、当要断开连接时(我们将发起断开请求的一端称作客户端),客户端会向服务端发送一个FIN,然后自身转变为FIN_WAIT_1状态。

2、服务端接受FIN后,返回一个ACK,然后进入CLOSE_WAIT状态。

3、客户端接收到服务端的ACK后,进入FIN_WAIT_2状态。

4、服务端完成数据传输后,会向客户端发送一个FIN,然后进入LAST_ACK。

5、客户端接收FIN后,向服务端发送ACK,进入TIME_WAIT状态。

6、服务端接受ACK后关闭进程,进入close状态。


通过nc 命令,使用本机telnet 观察整个过程

1、主机A启用端口8888,并使用tcpdump 监听此端口

[root@zabbixsvr ~]# nc -l -4 -p 8888 -k
[root@zabbixsvr ~]# tcpdump  -i ens3 host 192.168.1.17 and  port 8888

2、本地主机B,IP为192.168.1.17,使用telnet远程访问此端口:

C:\Users\admin>telnet 192.168.1.100 8888

      此时主机上的TCP状态为ESTABLISHED:

[root@zabbixsvr ~]# netstat -alntpu|grep 8888
tcp        0      0 0.0.0.0:8888            0.0.0.0:*               LISTEN      5019/nc             
tcp        0      0 192.168.1.100:8888     192.168.1.17:63633    ESTABLISHED 5019/nc

3、之后主机A主动断开连接,此时主机A上的TCP状态为TIME_WAIT:

[root@zabbixsvr ~]# netstat -alntpu|grep 8888
tcp        0      0 192.168.1.100:8888     192.168.1.17:63633    TIME_WAIT   -

     TIME_WAIT状态会持续60s的时间。

4、整个过程由于没有数据的发送,所以tcpdump捕获到的信息刚好是一个tcp 三次握手和四次挥手过程:

11:06:39.833871 IP 192.168.1.17.63633 > zabbixsvr.ddi-tcp-1: Flags [S], seq 1288094251, win 8192, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
11:06:39.833978 IP zabbixsvr.ddi-tcp-1 > 192.168.1.17.63633: Flags [S.], seq 204052783, ack 1288094252, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
11:06:39.834352 IP 192.168.1.17.63633 > zabbixsvr.ddi-tcp-1: Flags [.], ack 1, win 256, length 0
11:07:03.335179 IP zabbixsvr.ddi-tcp-1 > 192.168.1.17.63633: Flags [F.], seq 1, ack 1, win 229, length 0
11:07:03.335702 IP 192.168.1.17.63633 > zabbixsvr.ddi-tcp-1: Flags [.], ack 2, win 256, length 0
11:07:03.337265 IP 192.168.1.17.63633 > zabbixsvr.ddi-tcp-1: Flags [F.], seq 1, ack 2, win 256, length 0
11:07:03.337347 IP zabbixsvr.ddi-tcp-1 > 192.168.1.17.63633: Flags [.], ack 2, win 229, length 0


TCP连接优化

由上面的分析,我们可以得出一个结论:time-wait会发生在发起断开请求的一端。一般这样的服务器会是我们的代理节点或web服务器,因为他们需要作为客户端向后端节点发起访问。而系统进入time-wait后会等待60s的时间,在这段时间内,系统不会释放socket,仍然会占据端口,这样对于高并发的业务非常不利。

对此我们就需要对处于time-wait状态的socket进行快速回收。

time-wait调优:减少time-wait

优化参数:

/proc/sys/net/ipv4/tcp_tw_reuse   # 尽量复用连接
/proc/sys/net/ipv4/tcp_timestamps  # 开启时间戳,默认是开开启状态。
/proc/sys/net/ipv4/tcp_tw_recycle  # time-wait快速回收,开启必须开启tcp_timestamps。 使用NAT网络环境下不能开启,会造成SYN数据包被丢弃,
负载均衡上不能开,否则会出现收不到数据包的情况。

提示:如果关闭tcp_timestamp, tcp_tw_recycle则不能开启。


参数说明:

默认情况下,当tcp_tw_reuse和tcp_tw_recycle都被禁用时,内核将确保在TIME_WAIT状态下的套接字将保持在足够长的时间,这足以确保属于未来连接的数据包不会被误认为是旧的连接。

tcp_tw_reuse:

当启用tcp_tw_reuse时,在TIME_WAIT状态下的套接字可以在它们过期之前使用,并且内核将尝试确保没有关于TCP序列号的冲突。如果启用tcp_timestamps(a.k.a. PAWS,防止包装序列号),它将确保这些冲突不会发生。但是,需要在服务器两端都启用TCP时间戳。

tcp_tw_recycle:

启用tcp_tw_recycle时,内核变得更加积极回收time wait,并且将对远程主机使用的时间戳作出假设判断。它将跟踪具有TIME_WAIT状态的连接的每个远程主机使用的最后时间戳),并允许在时间戳正确增加的情况下重新使用套接字。但是,如果主机使用的时间戳发生变化(即时间回退),则SYN数据包将被静默地丢弃,并且连接不会建立(您将看到类似于“连接超时”的错误)。

在开启tcp_tw_recycle连接中间涉及网络地址转换(或智能防火墙)的场景就会出现这种情况:

在这种情况下,拥有相同IP地址后面有多个主机,因此,不同的时间戳序列(或者所有时间戳在防火墙的每个连接处被随机化)。在这种情况下,某些主机将无法连接,因为它们映射到服务器的TIME_WAIT桶具有较新的时间戳的端口。这就是为什么有些文档告诉你“NAT设备或负载均衡器可能由于设置而启动丢帧”。

这里建议不开启tcp_tw_recycle,但是启用tcp_tw_reuse并降低tcp_timewait_len。


除此之外我们还可以使用长连接的方式,减少time-wait。

长连接的优缺点:

1、长连接会长时间占用socket。

2、长连接会加快访问速度。


Nginx TCP配置优化

在nginx的配置文件中有相关TCP连接的优化,分别是sendfile,tcp_nopush和tcp_nodelay。

sendfile on|off;  # 是否启用文件快速传输
tcp_nopush on|off; # 是否缓存数据后集中发送,适用于大文件传输  
tcp_nodelay on|off; # 是否立即发送数据包,使用于即时性传输

参数说明:

当使用sendfile函数时,tcp_nopush才起作用,它和指令tcp_nodelay是互斥的。

为了避免网络拥塞,使用TCP协议发送数据时,有等待数据达到MSS值之后再发生数据的机制,因此不会发送太小的数据包。 这种机制由Nagle的算法保证,其算法逻辑是这样:

if there is new data to send
  if the window size >= MSS and available data is >= MSS
    send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
      enqueue data in the buffer until an acknowledge is received
    else
      send data immediately
    end if
  end if
end if

该算法与TCP延迟确认机制(TCP delayed acknowledgment)在20世纪80年代早期引入到TCP中。启用这两种算法后,应用程序对TCP连接进行两次连续写入,其次是在第二次写入的数据到达目的地之后将不会立即发送,在另一端会经历长达500毫秒的恒定延迟,就是所谓的“ ACK延迟(delay ACK默认会延迟40ms回复,其作用也是给发送数据的一端更多的时间来缓冲更多的数据)“。 因此,TCP实现通常为应用程序提供禁用Nagle算法的接口。 这个就是TCP_NODELAY选项。


tcp_nodelay :  

TCP_NODELAY选项允许绕过Naggle算法,然后尽快发送数据。当下载完整的网页时,TCP_NODELAY可以在每个HTTP请求上节省更多的时间,这样可以有更佳的用户体验。 在在线游戏或高频交易时的场景,即使以相对网络饱和的代价,摆脱延迟至关重要。

Nginx在HTTP keepalive连接上使用TCP_NODELAY。 keepalive连接是在发送数据后保持打开几次的套接字。 keepalive允许发送更多的数据,而不会启动新的连接,并重复每次HTTP请求的TCP 3次握手方式。 这样可以节省重新创建socket的时间,因为每次数据传输后都不会切换到FIN_WAIT。 Keep-alive是HTTP 1.0和HTTP 1.1默认行为的一个选项。


tcp_nopush : 

在Nginx上,配置选项tcp_nopush与tcp_nodelay相反。 不是优化延迟,但是它可以优化一次发送的数据量,和Nagle算法非常接近。

TCP_NOPUSH会调用tcp_cork函数,tcp_cork函数会阻塞数据,直到数据包到达MSS的长度,MSS长度等于MTU减去IP数据包的40(IPV4)或60(IPV6)字节,和Nagle算法不同的是,TCP_CORK软件将等待的时间上限设置为200毫秒,而不是等待上个数据包的ACK。 如果达到上限,则排队的数据将自动传输。

TCP(7)联机帮助页解释说TCP_NODELAY和TCP_CORK是互斥的,但是Linux 2.5.9以后的版本中两者可以兼容。

在Nginx的配置中,sendfile和tcp_nopush必须配合使用。


sendfile :

Nginx之所以在静态文件的传输上有很高的性能,主要原因就在于这里所说的三个参数。

 Nginx的sendfile选项可以使用sendfile(2)来处理与发送文件相关的所有内容。

sendfile(2)允许在文件描述符中直接在内核空间中传输数据,节省大量资源:

  • sendfile是一个系统调用,这意味着在内核空间内执行,因此没有昂贵的上下文切换。

  • 替换了read和write两者的组合。

  • 允许零拷贝,这意味着通过DMA从块设备内存直接写入内核缓冲区。

如果nginx是在为本地存储的静态文件提供服务,则sendfile对于加快Web服务器响应至关重要。 但是,如果使用Nginx作为反向代理来从应用程序服务器提供页面,则可以禁用它。 除非在tmpfs上提供微型缓存。 


三个参数同时开启:

之前提到,tcp_nodelay和tcp_nopush 是一对互斥的参数,那么这里同时开启会带来什么效果呢?

当开启sendfile,tcp_nopush时,可以确保在发送到客户端之前数据包已经充分“填满”, 这大大减少了网络开销,并加快了文件发送的速度。 然后,当它到达最后一个可能因为没有“填满”而暂停的数据包时,Nginx会忽略tcp_nopush参数, 然后,tcp_nodelay强制套接字发送数据,每个文件节200ms的时间。

这个运行过程可以从TCP_CORK相关的TCP stack 源码中得到确认  a comment from the TCP stack source,引用源码注释中的原话:

/* When set indicates to always queue non-full frames.
 * Later the user clears this option and we transmit
 * any pending partial frames in the queue.  This is
 * meant to be used alongside sendfile() to get properly
 * filled frames when the user (for example) must write
 * out headers with a write() call first and then use
 * sendfile to send out the data parts.
 *
 * TCP_CORK can be set together with TCP_NODELAY and it is
 * stronger than TCP_NODELAY.
 */


由此可知,TCP_CORK可以与TCP_NODELAY一起设置,它比单独配置TCP_NODELAY具有更强的性能。


参考链接:

https://www.unixhot.com/article/65 

https://stackoverflow.com/questions/8893888/dropping-of-connections-with-tcp-tw-recycle 

http://www.cnblogs.com/lulu/p/4149312.html 

https://en.wikipedia.org/wiki/Nagle%27s_algorithm 

https://t37.net/nginx-optimization-understanding-sendfile-tcp_nodelay-and-tcp_nopush.html