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,也没有琢磨过内部数据交互过程.现在有一种揉碎了再重新组合,更加清晰的感觉.把三次握手和这些函数调用真正有机理解联系起来了!”
好了,回头看下本章开头提到的问题.
-
为什么服务端程序都需要先
listen一下? 内核在响应listen的时候创建了半连接、全连接两个队列,这两个队列是三次握手中很重要的数据结构,有了它们服务器才能正常响应来自客户端的三次握手.所以服务器提供服务前都需要先listen一下才行. -
半连接队列和全连接队列长度如何决定? 服务器在执行
listen的时候确定好了半连接队列和全连接队列的长度.- 对于半连接队列来说,其最大长度是
min(backlog, somaxconn, tcp_max_syn_backlog) + 1再上取整到 2 的幂次,但最小不能小于 16.如果需要加大半连接队列长度,那么需要一并考虑backlog、somaxconn和tcp_max_syn_backlog. - 对于全连接队列来说,其最大长度是
listen时传入的backlog和net.core.somaxconn之间较小的那个值.如果需要加大全连接队列长度,那么就是调整backlog和somaxconn.
- 对于半连接队列来说,其最大长度是
-
“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状态连接数量等等方法都是可行的. -
一个客户端端口可以同时用在两条连接上吗?
connect在选择端口的时候如果端口没有被用过那么就是可用的.但是如果被用过了并不是说这个端口就不能用了,这个可能有点出乎大多数人的意料. 如果用过了,接下来进一步判断新连接和老连接四元组是否完全一致,如果不完全一致该端口仍然可用.例如5000这个端口号是完全可以用于下面两条不同的连接上的.连接1:192.168.1.101 5000 192.168.1.100 8090 连接2:192.168.1.101 5000 192.168.1.100 8091所有在保证四元组不相同的情况下,一个端口完全是可以用在两条,甚至是更多条的连接上的.
-
服务端半/全连接队列满了会怎么样? 服务器响应第一次握手的时候,会进行半连接队列和全连接队列满的判断.
- 如果半连接队列满了,且未开启
tcp_syncookies,那么该握手包将直接被丢弃,所以建议不要关闭tcp_syncookies这个内核参数. - 如果全连接队列满了,且有
young_ack(表示刚刚有 SYN 到达)的话,那么同样也是直接丢弃. 服务器响应第三次握手的时候,还会再次判断全连接队列是否满.如果满了,同样丢弃握手请求. 无论是哪种丢弃发生,肯定是会影响线上服务的.当收不到预期的握手或者响应包的时候,重传定时器会在最短 1 秒后发起重试.这样接口响应的耗时最少就得 1 秒起步了.如果重试也没握手成功,很有可能就会报超时了.
- 如果半连接队列满了,且未开启
-
新连接的 socket 内核对象是什么时候建立的? 内核其实在第三次握手完毕的时候就创建好了最核心的
sock内核对象了.在用户进程调用accept的时候,直接将该对象取出来,再包装个socket对象就返回了. -
一条 TCP 连接建立需要消耗多长的时间? 一般网络的 RTT 值根据服务器物理距离的不同大约是在零点几秒、几十毫秒之间.这个时间要比 CPU 本地的系统调用耗时要长的多.所以正常情况下,在客户端或者是服务器端看来,都基本上约等于一个 RTT.但是如果一旦出现了丢包,无论是哪种原因,需要重传定时器来接人的话,那耗时就最少得 1 秒以上了(在一些老版本下得 3 秒).
-
把服务器部署在北京,给纽约的用户访问可行吗? 正常情况下一条 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] 图片内容主要涉及内核队列数据结构、端口选择流程、三次握手状态迁移图、系统调用关系图等,具体细节已在文本中详细描述。