TCP SYN队列与Accept队列详解

LNMPRG源码研究

李乐

  尽信书,不如无书。

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

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

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请求并且是复用之前的长连接,便会出现情况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库还有待深入研究。

  参考资料:

SYN Queue与Accept Queue介绍

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

syn-accept.png

  不管是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))
sock.listen(1)

  记得通过netstat查看初始TCP统计信息:

# netstat -s |grep -E 'listen| resets sent| LISTEN'
    5236 resets sent //RST包发送数目
    438 times the listen queue of a socket overflowed //Accept Queue溢出数目
    2900 SYNs to LISTEN sockets dropped //三次握手过程丢弃数目

  客户端启动发包;-S设置SYN标志,-p指定目标端口号,-i设置发送间隔1000微妙(即每毫秒发送1个SYN数据包)。同时在服务端启动tcpdump抓包。

hping3 -S -p 8888 -i u1000  $ip

542 packets tramitted

  总发包数目为542;再次查看服务端
TCP统计信息:

# netstat -s |grep -E 'listen| resets sent| LISTEN'
    5236 resets sent
    438 times the listen queue of a socket overflowed
    3426 SYNs to LISTEN sockets dropped

  可以看到SYN丢弃数目增加了526=542-16(16为SYN Queue长度限制),服务端发送RST数目没有变化。

  查看tcpdump结尾的抓包情况,可以看到只有客户端的SYN请求,服务端没有给客户端返回SYN+ACK。

13:40:48.230881 IP xxxx.ms-sql-s > xxxx.8888: Flags [S], seq 340595037, win 512, length 0
13:40:48.231880 IP xxxx.ms-sql-m > xxxx.8888: Flags [S], seq 580674513, win 512, length 0
13:40:48.232920 IP xxxx.ibm-cics > xxxx.8888: Flags [S], seq 1559804617, win 512, length 0
13:40:48.233896 IP xxxx.saism > xxxx.8888: Flags [S], seq 2102270179, win 512, length 0

  SYN Queue溢出时,服务端只是丢弃客户端的SYN数据包。

tcp_syncookies

  其实还有一个内核参数tcp_syncookies可以影响SYN Queue行为。

tcp_syncookies (Boolean; since Linux 2.2)
              Enable TCP syncookies.  The kernel must be compiled with CONFIG_SYN_COOKIES.  Send out  syncookies  when  the  syn  backlog
              queue  of a socket overflows.  The syncookies feature attempts to protect a socket from a SYN flood attack.  This should be
              used as a last resort, if at all.  This is a violation of the TCP protocol, and conflicts with other areas of TCP  such  as
              TCP  extensions.   It  can  cause problems for clients and relays.  It is not recommended as a tuning mechanism for heavily
              loaded servers to help with overloaded or misconfigured conditions.  For recommended alternatives see  tcp_max_syn_backlog,
              tcp_synack_retries, and tcp_abort_on_overflow

  tcp_syncookies是一种专门防御 SYN Flood 攻击的方法,其 基于连接信息(包括源地址、源端口、目的地址、目的端口等)以及一个加密种子(如系统启动时间),计算出一个哈希值(SHA1),这个哈希值称为cookie。

  该cookie被用作TCP初始序列号,来应答SYN+ACK 包,并释放连接状态。当客户端发送完三次握手的最后一次ACK 后,服务端就会再次计算这个哈希值,确认是上次返回的 SYN+ACK 的返回包,才会进入TCP 的连接状态。

  即,开启 SYN Cookies 后,服务端就不需要维护半开连接状态了,从而也就不存在SYN Queue溢出情况了。

  是这样吗?我们来实验验证下。

  修改内核参数:

# sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1

  记录初始TCP统计信息:

# netstat -s |grep -E 'listen| resets sent| LISTEN'
    5236 resets sent
    438 times the listen queue of a socket overflowed
    4473 SYNs to LISTEN sockets dropped

  客户端启动hping3开始发送SYN包,服务端开启tcpdump抓包:

