TCP SYN队列与Accept队列详解

  尽信书,不如无书。

  纸上得来终觉浅,绝知此事要躬行。

  实验现象依赖于系统(如下)以及内核参数(附录);一切以实验结果为准。

cat /proc/version
Linux version 3.10.0-693.el7.x86_64

引子

  线上服务(Golang)调用内网API服务(经由内网网关/Nginx转发)时,偶尔会出现"connection reset by peer"报警;为此梳理TCP RST包可能产生的几种情况:

  • 目的主机防火墙拦截;
  • 向已关闭的socket发送数据;
  • 全连接队列溢出;
  • 向已经"消逝"的连接发送数据。

  情况说明:Golang服务作为客户端,内网网关Nginx作为服务端,HTTP请求默认基于长连接(连接池)。

  情况1,非常容易理解;同机房内网环境,基本可以排除。这里不做过多介绍。下面将详细介绍情况2/3/4。

Nginx关闭连接

  Golang服务通过长连接向网关Nginx发起请求;当Nginx主动断开连接,而恰好很不幸的此时Golang发起HTTP请求q并且复用了之前的长连接,便会出现情况2。那么什么时候Nginx会主动断开长连接呢?

  1)keepalive_timeout:设置每个TCP长连接在Nginx可以保持的最大时间,默认75秒;

  2)keepalive_requests:设置每个TCP长连接最多可以处理的请求数,默认100;

  Golang目前有这几个措施应对连接关闭情况:1)底层检测连接关闭事件,标记连接不可用;2)ECONNRESET错误时,对部分请求进行重试,比如:GET请求,请求头中出现{X-,}Idempotency-Key。当然实际判断是否重试逻辑还是比较复杂的;

+Transport.roundTrip
    +persistConn.shouldRetryRequest
        +RequestisReplayable
        
func (r *Request) isReplayable() bool {
	if r.Body == nil || r.Body == NoBody || r.GetBody != nil {
		switch valueOrDefault(r.Method, "GET") {
		case "GET", "HEAD", "OPTIONS", "TRACE":
			return true
		}
		
		if r.Header.has("Idempotency-Key") || r.Header.has("X-Idempotency-Key") {
			return true
		}
	}
	return false
}

  Transport.IdleConnTimeout可配置空闲连接超时时间;然而他与Nginx配置keepalive_timeout含义不同,因此无法保证Golang客户端主动关闭连接;

  另外,也可以通过短连接方式避免。

  Golang net/http库还有待深入研究。

  参考资料:

  • https://zhuanlan.zhihu.com/p/88356559
  • https://github.com/golang/go/issues/22158

SYN Queue与Accept Queue介绍

  如下图所示(摘抄自网络),1)server端接受到SYN请求,创建socket,存储于SYN Queue(半连接队列),并向客户端返回SYN+ACK;2)server端接收到第三次握手的ACK,socket状态更新为ESTABLISHED,同时将socket移动到Accept Queue(全连接队列),等待应用程序执行accept()。

image

  不管是SYN Queue还是Accept Queue,都有最大长度限制,超过限制时,内核或直接丢弃,或返回RST包。Queue大小计算方法如下:

  注:下文使用的backlog指调系统用listen(fd, backlog) 的第二个参数。

  • Accept Queue:

  min(backlog, net.core.somaxconn)

  校验Accept Queue是否满的逻辑如下(注意大于号才返回ture,即最终可存储socket数目会加1):

return sk->sk_ack_backlog > sk->sk_max_ack_backlog
  • SYN Queue:
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
nr_table_entries = max_t(u32, nr_table_entries, 8);
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
//向上取满足2的指数倍的整数;比如10=》16

for (lopt->max_qlen_log = 3;
     (1 << lopt->max_qlen_log) < nr_table_entries;
     lopt->max_qlen_log++);

  程序中的nr_table_entries初始值为min(backlog, net.core.somaxconn);sysctl_max_syn_backlog即内核参数net.ipv4.tcp_max_syn_backlog;变量lopt->max_qlen_log限制了SYN Queue大小。

  需要注意的,变量lopt->max_qlen_log的类型为u8(8比特无符号整型),最终SYN Queue大小为2^(lopt->max_qlen_log),其上限为roundup_pow_of_two(sysctl_max_syn_backlog + 1),下限为16。

  校验SYN Queue是否满的逻辑如下(qlen为当前SYN Queue长度,通过右移运算符判断):

return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;

  小知识

  可通过netstat或者ss命令查看socket信息;socket处于监听LISTEN状态时,Send-Q为Accept Queue最大长度,Recv-Q为Accept Queue累计的等待应用程序accept()的socket数目。(而当socket处于ESTABLISHED状态时,Send-Q与Recv-Q分别表示socket发送缓冲区与接收缓冲区数据大小)

# ss -lnt
State       Recv-Q Send-Q   Local Address:Port  Peer Address:Port
LISTEN      0      128          *:10088            *:*

SYN Queue

  那么当SYN Queue溢出时,服务端是怎么处理呢?丢弃还是回复RST包?我们将从实验验证与源码分析两个角度讲解。

SYN Queue溢出实验

  我们利用hping3模拟SYN发包(需要注意的是,在利用hping3模拟时,客户端收到SYN+ACK会返回RST;本文通过iptables -A INPUT -s $ip -j DROP拦截服务端返回数据包,消除了客户端RST包影响)。服务端启动监听(此时SYN Queue限制为16):

sock=socket(AF_INET, SOCK_STREAM)
sock.bind(('', 8888
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值