大家好,我是飞哥!
在《深入解析常见三次握手异常》 这一文中,我们讨论到如果发生连接队列溢出而丢包的话,会导致连接耗时会上涨很多。 那如何判断一台服务器当前是否有半/全连接队列溢出丢包发生呢?
我在我早期的一篇文章里提到过,可以通过 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
关注公众号:微信扫描下方二维码