文章

如何正确查看线上半/全连接队列溢出情况?

如何正确查看线上半/全连接队列溢出情况?

如何正确查看线上半/全连接队列溢出情况?

原创张彦飞allen开发内功修炼2021-08-23 08:33

收录于话题#开发内功修炼之网络篇30个

大家好,我是飞哥!

《深入解析常见三次握手异常》 这一文中,我们讨论到如果发生连接队列溢出而丢包的话,会导致连接耗时会上涨很多。那如何判断一台服务器当前是否有半/全连接队列溢出丢包发生呢?

我在我早期的一篇文章里提到过,可以通过 netstat 来查看。

```plain text

netstat -s

1
2
3
8 SYNs to LISTEN sockets ignored
160 times the listen queue of a socket overflowed
... ```
  • SYNs to LISTEN ockets ignored 前面的数字是半连接队列的溢出,
  • times the listen queue of a socket overflowed 前面的数字是全连接队列的溢出。

如果这两个数字在动态增长,那就说明当前有溢出发生了。

但可惜这个说法存在一些问题。其中对于全连接队列溢出描述 ok,但半连接队列的描述很不正确!所以我今天专门发篇文章纠正一下,来从源码角度来分析一下为啥这样说。

一、全连接队列溢出判断

全连接队列溢出判断比较简单,所以先说这个。

1. 全连接溢出丢包

全连接队列溢出都会记录到 ListenOverflows 这个 MIB(Management Information Base,管理信息库)上的,对应 SNMP 统计信息中的 ListenDrops 这一项。我们来展开看一下相关的源码。

服务器在响应客户端的 SYN 握手包的时候,有可能会在 tcp_v4_conn_request 这里发生全连接队列溢出而丢包。

```plain text //file: net/ipv4/tcp_ipv4.c int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb) { //看看半连接队列是否满了 …

//看看全连接队列是否满了 if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) { NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS); goto drop; } … drop: NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS); return 0; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
从上述代码中可以看到,全连接队列满了以后调用 NET_INC_STATS_BH 增加了 LINUX_MIB_LISTENOVERFLOWS 和 LINUX_MIB_LISTENDROPS 这两个 MIB。

服务器在响应第三次握手的时候,会再次判断全连接队列是否溢出。如果溢出,一样会增加这两个 MIB。源码如下:

```plain text
//file: net/ipv4/tcp_ipv4.c
struct sock *tcp_v4_syn_recv_sock(...)
{
 if (sk_acceptq_is_full(sk))
  goto exit_overflow;
 ...
exit_overflow:
 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
exit:
 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
 return NULL;
}

在 proc.c 中, LINUX_MIB_LISTENOVERFLOWS 和 LINUX_MIB_LISTENDROPS 都被整合进了 SNMP 统计信息。

```plain text //file: net/ipv4/proc.c static const struct snmp_mib snmp4_net_list[] = { SNMP_MIB_ITEM(“ListenDrops”, LINUX_MIB_LISTENDROPS), SNMP_MIB_ITEM(“ListenOverflows”, LINUX_MIB_LISTENOVERFLOWS), …… }

1
2
3
4
5
6
7
8
9
10
11
12
13
### 2.netstat 工具源码

我们在执行 netstat -s 的时候,该工具会读取 SNMP 统计信息并展现出来。netstat 命令属于 net-tool 工具集,所以得找 net-tool 的源码。我用 SYNs to LISTEN sockets dropped 这种关键词去搜到了:

```plain text
//file: https://github.com/giftnuss/net-tools/blob/master/statistics.c
struct entry Tcpexttab[] =
{
 { "ListenDrops", N_("%u SYNs to LISTEN sockets dropped"), opt_number },
 { "ListenOverflows", N_("%u times the listen queue of a socket overflowed"),
 ......
}

以上这些就是执行 netstat -s 时会执行到的源码。它从 SNMP 统计信息中获取到 ListenDrops 和 ListenOverflows 这两项显示了出来,分别对应 LINUX_MIB_LISTENDROPS 和 LINUX_MIB_LISTENOVERFLOWS 这两个 MIB。

```plain text

watch ‘netstat -s | grep overflowed’

198 times the listen queue of a socket overflowed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
所以,每当发生全连接队列满导致的丢包的时候,可以通过上述命令的结果里体现出来。而且幸运的是,ListenOverflows 这个 SNMP 统计项只有在全连接队列满的时候才会增加,内核源码其它地方没有用到。

**所以,通过 netstat -s 输出中的 xx times the listen queue 中的查看到数字如果有变化,那么一定是你的服务器上发生了全连接队列溢出了!!**

## 二、半连接队列溢出判断

再来看半连接队列,溢出时是更新的是 LINUX_MIB_LISTENDROPS 这个 MIB,对应到 SNMP 就是 ListenDrops 这个统计项。

```plain text
//file: net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
 //看看半连接队列是否满了
 if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
  want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
  if (!want_cookie)
   goto drop;
 }

 //看看全连接队列是否满了
 if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
  NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
  goto drop;
 }
 ...
