尽信书,不如无书。
纸上得来终觉浅,绝知此事要躬行。
实验现象依赖于系统(如下)以及内核参数(附录);一切以实验结果为准。
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()。
不管是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