日常英语,每天进步一点:前言网上很多博客都是针对增加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