hping3 -S -p 8888 -i u1000  10.90.101.6

282 packets tramitted

  总发包数目为282;再次查看服务端 TCP统计信息:

# netstat -s |grep -E 'listen| resets sent| LISTEN'
    5236 resets sent
    438 times the listen queue of a socket overflowed
    4739 SYNs to LISTEN sockets dropped

  可以很明显的看到,SYN包丢弃的数目依然有变化,增长266=282-16。怎么回事?为什么服务端还会丢弃SYN包呢?难道tcp_syncookies与我们理解的不一致?

  但是查看服务端tcpdump抓包情况,我们发现结尾服务端依然在向客户端返回SYN+ACK(不是SYN+ACK重试包,整个实验过程非常短,而SYN+ACK重试间隔初始为1秒)

15:10:27.895666 IP xxxx.8888 > xxxx.pxc-sapxom: Flags [S.], seq 2552938291, ack 277155377, win 29200, options [mss 1460], length 0
15:10:27.895670 IP xxxx.8888 > xxxx.syncserverssl: Flags [S.], seq 132109634, ack 1320827335, win 29200, options [mss 1460], length 0
15:10:28.095641 IP xxxx.8888 > xxxx.md-cg-http: Flags [S.], seq 571952037, ack 1550190463, win 29200, options [mss 1460], length 0
15:10:28.095680 IP xxxx.8888 > xxxx.ncdloadbalance: Flags [S.], seq 3043329827, ack 1288412213, win 29200, options [mss 1460], length 0

  服务端一直应答SYN+ACK,说明这些连接请求并没有丢弃,是生效的。(另外,在使用hping3模拟大量SYN请求的同时,可以发起正常连接请求,验证是否可以正常建立连接)。

源码分析

  上述实验看到,在开启tcp_syncookies之后,依然有SYN请求丢弃发生,但是服务端却依然在反馈SYN+ACK。下面将从源码角度分析。

  在接收到SYN请求时,服务端处理逻辑如下:

+tcp_v4_do_rcv
    +tcp_v4_hnd_req
    +tcp_rcv_state_process
        +tcp_v4_conn_request

  函数tcp_v4_conn_request处理客户端连接请求,校验SYN Queue逻辑如下:

if (inet_csk_reqsk_queue_is_full(sk)) {
    want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
    if (!want_cookie)
        goto drop;
}

drop:
    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
    //统计ListenDrops

  可以看到,在SYN Queue队列溢出时,根据want_cookie处理,如果配置tcp_syncookies=1,则want_cookie=true,同时继续处理(SYN Queue长度限制失效);否则会执行drop逻辑丢弃SYN包。

//配置tcp_syncookies=1,且SYN Queue溢出,want_cookie=true

skb_synack = tcp_make_synack(sk, dst, req,
        fastopen_cookie_present(&valid_foc) ? &valid_foc : NULL);

err = ip_build_and_send_pkt(skb_synack, sk, ireq->loc_addr,
             ireq->rmt_addr, ireq->opt);
if (err || want_cookie)
    goto drop_and_free;

//添加socket信息到SYN Queue    
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
    
drop_and_free:
    reqsk_free(req);
drop:
    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
    return 0;

  可以看到,在want_cookie时(tcp_syncookies=1),跳转到drop_and_free处理(没有添加加socket信息到SYN Queue);drop标签同时累加ListenDrops。

  上文实验netstat -s |grep -E 'LISTEN'统计的数据,是从/proc/net/netstat获取,即对应ListenDrops。

  通过这两段逻辑,我们明白了tcp_syncookies的处理过程:1)SYN Queue没有溢出时,与普通流程相同;2)SYN Queue溢出时,才真正开启SYN Cookie功能,开启后会丢弃所有SYN包,同时累加ListenDrops。

  补充:

  函数tcp_syn_flood_action还会做一些统计需要我们关注下:

