经过之前对内核源码的阅读,已经简单了解了 linux 内核是如何管理 tcp 连接信息的。当然,之前把重点放在了三次握手的全过程,忽略了不少细节,这篇文章来详细讨论下两个概念:“半连接队列” 和 全连接队列。
同样,本文还是基于 linux-4.18.1的版本。
三次握手过程
看下详细过程:
第一次握手:
调用 connect 请求连接,客户端向服务端发送 SYN 包,并设置 sock 状态 SYN_SENT;
第二次握手:
服务端接收到 SYN 报文后,新建一个 request_sock 对象,用来表示这个 半连接,同时将这个 sock 设置为 NEW_SYN_RECV 状态,并添加到 ehash 中,更新半连接数 qlen++, 最后向客户端响应 SYN + ACK;
第三次握手:
客户端收到第二次握手包后,向服务端发送 ACK 报文;
服务端接收到 ACK 报文后,新建一个 tcp_sock 对象表示这个全连接,设置状态为 SYN_RECV, 然后将它添加到 ehash中;此外,将代表半连接的那个 request_sock 对象从 ehash 中移除,添加到 全连接队列 中,然后更新半连接数 qlen-- 和 全连接数 sk_ack_backlog++,最后将代表全连接的 sock 状态修改为 ESTABLISHED;
通过以上流程可以看到,在 linux 4.18 这个版本中,并不存在所谓的 半连接队列,表示半连接状态的 requst_sock 保存在全局的 ehash 中, 具体结构可以参考文章 TCP源码浅读(1)数据结构,而且它的状态也不是 SYN_RECV,是 NEW_SYN_RECV。这个变化是 linux 4.4 以后产生的,可以阅读下这篇文章 TCP_NEW_SYN_RECV。
此外,全连接队列 保存的也不是表示全连接状态的 tcp_sock,而是表示半连接的 requst_sock ,只是每个 requst_sock 又关联到了 tcp_sock。
本文结合源码来解析下 半连接数 和 全连接数 是如何影响服务器接收新的请求连接的。
半连接
怎么查看半连接数?
可以通过 netstat 命令查看当前处于半连接状态的连接个数:
半连接数最大值
linux 不同的版本对 半连接数最大值 的定义有所不同,此外,服务端支持的最大半连接数还受内核参数的限制。在 TCP源码浅读(4)三次握手-被动方接收SYNTCP源码浅读(4)三次握手-被动方接收SYN 中分析过服务端接收 SYN 的流程,服务端接收到客户端的 SYN 请求包后,会创建一个 request_sock 对象表示这个半连接,而在创建这个对象之前,服务端会首先检查半连接数是否已经达到最大值,看下实现:
暂时忽略 sysctl_tcp_syncookies 这个值的影响,画个流程图:
从源码流程我们可以看到,服务端支持的最大半连接数受到三个参数的限制:
1、半连接数最大值的限制;
2、全连接数最大值的限制;
3、内核参数 /proc/sys/net/ipv4/tcp_max_syn_backlog 的限制。
在 linux 4.18.1 的这个版本中, 半连接数最大值和全连接数最大值一样,都等于 min(backlog, somaxconn),
- backlog: listen 函数第二个参数
- somaxconn: 内核参数,/proc/sys/net/core/somaxconn
而条件三则限制了最大半连接数要小于 3/4 * tcp_max_syn_backlog。
做两个模拟测试验证下
模拟测试一
写个简单的服务器程序,设置 backlog = 128: listen(listenfd, 128);
查看 somaxconn 和 tcp_max_syn_backlog :
可以得到 半连接数 和 全连接数 最大值都等于 min(somaxconn, backlog) = 128;
而达到以上 第三点 导致连接请求被丢弃的大小为 3/4 * tcp_max_syn_backlog = 3/4 * 1024 = 768 。
因此,该服务程序支持的最大半连接数应该是 128 。
接下来我们用 hping3 命令测试下,受机器限制,本次测试服务器程序 和 hping3 程序在同一台机器,端口随便起个 9090 。
查看当前半连接数,你会发现,它始终不会超过 128 (半连接数最大值得判断是大于等于就丢弃)。
模拟测试二
现在我们修改下参数,看下满足条件三的效果
再次执行同样的测试,你会发现这一次 半连接数始终是 61 (条件三判断是大于,所以要加1)。
此外,我们还可以通过以下方式判断 半连接数 是否已经达到服务端支持的最大值了:
达到最大值后怎么处理?
比较常见的有三种方案:
1、增大服务器支持的最大半连接数(要同时修改三个参数);
2、减小 SYN + ACK 重传次数,加速半连接状态连接的死亡;
3、设置 tcp_syncookies 参数,直接不使用半连接队列。
1、增大服务器支持的最大半连接数
如果真的是并发请求量大,我们可以修改相应参数,增大服务器支持的最大半连接数。从源码分析我们看到要同时修改以下三个参数才会有效:
- 服务器程序 listen() 函数中的第二个参数值 backlog;
- 内核参数 /proc/sys/net/core/somaxconn;
- 内核参数 /proc/sys/net/ipv4/tcp_max_syn_backlog
2、减小 SYN + ACK 的重传次数。
服务端收到第三次握手后,会将半连接状态的 requset_sock 对象从 ehash 中移除,同时,更新半连接数 qlen-- ;而在一定时间内如果一直没有收到第三次握手的响应包,就会向客户端重传 SYN + ACK 包,一直到收到第三次握手或重传次数达到最大值后才会断开连接:
在正常的连接请求过程中,重传次数越多,成功连接建立的概率就越大;
但是如果遇到 SYN 攻击,这个值越小就越好,因为它可以加速连接状态的死亡,减轻半连接的无效占用。
3、开启 tcp_syncookies
从前面的源码可以看出,在开启 tcp_syncookies 之后,条件一 和 条件三 将直接失效,此时,服务端支持的最大半连接数仅受 全连接最大值 的限制。
/proc/sys/net/ipv4/tcp_syncookies 参数有三个选项值:
- 0 ,关闭该功能;
- 1 ,当 SYN 半连接队列放不下时,启用;
- 2 ,无条件开启功能。
当然,在开启 tcp_syncookies 后,因为没有将这个半连接保存到全局信息表中,那在服务端在收到第三次握手包后是怎么获取连接信息的呢?
服务器收到 SYN 报文后,将 连接信息编码放在 ISN 中, 随着 SYN+ACK 响应报文发送给客户端,不用将这个连接加到半连接队列;
客户端返回 ACK 报文时要同时带上这个编码值,服务端收到响应后,取出该编码值,对其进行反算校验,如果验证通过,就可以成功建立连接,加入到全连接队列。
全连接队列
全连接队列长度查看
全连接队列 也叫 accept 队列 ,用来存放处于 ESTABLISHED 状态的连接,等待程序调用 accept 函数将连接从队列取走。
可以用 ss 命令 来查看 当前全连接队列大小 和 全连接队列最大值。
使用该命令查看全连接队列信息的时候一定要使用 -l 选项,表示查看处于 监听状态 下的连接信息:
- Recv-Q :当前全连接队列大小
- Send-Q:全连接队列最大值
如果不指定监听状态,Recv-Q 和 Send-Q 表示的含义为:
- Recv-Q:已收到但未被应用程序读取的字节数;
- Send-Q:已发送但未收到确认的字节数;
全连接队列最大值
看下 linux-4.18.1 版本的实现
可以看到,全队列最大值等于 min(backlog, somaxconn)。
而全连接数的检查在 TCP源码浅读(6)三次握手-被动方接收ACK已经看到过,服务端在收到客户端第三次握手包后,首先要对包进行合法性检查,检查通过后就会检查全连接数是否已经达到最大值,如果没有达到最大值才会开始创建一个新的 tcp_sock 对象表示这个连接并加入到全连接队列。
还是用之前的服务程序来验证下:
模拟测试一
服务器程序,设置 backlog = 128: listen(listenfd, 128);
查看 somaxconn ;
此时,全连接队列 最大值等于 min(somaxconn, backlog) = 128。
使用 wrktcp 工具来测试下:
查看全连接队列情况,会看到全连接队列一直等于 129。
此外,我们还可以通过命令 netstat -s 来查看 全连接队列溢出 情况 :
达到最大值后有什么现象?
当全连接队列达到最大值后,根据内核参数 /proc/sys/net/ipv4/tcp_abort_on_overflow 的不同配置,服务器有相应的处理:
- 0 :扔掉 客户端 发过来的 ACK 包 ;
- 1 :发送一个 RST 包给客户端,表示中断这个连接;
linux 默认设置为 0,因为这样可以提高连接建立成功率,有两个原因:
第一,虽然服务端扔掉了这个 ACK 包,但是客户端认为这个连接已经建立成功,并可以发送数据,而数据包里携带的 ACK 也能使服务端成功建立连接。
第二,在服务器扔掉客户端的 ACK 包后,如果客户端一直没有发送数据,服务端这边会触发重试机制,重新向客户端发送第二次握手包 SYN+ACK,客户端收到报文后也会重新发送 ACK 报文,进而提高成功建立连接的机会。