watch ‘netstat -s | grep overflowed’

198 times the listen queue of a socket overflowed


#### 6.6.2 半连接队列溢出判断

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

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

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

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

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

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

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

//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 的数量.

在 6.2 小节中我们讨论了半连接队列的实际长度怎么计算.如果 SYN_RECV 状态的连接数量达到你算出来的队列长度了,那么可以确定是有半连接队列溢出了.如果想加大半连接队列的长度,方法我们在上面小节里也一并讲过了.

作者信息

欢迎大家加入我的知识星球,也欢迎加入我的技术交流群 Github:https://github.com/yanfeizhang/coder-kung-fu

6.6.3 总结

简单总结一下.

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

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

至于如何加大半连接队列长度,参考 6.2 这一节.

# netstat -antp | grep SYN_RECV 
256
# watch 'netstat -s | grep overflowed'
  198 times the listen queue of a socket overflowed
# watch 'netstat -s | grep "SYNs"'
  258209 SYNs to LISTEN sockets dropped 
# netstat -antp | grep SYN_RECV | wc -l
5 

6.7 本章总结

本章中,深入分析了三次握手的内部细节:半连接队列全连接队列的创建与长度限制、客户端端口的选择、半连接队列的添加与删除、全连接队列的添加与删除以及重传定时器的启动.也分析了一些经常在线上出现的 TCP 握手问题,我们也给出了优化建议.

这里引用一位读者的评语:“编程多年但原先就知道 socket、listen、accept,也没有琢磨过内部数据交互过程.现在有一种揉碎了再重新组合,更加清晰的感觉.把三次握手和这些函数调用真正有机理解联系起来了!”

好了,回头看下本章开头提到的问题.

  1. 为什么服务端程序都需要先 listen 一下? 内核在响应 listen 的时候创建了半连接、全连接两个队列,这两个队列是三次握手中很重要的数据结构,有了它们服务器才能正常响应来自客户端的三次握手.所以服务器提供服务前都需要先 listen 一下才行.

  2. 半连接队列和全连接队列长度如何决定? 服务器在执行 listen 的时候确定好了半连接队列和全连接队列的长度.

    • 对于半连接队列来说,其最大长度是 min(backlog, somaxconn, tcp_max_syn_backlog) + 1 再上取整到 2 的幂次,但最小不能小于 16.如果需要加大半连接队列长度,那么需要一并考虑 backlogsomaxconntcp_max_syn_backlog.
    • 对于全连接队列来说,其最大长度是 listen 时传入的 backlognet.core.somaxconn 之间较小的那个值.如果需要加大全连接队列长度,那么就是调整 backlogsomaxconn.
  3. “Cannot assign requested address” 这个报错你知道是怎么回事吗,该如何解决? 一条 TCP 连接由一个四元组构成:Server IP、Server PORT、Client IP、Client Port.在连接建立前,前面的三个元素基本是确定了的,只有 Client Port 是需要动态选择出来的. 客户端会在 connect 发起的时候自动选择端口号.具体的选择过程就是随机地从 ip_local_port_range 选择一个位置开始循环判断,跳过 ip_local_reserved_ports 里设置要规避的端口,然后挨个判断是否可用.如果循环完也没有找到可用端口,会报错 “Cannot assign requested address”. 理解了这个报错的原理,解决这个问题的办法就多的很了.比如扩大可用端口范围、减小最大 TIME_WAIT 状态连接数量等等方法都是可行的.

  4. 一个客户端端口可以同时用在两条连接上吗? connect 在选择端口的时候如果端口没有被用过那么就是可用的.但是如果被用过了并不是说这个端口就不能用了,这个可能有点出乎大多数人的意料. 如果用过了,接下来进一步判断新连接和老连接四元组是否完全一致,如果不完全一致该端口仍然可用.例如 5000 这个端口号是完全可以用于下面两条不同的连接上的.

    连接1:192.168.1.101 5000 192.168.1.100 8090
    连接2:192.168.1.101 5000 192.168.1.100 8091

    所有在保证四元组不相同的情况下,一个端口完全是可以用在两条,甚至是更多条的连接上的.

  5. 服务端半/全连接队列满了会怎么样? 服务器响应第一次握手的时候,会进行半连接队列和全连接队列满的判断.

    • 如果半连接队列满了,且未开启 tcp_syncookies,那么该握手包将直接被丢弃,所以建议不要关闭 tcp_syncookies 这个内核参数.
    • 如果全连接队列满了,且有 young_ack(表示刚刚有 SYN 到达)的话,那么同样也是直接丢弃. 服务器响应第三次握手的时候,还会再次判断全连接队列是否满.如果满了,同样丢弃握手请求. 无论是哪种丢弃发生,肯定是会影响线上服务的.当收不到预期的握手或者响应包的时候,重传定时器会在最短 1 秒后发起重试.这样接口响应的耗时最少就得 1 秒起步了.如果重试也没握手成功,很有可能就会报超时了.
  6. 新连接的 socket 内核对象是什么时候建立的? 内核其实在第三次握手完毕的时候就创建好了最核心的 sock 内核对象了.在用户进程调用 accept 的时候,直接将该对象取出来,再包装个 socket 对象就返回了.

  7. 一条 TCP 连接建立需要消耗多长的时间? 一般网络的 RTT 值根据服务器物理距离的不同大约是在零点几秒、几十毫秒之间.这个时间要比 CPU 本地的系统调用耗时要长的多.所以正常情况下,在客户端或者是服务器端看来,都基本上约等于一个 RTT.但是如果一旦出现了丢包,无论是哪种原因,需要重传定时器来接人的话,那耗时就最少得 1 秒以上了(在一些老版本下得 3 秒).

  8. 把服务器部署在北京,给纽约的用户访问可行吗? 正常情况下一条 TCP 连接建立耗时是双端网络一次 RTT 时间.那么如果服务器在北京,用户在美国,那么这个 RTT 是多少呢?后续可参考系统调优参数配置.

调优参考配置

以下配置可用于调整客户端端口范围及 TIME_WAIT 数量限制,避免端口耗尽或过多 TIME_WAIT 连接积压.

# vi /etc/sysctl.conf
# 修改可用端口范围
net.ipv4.ip_local_port_range = 5000 65000 
# 设置最大 TIME_WAIT 数量
net.ipv4.tcp_max_tw_buckets = 10000 
# sysctl -p

原文档图片说明

原文档中包含以下图片,此处以文字形式保留其索引:

  • [Image 17 on Page 150]
  • [Image 61 on Page 152]
  • [Image 644 on Page 154]
  • [Image 645 on Page 154]
  • [Image 663 on Page 159]
  • [Image 687 on Page 166]
  • [Image 691 on Page 167]
  • [Image 17 on Page 169]
  • [Image 731 on Page 179]
  • [Image 61 on Page 180]
  • [Image 739 on Page 181]
  • [Image 743 on Page 182]
  • [Image 750 on Page 184]
  • [Image 762 on Page 187]
  • [Image 766 on Page 188]
  • [Image 767 on Page 188]
  • [Image 49 on Page 194] 图片内容主要涉及内核队列数据结构、端口选择流程、三次握手状态迁移图、系统调用关系图等,具体细节已在文本中详细描述。