bool tcp_syn_flood_action(struct sock *sk,
             const struct sk_buff *skb,
             const char *proto)
{
    bool want_cookie = false;
    
    if (sysctl_tcp_syncookies) {
        want_cookie = true;
        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);
        //TCPReqQFullDoCookies,发送cookie
    } else
        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);
        //TCPReqQFullDrop,SYN丢弃
    return want_cookie;
}

  上述实验我们再执行两次:

  • 开启tcp_syncookies:
# netstat -s | grep -E 'TCPReqQFullDrop|TCPReqQFullDoCookies'
    TCPReqQFullDoCookies: 1194
    TCPReqQFullDrop: 3265
    
455 packets tramitted

# netstat -s | grep -E 'TCPReqQFullDrop|TCPReqQFullDoCookies'
    TCPReqQFullDoCookies: 1633
    TCPReqQFullDrop: 3265
    
//drop新增0;cookie新增439=455-16
  • 关闭tcp_syncookies:
# netstat -s | grep -E 'TCPReqQFullDrop|TCPReqQFullDoCookies'
    TCPReqQFullDoCookies: 1633
    TCPReqQFullDrop: 3265
    
358 packets tramitted

# netstat -s | grep -E 'TCPReqQFullDrop|TCPReqQFullDoCookies'
    TCPReqQFullDoCookies: 1633
    TCPReqQFullDrop: 3607
    
//drop新增342=358-16;cookie新增0

Accept Queue

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

Accept Queue溢出实验

  • 服务端通过如下方式启动监听,此时Accept Queue最大长度为2:
sock=socket(AF_INET, SOCK_STREAM)
sock.bind(('', 8888))
sock.listen(1)
  • 发送2个连接请求,ss查看Accept Queue统计情况,Accept Queue已达到最大长度:
# ss -lnt
State       Recv-Q Send-Q   Local Address:Port  Peer Address:Port
LISTEN      2      1          *:8888             *:*
  • netstat查看初始TCP状态统计数据:
# netstat -s |grep -E 'listen| resets sent| LISTEN | Cookies'
    5244 resets sent
    448 times the listen queue of a socket overflowed
    5896 SYNs to LISTEN sockets dropped
  • 发起新的连接请求,同时启动tcpdump抓包:

  netstat再次查看初始TCP状态统计数据:

# netstat -s |grep -E 'listen| resets sent| LISTEN | Cookies'
    5244 resets sent
    451 times the listen queue of a socket overflowed
    5899 SYNs to LISTEN sockets dropped

  可以看到,RST包发送统计没有增加;listenoverflow以及listendrop均有增加,且同时增加3。这里留个疑问,为什么会增加3呢?不是只发起一个请求吗?

  ss查看新的连接状态为SYN-RECV:

ss -nat | grep -E 'State|8888'
State      Recv-Q Send-Q Local Address:Port      Peer Address:Port
LISTEN     2      1            *:8888            *:*                   
SYN-RECV   0      0      xxxx:8888        xxxx:35453

   查看tcpdump抓包数据:

16:52:33.942358 IP xxxx.35453 > xxxx.8888: Flags [S], seq 2051886524, win 29200, length 0
16:52:33.942588 IP xxxx.8888 > xxxx.35453: Flags [S.], seq 3268637378, ack 2051886525, win 28960, length 0
16:52:33.942916 IP xxxx.35453 > xxxx.8888: Flags [.], ack 3268637379, win 58, length 0
16:52:35.345579 IP xxxx.8888 > xxxx.35453: Flags [S.], seq 3268637378, ack 2051886525, win 28960, length 0
16:52:35.345953 IP xxxx.35453 > xxxx.8888: Flags [.], ack 3268637379, win 58, length 0
16:52:37.345598 IP xxxx.8888 > xxxx.35453: Flags [S.], seq 3268637378, ack 2051886525, win 28960, length 0
16:52:37.346078 IP xxxx.35453 > xxxx.8888: Flags [.], ack 3268637379, win 58, length 0

  可以看到,再服务端接收到客户端第三次ACK之后(参照ss结果,由于Accept Queue溢出,丢弃了ACK包,连接状态依然为SYN-RECV);服务端超时后还发送了两次SYN+ACK包,客户端均应答ACK。

  通过tcpdump抓包结果可以看到,由于重试机制,服务端总共收到了三次客户端的第三次握手ACK,而三次都由于Accept Queue溢出丢弃,因此上面说的listenoverflow以及listendrop增加3。(至于为何两者同时增加,待会源码分析)。

  知识补充

  服务端的SYN+ACK重试次数,由内核参数tcp_synack_retries决定。

