全连接队列和半连接队列
全连接队列和半连接队列

全连接队列和半连接队列

什么是全连接队列和半连接队列?

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;
  • 全连接队列,也称 accepet 队列;

服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。

不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。

如何查看进程半连接队列长度?

服务端处于 SYN_RECV 状态的 TCP 连接,就是 TCP 半连接队列。

netstat -natp | grep SYN_RECV | wc -l

# -n: Do not try to resolve service names.
# -a: Display both listening and non-listening (for TCP this means established connections) sockets.
# -t: Display TCP sockets.
# -p: Show process using socket.

如何观察半连接队列溢出情况?

netstat -s | grep "SYNs to LISTEN"

# -s: Display summary statistics for each protocol.

上面输出的数值是累计值,表示共有多少个 TCP 连接因为半连接队列溢出而被丢弃。隔几秒执行几次,如果有上升的趋势,说明当前存在半连接队列溢出的现象

半连接队列的大小由什么决定?

注意⚠️:max_qlen_log理论半连接队列最大值,并不一定代表服务端处于 SYN_REVC 状态的最大个数。

注意⚠️:Linux内核版本对于该值的计算是不同的。

Linux内核对于半连接队列溢出的逻辑大致如下:

  1. 如果半连接队列满了,并且没有开启 tcp_syncookies,则会丢弃;
  2. 若全连接队列满了,且没有重传 SYN+ACK 包的连接请求多于 1 个,则会丢弃;
  3. 如果没有开启 tcp_syncookies,并且 max_syn_backlog 减去 当前半连接队列长度小于(max_syn_backlog >> 2),则会丢弃;

半如果 SYN 半连接队列已满,只能丢弃连接吗?

并不是这样开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,在前面我们源码分析也可以看到这点,当开启了 syncookies 功能就不会丢弃连接。也就是仅当SYN半连接队列放不下时,再启动这个功能。

syncookies 是这么做的:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功,如下图所示。

开启 syncookies 功能:

echo 1 > /proc/sys/net/ipv4/tcp_syncookies

# syncookies 参数主要有以下三个值:
#   0 值,表示关闭该功能;
#   1 值,表示仅当 SYN 半连接队列放不下时,再启用它;
#   2 值,表示无条件开启功能;

调整半连接队列长度的方法

要想增大半连接队列,不能只单纯增大 tcp_max_syn_backlog 的值,还需一同增大 somaxconn backlog,也就是增大全连接队列。否则,只单纯增大 tcp_max_syn_backlog 是无效的。

  • 增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 内核参数:
echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog

echo 2048 > /proc/sys/net/core/somaxconn

  • 增大 backlog 的方式,每个 Web 服务都不同,比如 Nginx 增大 backlog 的方法如下:

server {
  listen 80 default backlog=1024;
  server_name localhost;
  ...
}
# 更改完成后需要重载

如何知道应用程序的 TCP 全连接队列大小?

可以使用ss命令获取的Recv-Q/Send-Q在「LISTEN 状态」和「非 LISTEN 状态」下的全连接队列长度(注意这俩是不是一样的)

在「LISTEN 状态」时,Recv-Q/Send-Q 表示的含义如下:

  • Recv-Q:当前全连接队列的大小,也就是当前已完成三次握手并等待服务端 accept() 的 TCP 连接;
  • Send-Q:当前全连接最大队列长度;
ss -lnt

State    Recv-Q   Send-Q   Local Address:Port     Peer Address:Port   Process                 
LISTEN   0        4096           0.0.0.0:8010          0.0.0.0:*                              
LISTEN   0        1024           0.0.0.0:19222         0.0.0.0:*                              
LISTEN   0        1024           0.0.0.0:11221         0.0.0.0:*                              

# -l: Display only listening sockets (these are omitted by default).
# -n: Do not try to resolve service names. Show exact bandwidth values, instead of human-readable.
# -t: Display TCP sockets.

在「非 LISTEN 状态」时,Recv-Q/Send-Q 表示的含义如下:

  • Recv-Q:已收到但未被应用进程读取的字节数;
  • Send-Q:已发送但未收到确认的字节数;
ss -nt

State        Recv-Q   Send-Q   Local Address:Port      Peer Address:Port     Process                
FIN-WAIT-2   0        0         10.189.64.90:36556      10.189.65.3:8101                            
ESTAB        0        0         10.189.64.90:45454      10.189.65.3:8101                            

# -n: Do not try to resolve service names. Show exact bandwidth values, instead of human-readable.
# -t: Display TCP sockets.

如何查看TCP全队列的溢出情况?

当超过了 TCP 最大全连接队列,服务端则会丢掉后续进来的 TCP 连接,丢掉的 TCP 连接的个数会被统计起来,我们可以使用 netstat -s 命令来查看:

netstat -s | grep overflowed

# 输出示例:
    1079 times the listen queue of a socket overflowed

# -s: Display summary statistics for each protocol.

当服务端并发处理大量请求时,如果 TCP 全连接队列过小,就容易溢出。发生 TCP 全连接队溢出的时候,后续的请求就会被丢弃,这样就会出现服务端请求数量上不去的现象。

当全队列溢出时,回复客户端的包是什么样的?

丢弃连接只是 Linux 的默认行为,我们还可以选择向客户端发送 RST 复位报文,告诉客户端连接已经建立失败。

cat /proc/sys/net/ipv4/tcp_abort_on_overflow 
0

tcp_abort_on_overflow 共有两个值分别是 0 和 1,其分别表示:

  • 0 :如果全连接队列满了,那么 server 扔掉 client 发过来的 ack ;
  • 1 :如果全连接队列满了,server 发送一个 reset 包给 client,表示废掉这个握手过程和这个连接;

如果要想知道客户端连接不上服务端,是不是服务端 TCP 全连接队列满的原因,那么可以把 tcp_abort_on_overflow 设置为 1,这时如果在客户端异常中可以看到很多 connection reset by peer 的错误,那么就可以证明是由于服务端 TCP 全连接队列溢出的问题。

通常情况下,应当把 tcp_abort_on_overflow 设置为 0,因为这样更有利于应对突发流量。

举个例子,当 TCP 全连接队列满导致服务器丢掉了 ACK,与此同时,客户端的连接状态却是 ESTABLISHED,进程就在建立好的连接上发送请求。只要服务器没有为请求回复 ACK,请求就会被多次重发。如果服务器上的进程只是短暂的繁忙造成 accept 队列满,那么当 TCP 全连接队列有空位时,再次接收到的请求报文由于含有 ACK,仍然会触发服务器端成功建立连接。

所以,tcp_abort_on_overflow 设为 0 可以提高连接建立的成功率,只有你非常肯定 TCP 全连接队列会长期溢出时,才能设置为 1 以尽快通知客户端。

如何增大TCP全连接队列?

TCP 全连接队列的最大值取决于 somaxconnbacklog 之间的最小值,也就是 min(somaxconn, backlog)

  • 增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 内核参数:
echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog

echo 2048 > /proc/sys/net/core/somaxconn
  • 增大 backlog 的方式,每个 Web 服务都不同,比如 Nginx 增大 backlog 的方法如下:
server {
  listen 80 default backlog=1024;
  server_name localhost;
  ...
}
# 更改完成后需要重载

参考文章:https://www.cnblogs.com/xiaolincoding/p/12995358.html