drop:
 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
 return 0;
}

上述源码中可见,半连接队列满的时候 goto drop,然后增加了 LINUX_MIB_LISTENDROPS 这个 MIB。通过上一节 netstat -s 的源码我们看到也会展示它出来(对应 SNMP 中的 ListenDrops 这个统计项)。

但是问题在于,不仅仅只是在半连接队列发生溢出的时候会增加该值。所以根据 netstat -s 看半连接队列是否溢出是不靠谱的!

上面看到,即使半连接队列没问题,全连接队列满了该值也会增加。另外就是当在 listen 状态握手发生错误的时候,进入 tcp_v4_err 函数时也会增加该值。

对于如何查看半连接队列溢出丢包这个问题,我的建议是不要纠结咋看是否丢包了。直接看服务器上的 tcp_syncookies 是不是 1 就行。

如果该值是 1 ,那么下面代码中 want_cookie 就返回是真,是根本不会发生半连接溢出丢包的。

```plain text //file: net/ipv4/tcp_ipv4.c int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb) { //看看半连接队列是否满了 if (inet_csk_reqsk_queue_is_full(sk) && !isn) { want_cookie = tcp_syn_flood_action(sk, skb, “TCP”); if (!want_cookie) goto drop; }

1
2
3
4
5
6
7
8
如果 tcp_syncookies 不是 1,则建议改成 1 就完事了。

如果因为各种原因就是不想打开 tcp_syncookies。就想死磕看下是否有因为半连接队列满而导致的 SYN 丢弃,除了 netstat -s 的结果,我建议同时查看下当前 listen 的端口上的 SYN_RECV 的数量。

```plain text
# netstat -antp | grep SYN_RECV
256

《为什么服务端程序都需要先 listen 一下?》中我们讨论了半连接队列的实际长度怎么计算。如果 SYN_RECV 状态的连接数量达到你算出来的队列长度了,那么可以确定是有半连接队列溢出了。如果想加大半连接队列的长度,方法我们在上面文章里也一并讲过了。

三、总结

最后,总结一下。

对于全连接队列来说,使用 netstat -s (最好再配合 watch 命令动态观察)就可以判断是否有丢包发生。如果看到 “xx times the listen queue of a socket overflowed” 中的数值在增长,那么就确定是全连接队列满了。

```plain text

watch ‘netstat -s | grep overflowed’

198 times the listen queue of a socket overflowed

1
2
3
4
5
6
7
8
9
10
对于半连接队列来说,只要保证 tcp_syncookies 这个内核参数是 1 就能保证不会有因为半连接队列满而发生的丢包。

如果确实较真就像看一看,netstat -s | grep “SYNs” 这个是没有办法说明问题的。还需要你自己计算一下半连接队列的长度,再看下当前 SYN_RECV 状态的连接的数量。

```plain text
# watch 'netstat -s | grep "SYNs"'
 258209 SYNs to LISTEN sockets dropped
# netstat -antp | grep SYN_RECV | wc -l
5

至于如何加大半连接队列长度,参考《为什么服务端程序都需要先 listen 一下?》这篇文章就行了。

结尾再感叹一句,很多问题真的是只有通过源码才能找到最准确的结论啊!!

最后,求再看,求转发!

Github: https://github.com/yanfeizhang/coder-kung-fu

本文由作者按照 CC BY 4.0 进行授权