tcp_synack_retries (integer; default: 5; since Linux 2.2)
              The  maximum  number of times a SYN/ACK segment for a passive TCP connection will be retransmitted.  This number should not
              be higher than 255.

HTTP请求验证

  上面实验我们只是发起了连接请求,HTTP请求时,服务端丢弃第三次ACK导致连接状态为SYN-RECV,但是此时客户端状态已经为ESTABLISHED,当客户端此时传输HTTP请求数据时,会导致RST吗?

  其他同上面的实验,curl请求:

curl http://xxxx:8888/user/login
curl: (56) Recv failure: Connection timed out

  tcpdump抓包结果如下:

17:18:27.820307 IP xxxx.35515 > xxxx.8888: Flags [S], seq 3685002386, win 29200, length 0
17:18:27.820378 IP xxxx.8888 > xxxx.35515: Flags [S.], seq 1886008256, ack 3685002387, win 28960, length 0
17:18:27.820672 IP xxxx.35515 > xxxx.8888: Flags [.], ack 1886008257, win 58, length 0
//发起HTTP请求
17:18:27.820680 IP xxxx.35515 > xxxx.8888: Flags [P.], seq 3685002387:3685002477, ack 1886008257, win 58, length 90
//HTTP请求重试
17:18:28.020543 IP xxxx.35515 > xxxx.8888: Flags [P.], seq 3685002387:3685002477, ack 1886008257, win 58, length 90
//HTTP请求重试
17:18:28.220471 IP xxxx.35515 > xxxx.8888: Flags [P.], seq 3685002387:3685002477, ack 1886008257, win 58, length 90
//HTTP请求重试
17:18:28.621487 IP xxxx.35515 > xxxx.8888: Flags [P.], seq 3685002387:3685002477, ack 1886008257, win 58, length 90
//SYN+ACK重试
17:18:29.021763 IP xxxx.8888 > xxxx.35515: Flags [S.], seq 1886008256, ack 3685002387, win 28960, length 0
17:18:29.022193 IP xxxx.35515 > xxxx.8888: Flags [.], ack 1886008257, win 58, length 0
//HTTP请求重试
17:18:29.424432 IP xxxx.35515 > xxxx.8888: Flags [P.], seq 3685002387:3685002477, ack 1886008257, win 5
//SYN+ACK重试8, length 90
17:18:31.221631 IP xxxx.8888 > xxxx.35515: Flags [S.], seq 1886008256, ack 3685002387, win 28960, length 0
//客户端超时,发起RST
17:18:31.221942 IP xxxx.35515 > xxxx.8888: Flags [R], seq 3685002387, win 0, length 0

  可以看到服务端对于客户端的HTTP请求数据,并没有响应(直接丢弃);客户端连接状态为ESTABLISHED,服务端为SYN-RECV,客户端一直在重试HTTP请求,服务端一直在重试SYN+ACK。最后,客户端HTTP请求传输超时(TCP重传失败),客户端发起RST包。TCP重传失败时,上层错误信息为Connection timed out,与curl失败报错相对应。

  知识补充

  TCP数据传输重试次数由内核参数tcp_retries2决定。

tcp_retries2 (integer; default: 15; since Linux 2.2)
              The  maximum number of times a TCP packet is retransmitted in established state before giving up.  The default value is 15,
              which corresponds to a duration of approximately between 13 to 30 minutes, depending on the  retransmission  timeout.   The
              RFC 1122 specified minimum limit of 100 seconds is typically deemed too short.

