开发内功修炼@张彦飞开发内功修炼@张彦飞

talk is cheap,
show me the code!

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

大家好,我是飞哥!

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

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

# netstat -s 
    8 SYNs to LISTEN sockets ignored
    160 times the listen queue of a socket overflowed
    ...
  • SYNs to LISTEN sockets ignored 前面的数字是半连接队列的溢出,
  • times the listen queue of a socket overflowed 前面的数字是全连接队列的溢出。

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

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

一、全连接队列溢出判断

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

1. 全连接溢出丢包

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

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

//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;
}

从上述代码中可以看到,全连接队列满了以后调用 NET_INC_STATS_BH 增加了 LINUX_MIB_LISTENOVERFLOWS 和 LINUX_MIB_LISTENDROPS 这两个 MIB。

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

//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 统计信息。

//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),
    ......
}

2.netstat 工具源码

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

//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。

# watch 'netstat -s | grep overflowed'
    198 times the listen queue of a socket overflowed

所以,每当发生全连接队列满导致的丢包的时候,可以通过上述命令的结果里体现出来。而且幸运的是,ListenOverflows 这个 SNMP 统计项在只有在全连接队列满的时候才会增加,内核源码其它地方没有用到。

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

二、半连接队列溢出判断

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

//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 就返回是真,是根本不会发生半连接溢出丢包的。

//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;
    }

如果 tcp_syncookies 不是 1,则建议改成 1 就完事了。

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

# netstat -antp | grep SYN_RECV 
256

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

三、总结

最后,总结一下。

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

# watch 'netstat -s | grep overflowed'
    198 times the listen queue of a socket overflowed

对于半连接队列来说,只要保证 tcp_syncookies 这个内核参数是 1 就能保证不会有因为半连接队列满而发生的丢包。如果确实较真就像看一看,netstat -s | grep "SYNs" 这个是没有办法说明问题的。还需要你自己计算一下半连接队列的长度,再看下当前 SYN_RECV 状态的连接的数量。

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

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

写在最后,由于我的这些知识在公众号里文章比较分散,很多人似乎没有理解到我对知识组织的体系结构。而且图文也不像视频那样理解起来更直接。所以我在知识星球上规划了视频系列课程,包括硬件原理、内存管理、进程管理、文件系统、网络管理、Golang语言、容器原理、性能观测、性能优化九大部分大约 120 节内容,每周更新。加入方式参见我要开始搞知识星球啦如何才能高效地学习技术,我投“融汇贯通”一票

Github:https://github.com/yanfeizhang/coder-kung-fu
关注公众号:微信扫描下方二维码
qrcode2_640.png

本原创文章未经允许不得转载 | 当前页面:开发内功修炼@张彦飞 » 如何正确查看线上半/全连接队列溢出情况?