当前位置: 首页 > Linux

TCP半连接队列和全连接队列满了怎么办?我们应该如何处理呢?

时间:2023-04-06 19:07:07 Linux

日常英语,每天进步一点:前言网上很多博客都是针对增加TCP半连接队列和全连接队列如下:增加TCP半连接队列的方法是增加/proc/系统/网络/ipv4/tcp_max_syn_backlog;增加TCP全连接队列的方法是增加listen()函数中的backlog;告诉你,以上方法都不准确。“你怎么知道它不准确?”很简单,因为我做过实验,看了TCP协议栈的内核源码,发现要增加这两个队列的长度,并不是简单的增加某个参数的问题。接下来,我们将以实战+源码分析的方式,为大家揭秘TCP半连接队列和全连接队列的解密方法。“源码分析,这不是说服力吗?搞Java的我们看不懂。”不用担心,本文的源码分析不会涉及深奥的知识,因为我已经删掉了,你只需要知道条件判断语句if,左移右移运算符等基本语法你就能看懂,加减。此外,不仅会介绍源码分析,还会介绍排查半连接队列和全连接队列的Linux命令。“哦?好像很有意思,我来看看吧!”好吧,没有被劝走的朋友值得鼓励。下图是这篇文章的提纲:什么是TCP半连接队列和全连接队列?在TCP三次握手过程中,Linux内核会维护两个队列,即:半连接队列,也叫SYN队列;全连接队列,也叫acceptpet队列;服务端收到客户端发起的SYN请求后,内核会将连接存储在半连接队列中,并向客户端响应SYN+ACK,然后客户端返回ACK。服务器收到第三次握手的ACK后,内核会将该连接从半连接队列中移除,然后创建一个新的完整连接加入accept队列,进程调用accept时取出连接功能。无论是半连接队列还是全连接队列,都有一个最大长度限制。当超过限制时,内核会直接丢弃或返回RST数据包。实战-TCP全连接队列溢出如何知道应用的TCP全连接队列大小?可以在服务器端使用ss命令查看TCP全连接队列的状态:但需要注意的是,ss命令得到的Recv-Q/Send-Q在“LISTEN状态”和“非LISTEN状态”的。从下面的内核代码可以看出区别:在“LISTEN状态”下,Recv-Q/Send-Q的含义如下:Recv-Q:当前全连接队列的大小,即当前三向握手已经完成,等待服务端的TCP连接accept();Send-Q:当前全连接的最大队列长度。以上输出显示TCP服务监听8088端口,最大全连接长度为128;在“非LISTEN状态”下,Recv-Q/Send-Q的含义如下:Recv-Q:应用进程收到但未读取的字节数;send-Q:发送但未收到确认的字节数;如何模拟TCP全连接队列溢出场景?实验环境:客户端和服务器都是CentOs6.5,Linux内核版本2.6.32服务器IP192.168.3.200,客户端IP192.168.3.100服务器是Nginx服务,端口是8088这里先介绍wrk工具,它是一个简单的HTTP压测工具,它可以在单个多核CPU的情况下,利用系统自身的高性能I/O机制,通过多线程和事件模式,在目标机器上产生大量的负载。本次模拟实验使用wrk工具对服务器进行压力测试,发起大量请求,看看当服务器上的TCP全连接队列满时会发生什么情况?观察指标有哪些?客户端执行wrk命令对服务端发起压力测试,并发发送3万个连接:可以在服务端使用ss命令查看当前TCP全连接队列情况:期间一共执行了两次ss命令,以及上面的输出结果,可以发现当前TCP全连接队列的大小已经上升到129,超过了最大的TCP全连接队列。当超过TCP最大全连接队列时,服务器将丢弃后续传入的TCP连接,并统计丢失的TCP连接数。我们可以使用netstat-s命令查看:上面看到的41150次表示全连接队列溢出的次数,注意这是一个累加值。它可以每隔几秒执行一次。如果这个数字不断增加,那么全连接队列肯定偶尔会满。从上面的模拟结果我们可以知道,当服务器并发处理大量请求时,如果TCP全连接队列太小,很容易溢出。当TCP全连接队溢出时,后续请求将被丢弃,服务器请求数不会增加。Linux有一个参数来指定当TCP全连接队列满时,将采用什么策略响应客户端。事实上,断开连接只是Linux的默认行为。我们也可以选择向客户端发送RST重置消息,告诉客户端连接失败。tcp_abort_on_overflow有两个值,0和1,分别代表:0:如果全连接队列已满,则服务器丢弃客户端发送的ack;1:如果全连接队列已满,服务器向客户端发送一个复位包,表示握手过程和连接被取消;如果想知道客户端是否连接不上服务器,或者服务器上的TCP全连接队列是否已满,那么可以将tcp_abort_on_overflow设置为1,此时如果客户端出现异常,可以看到很多连接resetbypeererrors,所以可以证明是服务器端TCP全连接队列溢出造成的。通常情况下,tcp_abort_on_overflow应该设置为0,因为这样更有利于应对突发流量。例如,当TCP全连接队列已满,服务器丢失ACK时,同时客户端的连接状态为ESTABLISHED,进程在已建立的连接上发送请求。只要服务器没有为请求回复ACK,请求就会被重发多次。如果服务器上的进程只是暂时忙,accept队列已满,那么当TCP全连接队列有空间时,再次收到的请求报文仍会触发服务器成功建立连接,因为其中包含ACK。因此,将tcp_abort_on_overflow设置为0可以提高连接建立的成功率。只有当你非常确定TCP全连接队列会长时间溢出时,才可以设置为1,尽快通知客户端。如何增加TCP全连接队列?是的,当我们发现TCP全连接队列溢出时,我们需要增加队列的大小,这样才能处理来自客户端的大量请求。TCP全连接队列的最大值取决于somaxconn和backlog之间的最小值,即min(somaxconn,backlog)。从下面的Linux内核代码我们可以知道:somaxconn是Linux内核的一个参数,默认值为128,可以通过/proc/sys/net/core/somaxconn设置它的值;backlog是listen(intsockfd,intbacklog)函数中Nginx中的backlog大小,Nginx默认值为511,其长度可以通过修改配置文件来设置;在之前的模拟测试中,我的测试环境:somaxconn是默认值128;Nginx的backlog是默认值511,所以测试环境的TCP全连接队列的最大值是min(128,511),也就是128,可以执行ss命令查看:现在我们重新-test,增加TCP全连接队列,设置somaxconn为5000:然后设置Nginx的backlog为5000:最后重启Nginx服务,因为只有调用listen()函数才会重新初始化TCP全连接队列再次。重启Nginx服务后,服务端执行ss命令查看TCP全连接队列大小:从执行结果可以发现TCP全连接的最大值为5000,增加TCP全连接后queue,继续压测client,并发30000个连接向server发送请求:server执行ss命令,查看TCP全连接queue的使用情况:从上面的执行结果可以发现,全连接的queueusage增长很快,但是一直没有超过最大值,所以不会溢出,那么netstat-s不会显示TCP全连接队列的溢出数:说明TCP全连接队列的最大值从1285000之后,服务器已经抵挡了3万个并发连接请求,没有满连接队列溢出。如果有连续连接因TCP全连接队列溢出而被丢弃,则应增加backlog和somaxconn参数。实战-TCP半连接队列溢出如何查看TCP半连接队列长度?不幸的是,TCP半连接队列的长度不能像全连接队列那样用ss命令查看。但是我们可以把握TCP半连接的特点,即服务器处于SYN_RECV状态的TCP连接在TCP半连接队列中。因此,我们可以使用如下命令来计算当前的TCP半连接队列长度:如何模拟TCP半连接队列溢出场景?模拟TCP半连接溢出场景并不难。其实就是一直向服务端发送TCPSYN包,但是第三次??握手没有返回ACK,这会让服务端有大量的TCP连接处于SYN_RECV状态。这其实就是所谓的SYNflood、SYNattack、DDosattack。实验环境:客户端和服务器均为CentOs6.5,Linux内核版本2.6.32服务器IP192.168.3.200,客户端IP192.168.3.100服务器为Nginx服务,端口为8088注:本次模拟实验未启用tcp_syncookies,tcp_syncookies的功能稍后会解释。本实验使用hping3工具模拟SYN攻击:当服务器受到SYN攻击时,与服务器的ssh连接会断开,无法再次连接。只能在服务器主机上执行查看当前TCP半连接队列大小:同时也可以通过netstat-s观察半连接队列溢出情况:上面输出的值是一个累计值,表示有多少个TCP连接因半连接队列溢出而被丢弃。每隔几秒执行几次。如果有上升趋势,说明当前存在半连接队列溢出现象。大部分人说tcp_max_syn_backlog指定了半连接队列的大小,是这样吗?不幸的是,半连接队列的大小不仅仅与tcp_max_syn_backlog有关。上面模拟SYN攻击场景时,服务器上tcp_max_syn_backlog的默认值如下:但是在测试的时候发现服务器最多只有256个半连接队列,而不是512个,所以最大长度半连接队列的大小不一定由tcp_max_syn_backlog的值决定。接下来,走进Linux内核源码,分析一下TCP半连接队列的最大值是如何确定的。TCP第一次握手(收到SYN包)的Linux内核代码如下,减少了很多代码,只需要关注TCP半连接队列溢出的处理逻辑:从源码可以得出结论有三个条件。队列长度丢弃:如果半连接队列已满且未开启tcp_syncookies,则丢弃;如果全连接队列已满,并且有多个连接请求没有重传SYN+ACK包,则被Discard;如果tcp_syncookies没有开启,max_syn_backlog减去当前半连接队列长度小于(max_syn_backlog>>2),就会被丢弃;tcp_syncookies的设置后面会详细介绍,先告诉大家,开启tcp_syncookies是缓解SYN攻击的手段之一。接下来我们继续检测半连接队列是否满的函数inet_csk_reqsk_queue_is_full和检测全连接队列是否满的函数sk_acceptq_is_full:从上面的源码我们可以知道全连接队列的最大值是sk_max_ack_backlog变量,而sk_max_ack_backlog其实是在listen()源码中指定的,即min(somaxconn,backlog);半连接队列的最大值是max_qlen_log变量,max_qlen_log指定在哪里?我们还不知道,让我们继续跟进;我们继续跟进代码,看看semi-join队列的最大值max_qlen_log是在哪里初始化的:从上面的代码可以计算出max_qlen_log为8,所以我们代入检测semi-join的函数reqsk_queue_is_full队列是否满:即当qlen>>8为1时,表示半连接队列满。这个不难计算,很明显当qlen为256时,256>>8=1。至此,我终于知道为什么上面模拟SYN攻击时,服务器上的最大SYN_RECV连接数是只有256。可见半连接队列的最大值不仅由max_syn_backlog决定,还与somaxconn和backlog有关。在Linux2.6.32内核版本中,它们之间的关系可以概括为:当max_syn_backlog>min(somaxconn,backlog)时,半连接队列的最大值max_qlen_log=min(somaxconn,backlog)*2;当max_syn_backlog>2),就会被丢弃;假设条件1是当前半连接队列的长度“没有超过”半连接队列max_qlen_log的理论最大值,那么如果条件3成立,SYN包仍然会被丢弃,最大数量处于SYN_REVC状态的服务器不会是理论值max_qlen_log。好像很难理解,我们继续做实验,实验看真相。服务器环境如下:配置完成后,服务器需要重启Nginx,因为在listen()函数中初始化了全连接队列的最大值和半连接队列的最大值。根据前面的源码分析,我们可以计算出半连接队列max_qlen_log的最大值为256:客户端执行hping3发起SYN攻击:服务端执行如下命令查看SYN_RECV状态下的最大值:可以发现服务器在SYN_RECV状态下的最大数量不是max_qlen_log变量的值。这就是上面说的原因:如果当前半连接队列的长度“没有超过”理论半连接队列max_qlen_log的最大值,那么如果条件3成立,SYN包还是会被丢弃,并且服务器将处于SYN_REVC状态最大数量不会是理论值max_qlen_log。下面分析一波条件3:从上面的分析我们可以知道,如果触发了“当前半连接队列长度>192”这个条件,那么第一次TCP握手的SYN包就会被丢弃。由于我们之前测试的结果,SYN_RECV状态的服务器最大数量为193,恰好触发了条件3,所以SYN_RECV状态还没有达到“理论半连接队列的最大值256”,并且SYN包掉线了。因此,服务器处于SYN_RECV状态的最大数量分为以下两种情况:如果“当前半连接队列”没有超过“半连接队列的理论最大值”,但超过max_syn_backlog-(max_syn_backlog>>2),那么它在SYN_RECV状态的最大个数是max_syn_backlog-(max_syn_backlog>>2);如果“当前半连接队列”超过“半连接队列理论最大值”,则SYN_RECV状态下的最大数为“半连接队列理论最大值”;对于每个Linux内核版本,“理论”半连接最大值的计算方式不同。上面我们针对Linux2.6.32版本分析了“理论上”的半连接最大算法,每个版本可能略有不同。比如在Linux5.0.0中,“理论上”的最大半连接是全连接队列的最大值,但是队列溢出还有三种情况:只能被丢弃?不是这种情况。开启syncookies功能可以在不使用SYN半连接队列的情况下成功建立连接。我们在上面的源码分析中也可以看出这一点。开启syncookies功能后,连接不会被丢弃。syncookies的作用是这样的:服务器根据当前状态计算出一个值,并在自己这边发送的SYN+ACK报文中发送。客户端返回ACK报文时,取出值进行校验。如果合法则认为连接成功,如下图。syncookies参数主要有以下三个值:值为0表示禁用该功能;值为1表示仅当SYN半连接队列放不下时才启用;值为2表示无条件启用该功能;那么在处理SYN攻击时,只需要设置为1即可:如何防御SYN攻击?下面介绍几种防御SYN攻击的方法:增加半连接队列;开启tcp_syncookies功能,减少SYN+ACK重传次数。方法一:增加半连接队列。在前面的源码和实验中,我们了解到,要增加半连接队列,我们??知道不能只增加tcp_max_syn_backlog的值,还需要同时增加somaxconn和backlog,即增加全连接队列。否则,简单地增加tcp_max_syn_backlog是无效的。增加tcp_max_syn_backlog和somaxconn的方法是修改Linux内核参数:每个Web服务增加backlog的方法不一样。例如增加Nginx的backlog的方法如下:最后,更改以上参数后,重启Nginx服务,因为半连接队列和全连接队列都是在listen()中初始化的。方法二:启用tcp_syncookies功能启用tcp_syncookies功能的方法也很简单。修改Linux内核参数:方法三:减少SYN+ACK重传次数当服务器受到SYN攻击时,会出现大量处于SYN_REVC状态的TCP连接。该状态下的TCP会重传SYN+ACK,当重传次数超过上限时,就会断开连接。那么对于SYN攻击场景,我们可以通过减少SYN+ACK的重传次数来加速SYN_REVC状态下TCP连接的断开。巨人的肩膀[1]系统性能调优必知。陶辉。极客时间。[2]https://www.cnblogs.com/zengk...[3]https://blog.cloudflare。com/s...小林是插画的工具人,再见,下次再见!