tcp_abort_on_overflow

  其实,服务端Accept Queue溢出的行为还受到内核参数tcp_abort_on_overflow决定。而我们的系统配置tcp_abort_on_overflow=0。

tcp_abort_on_overflow (Boolean; default: disabled; since Linux 2.4)
              Enable resetting connections if the listening service is too slow and unable to keep up and accept them.  It means that  if
              overflow  occurred  due  to  a burst, the connection will recover.  Enable this option only if you are really sure that the
              listening daemon cannot be tuned to accept connections faster.  Enabling this option can harm the clients of your server.

  修改配置tcp_abort_on_overflow=1,重试上面实验:

# sysctl -w net.ipv4.tcp_abort_on_overflow=1
net.ipv4.tcp_abort_on_overflow = 1

  客户端curl请求立即报错:

time curl http://10.90.101.6:8888/user/login
curl: (56) Recv failure: Connection reset by peer

real    0m0.005s

  tcpdump抓包情况如下,服务端在接收到第三次握手ACK时,立即返回RST包:

17:35:02.063694 IP xxxx.35547 > xxxx.8888: Flags [S], seq 1965671248, win 29200, length 0
17:35:02.063804 IP xxxx.8888 > xxxx.35547: Flags [S.], seq 3965903705, ack 1965671249, win 28960, length 0
17:35:02.064200 IP xxxx.35547 > xxxx.8888: Flags [.], ack 3965903706, win 58, length 0
17:35:02.064228 IP xxxx.8888 > xxxx.35547: Flags [R], seq 3965903706, win 0, length 0

源码分析

  在接收到第三次握手ACK时,服务端处理逻辑如下:

+tcp_v4_do_rcv
    +tcp_v4_hnd_req
        +tcp_check_req
            +tcp_v4_syn_recv_sock

  tcp_v4_syn_recv_sock函数判断Accept Queue是否溢出:

if (sk_acceptq_is_full(sk))
    goto exit_overflow;

exit_overflow:
    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
    //统计ListenOverflows
exit_nonewsk:
    dst_release(dst);
exit:
    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENDROPS);
    //统计ListenDrops
    return NULL;

  可以看到,在溢出时,同时修改ListenOverflows以及ListenDrops。(与上面实验同时增加3相对应)。

  函数tcp_check_req根据tcp_v4_syn_recv_sock返回结果,以及tcp_abort_on_overflow,决定是否发送RST包:

child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
if (child == NULL)
    goto listen_overflow;
    
embryonic_reset:
    req->rsk_ops->send_reset(sk, skb);
    //实现函数为:tcp_v4_send_reset
    
    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_EMBRYONICRSTS);
    //统计EmbryonicRsts
    return NULL;

Accept Queue再验证

  为什么有些人在实验Accept Queue溢出时,哪怕配置的tcp_abort_on_overflow=0,依然客户端会收到RST包,这是为什么呢?其实还是与系统配置有关。

  另外,本文最开始提到向已经"消逝"的连接发送数据,同样会导致RST。

  当tcp_synack_retries配置非常小时,由于Accept Queue溢出,服务端的SYN-RECV状态很快超时,连接被释放;而客户端的tcp_retries2配置的比较大时,客户端还在一直重试发送HTTP请求,此时服务端便会返回RST包。

  修改tcp_retries2:

# sysctl -w net.ipv4.tcp_retries2=15
net.ipv4.tcp_retries2 = 15

  再次发起curl请求:

# time curl http://10.90.101.6:8888/user/login
curl: (56) Recv failure: Connection reset by peer

real    0m12.844s

  tcpdump抓包情况如下:

