什么是半连接队列?什么是全连接队列?
半连接队列是指:用于存储处于 SYN_RECV 状态的连接的队列
当内核收到一个 SYN 报文,就将该连接放到半连接队列,并发送 ACK 给对方
全连接队列是指:用于存储已经建立好 TCP 连接,但还未被应用调用 accept 取走的连接的队列
当内核收到 SYN 报文对应的 ACK 后,就将该连接从半连接队列中取出,并放到全连接队列,等待应用程序调用 accept 取走
半连接队列与全连接队列的本质
虽然都有「队列」二字,但二者 实际上都不是队列
- 当一个连接请求到来,内核会将连接元数据存到 半连接队列,发送 ack
- 收到某个连接的 ack 的 ack 后,从半连接队列中取出该连接,发送 ack,并放到 全连接队列 中
- 服务进程调用 accept,取出一个建立好的 TCP 连接
“收到某个连接的 ack 的 ack 后,从半连接队列中取出该连接”的「取出」过程是一个 随机 的过程,因为这个 ack 的顺序并不固定
如果半连接队列设计成线性结构,那么取出对应连接就需要 O(n) 的时间
因此,半连接队列实际上是一个哈希表,取出对应连接的期望时间是 O(1)
而全连接队列实际上是一个链表,当然,也可以理解成队列(毕竟队列也有基于链表实现的)
在 Linux 上查看「全连接队列」的大小与容量
可以使用 ss -lnt 查看某个全连接队列的大小:
root@SkyLee:~# ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 4096 *:1145 *:*
- Recv-Q:当前监听的 socket,全连接队列中,没有被取走的 socket 数量(理解成 size)
- Send-Q:当前监听的 socket,全连接队列 size 的最大值(理解成 capacity)
「全连接队列」溢出,会发生什么?
当全连接队列满了以后,如果还有新的连接请求到来,内核会 默认直接丢弃 该连接,这个行为可以通过修改 /proc/sys/net/ipv4/tcp_abort_on_overflow 的值来改变:
root@SkyLee:~# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0
- 0:直接丢弃
- 1:发送 RST 报文给对方
为啥默认值为 0?直接丢弃,不会占用客户端的资源吗?
设置为 0,有利于提高请求的成功率
进入全连接队列,意味着客户端已经进入 ESTABLISHED 状态
当用户给服务器发请求时,只要服务器不 Response,客户端就会不断重试发送第三次握手的 ACK,只要在重试次数到达上限前,服务器调用了 accept 接收了该连接,就可以处理用户的请求了
因此,除非认为服务器需要很长时间才来得及 accept(超过了重传的上限时间),不要将 tcp_abort_on_overflow 修改成 1
「全连接队列」的容量如何修改?
经过上面的分析,可以发现,如果全连接队列的 capacity:
- 过小,可能造成 QPS 上不去
- 过大,可能造成连接数过多,服务器承受不了
有些时候,在生产环境发现 QPS 上不去,硬件又没吃满,排查不出问题,就可以考虑是不是全连接队列的 capacity 太小了
那么,这个容量应该怎么修改呢?
两个配置:
- listen 中的 backlog
/proc/sys/net/core/somaxconn
capacity 的值为二者的 最小值
在 Linux 上查看半连接队列的大小和容量
可以使用 netstat -natp | grep SYN_RECV | grep 1145 | wc -l 来查看 1145 端口上的,处于 SYN_RECV 状态的连接数量,间接的查看半连接队列的大小
netstat -natp | grep SYN_RECV | grep 1145 | wc -l
0
目前没有一个命令可以直接查看半连接队列的容量,但是如果想查看的话:
- 可以使用工具(如 hping3)对想查看的 server socket 发起 SYN 攻击,把半连接队列打满
- 再使用上面的命令查看,就可以间接的查看半连接队列的 capacity
「半连接队列」溢出,会发生什么?
当半连接队列满了以后,如果还有新的连接请求到来,并且 没有启用 syn_cookies,那么,新的连接请求 将会被抛弃
但是,可以通过启用 syn_cookies,来避免抛弃新的连接请求
syncookies 参数主要有以下三个值:
- 0,表示关闭该功能;
- 1,表示仅当半连接队列放不下时,再启用它;
- 2,表示无条件开启功能
应对 SYN 攻击,可以将 syn_cookies 设置为 1
root@SkyLee:~# echo 1 > /proc/sys/net/ipv4/tcp_syncookies
开启 syn_cookies,可以实现有无半连接队列,都可以避免新的连接请求被抛弃(syn_cookies = 2),那为什么还需要半连接队列?全部都用 syn_cookies 不就好了?
有以下原因:
- 兼容性:不是所有的系统都支持或默认启用 SYN Cookie。保持半连接队列的机制可以确保在不支持 SYN Cookie 的系统上也能处理 SYN 连接
- 性能因素:syn_cookies 在正常操作中可能会引起一些额外的计算负担,尤其是在必须验证和重建初始序号时。当流量正常时,使用半连接队列可能会更有效率
- 资源管理:半连接队列允许服务器有更精确的控制方式来管理半开连接的资源。例如,它可以限制队列长度来防止资源耗尽
因此,半连接队列的存在是有必要的
最佳实践:在正常操作时使用半连接队列,而当检测到可能的 SYN Flood 攻击时启用 SYN Cookie
「半连接队列」的容量如何确定?
首先,是网上经常谈到的 /proc/sys/net/ipv4/tcp_max_syn_backlog 参数
root@SkyLee:~# cat /proc/sys/net/ipv4/tcp_max_syn_backlog
1024
但是,半连接队列的容量在不同的 Linux 版本上,有所不同
下面以内核版本为 2.6.32 的 Linux 为例,说说如何确定半连接队列的容量
第一个因素就是上面提到的 tcp_max_syn_backlog
第二个因素是 全连接队列的容量
是的,全连接队列的容量也会影响半连接队列,具体算法如下:
- 如果 tcp_max_syn_backlog > min(somaxconn, listen_backlog),即全连接队列的大小,那么
max_qlen_log = min(somaxconn, listen_backlog) * 2 - 否则,
max_qlen_log = tcp_max_syn_backlog * 2
简单来说,max_qlen_log 的值为 min(全连接队列容量,tcp_max_syn_backlog) * 2
max_qlen_log 的值就是 理论 半连接队列的容量
但实际上,如果 size > tcp_max_syn_backlog - (tcp_max_syn_backlog >> 2),连接也会被抛弃
因此,2.6.32 版本的 Linux 实际的 capacity 的值的计算公式 为:
min(tcp_max_syn_backlog - (tcp_max_syn_backlog >> 2), min(tcp_max_syn_backlog, min(somaxconn, listen_backlog)) * 2)
在 2.2 以前的 Linux 中,理论 半连接队列的大小为 listen 中的 backlog 在 5.0 版本的 Linux,理论 半连接队列的大小就是全连接队列的大小
总结一波:
- 不同版本的 Linux,半连接队列的 capacity 的计算方式不同
- 但半连接队列的 capacity 还是要受到全连接队列大小的影响
- 为了防止 SYN 攻击,可以启用 syn_cookies