从TCP/IP协议谈Linux内核参数优化
在硬件资源有限的情况下,最大的压榨服务器性能,提高服务器的并发处理能力,是很多技术人员思考的问题,除了优化Nginx/PHP-FPM/Mysql/Redis这类服务软件配置外,还可以通过修改Linux的内核相关TCP参数,来最大的提高服务器性能。
在Linux内核参数优化之前,我们需要先搞懂TCP/IP协议,这是我们实施优化的理论依据。
TCP/IP协议
TCP/IP协议是十分复杂的协议,完全掌握不是一件容易的事情,但作为基本知识,我们必须知道TCP/IP协的三次握手和四次挥手的逻辑过程。
三次握手
所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发。
三次握手流程图:
三次握手流程
- 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
- 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
- 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
四次挥手
四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。
四次挥手的流程图:
四次挥手流程
- 中断连接端可以是客户端,也可以是服务器端。
- 第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说”我客户端没有数据要发给你了”,但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
- 第二次挥手:服务器端收到FIN后,先发送ack=M+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2状态,继续等待服务器端的FIN报文。
- 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
- 第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手。
序列号与确认应答
大家都知道TCP/IP协议是以一种高可靠的通信协议,通过序列号与确认应答来保障通信高可靠,有如下几个关键点:
- 当发送端的数据到达接收主机时,接收端主机会返回一个已收到消息的通知。这个消息叫做确认应答(ACK)。当发送端将数据发出之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。反之,则数据丢失的可能性很大。
- 在一定时间内没有等待到确认应答,发送端就可以认为数据已经丢失,并进行重发。由此,即使产生了丢包,仍然能够保证数据能够到达对端,实现可靠传输。
- 未收到确认应答并不意味着数据一定丢失。也有可能是数据对方已经收到,只是返回的确认应答在途中丢失。这种情况也会导致发送端误以为数据没有到达目的地而重发数据。
- 此外,也有可能因为一些其他原因导致确认应答延迟到达,在源主机重发数据以后才到达的情况也屡见不鲜。此时,源主机只要按照机制重发数据即可。
- 对于目标主机来说,反复收到相同的数据是不可取的。为了对上层应用提供可靠的传输,目标主机必须放弃重复的数据包。为此我们引入了序列号。
- 序列号是按照顺序给发送数据的每一个字节(8位字节)都标上号码的编号。接收端查询接收数据 TCP 首部中的序列号和数据的长度,将自己下一步应该接收的序列号作为确认应答返送回去。通过序列号和确认应答号,TCP 能够识别是否已经接收数据,又能够判断是否需要接收,从而实现可靠传输。
- 重发超时是指在重发数据之前,等待确认应答到来的那个特定时间间隔。如果超过这个时间仍未收到确认应答,发送端将进行数据重发。最理想的是,找到一个最小时间,它能保证“确认应答一定能在这个时间内返回”。
- TCP 要求不论处在何种网络环境下都要提供高性能通信,并且无论网络拥堵情况发生何种变化,都必须保持这一特性。为此,它在每次发包时都会计算往返时间及其偏差。将这个往返时间和偏差时间相加,重发超时的时间就是比这个总和要稍大一点的值。
- 数据被重发之后若还是收不到确认应答,则进行再次发送。此时,等待确认应答的时间将会以2倍、4倍的指数函数延长。
- 此外,数据也不会被无限、反复地重发。达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接。并且通知应用通信异常强行终止。
TCP/IP协议缺陷
了解了TCP/IP协议之后,我们就会发现几个问题:
在三次握手中,如果客户端发起第一次握手后就中断或者不响应服务器发回的ACK=1数据包,那服务器就会不断的重试发送数据包,直到超时。 没错,这就是SYN FLOOD攻击原理。
在四次挥手中,主动关闭连接的客户端处在TIME_WAIT状态后,会一直持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失(TIME_WAIT状态一般维持在1-4分钟)。通过2MSL时间长度来确保旧的连接状态不会对新连接产生影响。处于TIME_WAIT状态的连接占用的资源不会被内核释放,所以作为服务器,在可能的情 况下,尽量不要主动断开连接,以减少TIME_WAIT状态造成的资源浪费。如果我们的服务器是负载均衡服务器,上游服务器长时间没有影响,负载均衡服务器将主动关闭链接,高并发场景下将导致TIME_WAIT状态的累积。
在四次挥手中,如果客户端在收到FIN 报文后,应用没有返回 ACK,服务端同样会不断尝试发送FIN报文,这样服务端就会出现CLOSE_WAIT状态的累积。
SYN Flood攻击
Syn Flood攻击是当前网络上最为常见的DDoS攻击,也是最为经典的拒绝服务攻击,它利用了TCP协议实现上的一个缺陷,通过向网络服务所在端口发送大量的伪造源地址的攻击报文,就可能造成目标服务器中的半开连接队列被占满,从而阻止其他合法用户进行访问。
Syn Flood攻击原理
攻击者首先伪造地址对服务器发起SYN请求(我可以建立连接吗?),服务器就会回应一个ACK+SYN(可以+请确认)。而真实的IP会认为,我没有发送请求,不作回应。服务器没有收到回应,会重试3-5次并且等待一个SYN Time(一般30秒-2分钟)后,丢弃这个连接。
如果攻击者大量发送这种伪造源地址的SYN请求,服务器端将会消耗非常多的资源来处理这种半连接,保存遍历会消耗非常多的CPU时间和内存,何况还要不断对这个列表中的IP进行SYN+ACK的重试。TCP是可靠协议,这时就会重传报文,默认重试次数为5次,重试的间隔时间从1s开始每次都番倍,分别为1s + 2s + 4s + 8s +16s = 31s,第5次发出后还要等32s才知道第5次也超时了,所以一共是31 + 32 = 63s。
一段假的syn报文,会占用TCP准备队列63s之久,而半连接队列默认为1024,在没有任何防护的情况下,每秒发送20个伪造syn包,就足够撑爆半连接队列,从而使真正的连接无法建立,无法响应正常请求。 最后的结果是服务器无暇理睬正常的连接请求—拒绝服务。
内核TCP参数优化
编辑文件/etc/sysctl.conf,加入以下内容:
- net.ipv4.tcp_fin_timeout = 2
- net.ipv4.tcp_tw_reuse = 1
- net.ipv4.tcp_tw_recycle = 1
- net.ipv4.tcp_syncookies = 1
- net.ipv4.tcp_keepalive_time = 600
- net.ipv4.ip_local_port_range = 4000 65000
- net.ipv4.tcp_max_syn_backlog = 16384
- net.ipv4.tcp_max_tw_buckets = 36000
- net.ipv4.route.gc_timeout = 100
- net.ipv4.tcp_syn_retries = 1
- net.ipv4.tcp_synack_retries = 1
- net.core.somaxconn = 16384
- net.core.netdev_max_backlog = 16384
- net.ipv4.tcp_max_orphans = 16384