17:58:33.522067 IP xxxx.35603 > xxxx.8888: Flags [S], seq 2997388295, win 29200, olength 0
17:58:33.522182 IP xxxx.8888 > xxxx.35603: Flags [S.], seq 2883911494, ack 2997388296, win 28960, length 0
17:58:33.522463 IP xxxx.35603 > xxxx.8888: Flags [.], ack 2883911495, win 58, length 0
//发起HTTP请求
17:58:33.522583 IP xxxx.35603 > xxxx.8888: Flags [P.], seq 2997388296:2997388386, ack 2883911495, win 58, length 90
//HTTP请求重试
17:58:33.723351 IP xxxx.35603 > xxxx.8888: Flags [P.], seq 2997388296:2997388386, ack 2883911495, win 58, length 90
//HTTP请求重试
17:58:33.924422 IP xxxx.35603 > xxxx.8888: Flags [P.], seq 2997388296:2997388386, ack 2883911495, win 58, length 90
//HTTP请求重试
17:58:34.327366 IP xxxx.35603 > xxxx.8888: Flags [P.], seq 2997388296:2997388386, ack 2883911495, win 58, length 90
//服务端重试SYNc+ACK
17:58:34.523613 IP xxxx.8888 > xxxx.35603: Flags [S.], seq 2883911494, ack 2997388296, win 28960, length 0
17:58:34.523916 IP xxxx.35603 > xxxx.8888: Flags [.], ack 2883911495, win 58, length 0
//HTTP请求重试
17:58:35.133451 IP xxxx.35603 > xxxx.8888: Flags [P.], seq 2997388296:2997388386, ack 2883911495, win 58, length 90
//服务端重试SYNc+ACK
17:58:36.723600 IP xxxx.8888 > xxxx.35603: Flags [S.], seq 2883911494, ack 2997388296, win 28960, length 0
17:58:36.723987 IP xxxx.35603 > xxxx.8888: Flags [.], ack 2883911495, win 58, length 0
//HTTP请求重试
17:58:36.743318 IP xxxx.35603 > xxxx.8888: Flags [P.], seq 2997388296:2997388386, ack 2883911495, win 58, length 90
//HTTP请求重试
17:58:39.967405 IP xxxx.35603 > xxxx.8888: Flags [P.], seq 2997388296:2997388386, ack 2883911495, win 58, length 90
//HTTP请求重试
17:58:46.423467 IP xxxx.35603 > xxxx.8888: Flags [P.], seq 2997388296:2997388386, ack 2883911495, win 58, length 90
//服务端返回RST
17:58:46.423716 IP xxxx.8888 > xxxx.35603: Flags [R], seq 2883911495, win 0, length 0

  可以看到,在客户端TCP多次重试的过程中,服务端的连接SYN-RECV已经超时释放,导致服务端最终返回RST包。

总结

  还是那句话:尽信书,不如无书。

  很多人的实验现象,是与其系统以及内核参数息息相关。不能简简单单的认为TCP队列溢出就会导致RST或者不会RST。

  只是在本文的系统配置下,HTTP请求异常"connection reset by peer"(服务端RST)不是由TCP队列溢出导致的。

  Golang为了避免"connection reset by peer"情况,目前可以通过短链接方式避免,或者异常时重试。而本文重点介绍了TCP SYN Queue以及Accept Queue,至于Golang长连接RST情况还有待深究。

附录

net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_sack = 0
net.ipv4.tcp_retrans_collapse = 1
net.ipv4.ip_default_ttl = 64
net.ipv4.ip_nonlocal_bind = 0
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_max_orphans = 262144
net.ipv4.tcp_max_tw_buckets = 1600000
net.ipv4.ip_dynaddr = 0
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_retries1 = 3
net.ipv4.tcp_retries2 = 2
net.ipv4.tcp_fin_timeout = 5
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_abort_on_overflow = 0
net.ipv4.tcp_stdurg = 0
net.ipv4.tcp_rfc1337 = 0
net.ipv4.tcp_max_syn_backlog = 81920
net.core.somaxconn = 65535

参考资料

阅读 2.8k

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人

7k 声望
12.6k 粉丝
0 条评论

一群热爱代码的人 研究Nginx PHP Redis Memcache Beanstalk 等源码 以及一群热爱前端的人

7k 声望
12.6k 粉丝
文章目录
宣传栏