美文网首页
一次HTTP connect-timeout的排查(下)

一次HTTP connect-timeout的排查(下)

作者: 小菜Yang | 来源:发表于2020-03-14 13:05 被阅读0次

回顾-上期的遗留问题

  • 疑惑1:按道理全连接队列满了,但是客户端的连接请求是已经接收到SYN+ACK了,所以对于客户端来说该连接已经建立了,为啥会报connect timeout ? 应该是read timeout或者connect reset 。
  • 疑惑2:全连接队列满了,但客户端的请求认为连接ESTABLISH状态,可以继续发送数据请求。这时候服务端如何处理?

TCP三次握手

  • client发送syn给server端。
  • server接收到client的syn,这个时候则会将相关信息放到半链接队列(syn queue),并且发送syn+ack发送给client。
  • client接受到server的syn+ack之后,发送一个ack给server告诉server我接收到了。这个时候server就会将相关信息放到全连接队列(accept queue)中。


    image.png

回顾-TCP三次握手-半连接队列和全连接队列

  1. 当 client 通过 connect 向 server 发出 SYN 包时,client 会维护一个 socket 等待队列,而 server 会维护一个 SYN 队列
  2. 此时进入半链接的状态,如果 socket 等待队列满了,server 则会丢弃,而 client 也会由此返回 connection time out;只要是 client 没有收到 SYN+ACK,3s 之后,client 会再次发送,如果依然没有收到,9s 之后会继续发送
  3. 半连接 syn 队列的长度为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog) 决定
  4. 当 server 收到 client 的 SYN 包后,会返回 SYN, ACK 的包加以确认,client 的 TCP 协议栈会唤醒 socket 等待队列,发出 connect 调用
  5. client 返回 ACK 的包后,server 会进入一个新的叫 accept 的队列,该队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128,表示最多有 129 的 ESTAB 的连接等待 accept(),而 backlog 的值则由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 的含义请看这里。
  6. 当accept 队列满了之后,即使 client 继续向 server 发送 ACK 的包,也会不被相应,此时,server 通过 /proc/sys/net/ipv4/tcp_abort_on_overflow 来决定如何返回,0 表示直接丢丢弃该 ACK,1 表示发送 RST 通知 client;相应的,client 则会分别返回 read timeout 或者 connection reset by peer。
    (在自己的测试验证过程中,实际情况还会产生:服务器会随机的忽略收到的 SYN,建立起来的连接数可以无限的增加,只不过客户端会遇到延时以及超时的情况。)

验证疑惑1:全连接队列满了情况怎么样?

  1. 启动一个TCP服务端Socket,只对端口做listen监听,不进行accept操作。(port = 9999, backlog = 2)
  2. 分别调用4次该端口(采用nc命令: nc ip port的方式)
  3. 服务器的TCP参数 cat /proc/sys/net/ipv4/tcp_abort_on_overflow = 0
    PS: 注意以下的代码,servserSocket.accpet()是注释掉的,代表不会从全连接队列中获取请求信息。


    image.png
第1次Socket连接:
13:55:31.379052 IP 10.127.4.74.56720 > 10.16.30.142.9999: Flags [S], seq 1290846985, win 8192, options [mss 1360,nop,wscale 8,nop,nop,sackOK], length 0
13:55:31.379065 IP 10.16.30.142.9999 > 10.127.4.74.56720: Flags [S.], seq 3765322754, ack 1290846986, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:31.406038 IP 10.127.4.74.56720 > 10.16.30.142.9999: Flags [.], ack 1, win 69, length 0
第2次Socket连接:
13:55:33.188479 IP 10.127.4.74.56728 > 10.16.30.142.9999: Flags [S], seq 3220053736, win 8192, options [mss 1360,nop,wscale 8,nop,nop,sackOK], length 0
13:55:33.188494 IP 10.16.30.142.9999 > 10.127.4.74.56728: Flags [S.], seq 3458261601, ack 3220053737, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:33.208279 IP 10.127.4.74.56728 > 10.16.30.142.9999: Flags [.], ack 1, win 69, length 0
第3次Socket连接:
13:55:34.974518 IP 10.127.4.74.56735 > 10.16.30.142.9999: Flags [S], seq 1314208155, win 8192, options [mss 1360,nop,wscale 8,nop,nop,sackOK], length 0
13:55:34.974539 IP 10.16.30.142.9999 > 10.127.4.74.56735: Flags [S.], seq 3817678685, ack 1314208156, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:34.999211 IP 10.127.4.74.56735 > 10.16.30.142.9999: Flags [.], ack 1, win 69, length 0
第4次Socket连接
backlog配置的2,全连接队列个数为2+1=3,
第4次开始溢出,/proc/sys/net/ipv4/tcp_abort_on_overflow 配置的为0,则系统会默认重试5次SYN+ACK(重试次数见: /proc/sys/net/ipv4/tcp_synack_retries)
13:55:36.990148 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [S], seq 713717214, win 8192, options [mss 1360,nop,wscale 8,nop,nop,sackOK], length 0
13:55:36.990170 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:37.011510 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, length 0
13:55:38.389731 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:38.401817 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
13:55:40.589773 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:40.607068 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
13:55:44.789766 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:44.804919 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
13:55:52.789749 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:55:52.804957 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0
13:56:08.789786 IP 10.16.30.142.9999 > 10.127.4.74.56739: Flags [S.], seq 2481376603, ack 713717215, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
13:56:08.847814 IP 10.127.4.74.56739 > 10.16.30.142.9999: Flags [.], ack 1, win 69, options [nop,nop,sack 1 {0:1}], length 0

验证结果:抓包的TCP DUMP 显示,在第4次的时候全连接队列发生溢出:在收到客户端的ack确认时候(时间13:55:37.011510),由于backlog已满,所以服务端直接丢弃该ACK确认,再次重试响应SYN+ACK(时间13:55:38.389731) 。反复5次以后,客户端出现read-timeout。 这个就证实了开章首提的疑惑1确实如我们所想

验证疑惑1:为啥会出现connect-timeout?

启动一个http服务,配置tomcat-acceptCount为1,并发10个线程请求进行,发现出现了一次connect timeout

#正常的三次握手
18:15:14.975879 IP 10.16.80.136.52633 > 10.16.30.142.19966: Flags [S], seq 17196608, win 8192, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
18:15:14.975907 IP 10.16.30.142.19966 > 10.16.80.136.52633: Flags [S.], seq 3788688748, ack 17196609, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
18:15:14.981048 IP 10.16.80.136.52633 > 10.16.30.142.19966: Flags [.], ack 1, win 256, length 0
#由于全队列满随机忽略不响应SYN+ACK:
18:15:14.981065 IP 10.16.80.136.52637 > 10.16.30.142.19966: Flags [S], seq 1960972950, win 8192, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
为啥会出现这种情况,查找了相关Linux资料,如下:

man listen中提到每次收到新SYN包,内核往SYN队列追加一个新连接(除非该队列已满)。但事实并非如此,net/ipv4/tcp_ipv4.c中tcp_v4_conn_request函数负责处理SYN包,请看以下代码:

if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)     
    goto drop;     
  • sk_acceptq_is_full()函数很好理解,根据字面意思就可以看出,该函数是检查连接队列是否已满
  • inet_csk_reqsk_queue_young()函数返回半连接队列中未重传过SYN+ACK段的连接请求块数量。 如果连接队列已满并且半连接队列中的连接请求块中未重传的数量大于1,则会跳转到drop处,丢弃SYN包。如果半连接队列中未重传的请求块数量大于1,则表示未来可能有2个完成的连接,这些新完成的连接要放到连接队列中,但此时连接队列已满。如果在接收到三次握手中最后的ACK后连接队列中没有空闲的位置,会忽略接收到的ACK包,连接建立会推迟,所以此时最好丢掉部分新的连接请求,空出资源以完成正在进行的连接建立过程。还要注意,这个判断并没有考虑半连接队列是否已满的问题。从这里可以看出,即使开启了SYN cookies机制并不意味着一定可以完成连接的建立。
  • 参考资料:https://blog.csdn.net/justlinux2010/article/details/12619761
    自己按照理解画了一个简图,如下:
    image.png

结论:所以正是由于inet_csk_reqsk_queue_young()函数机制的存在,所以在极端情况下,全连接队列满了,也会引起客户端出现connect-timeout的情况

验证疑惑2:全连接队列满了,客户端继续发送数据请求。这时候服务端如何处理?

这个其实就是经典的client fooling问题
由于全队列满的情况对于客户端来说是透明的,因为请求方已经收到了服务端的SYN+ACK,状态已经变为ESTABLISH,客户端认为连接是建立的,所以继续发消息,那么此种情况下(全队列满,服务端没有对应连接,客户端认为连接建立成功),客户端仍发消息会产生什情况?,试验如下:


image.png

结论:可以发现客户端发送的数据包如果没有收到服务端的ack, 客户端会自动重试几次,如果仍没收到ack,则客户端发送RST指令断开已服务端的连接。

小结

TCP握手的全连接队列、半连接队列溢出这种问题很容易被大家忽视,但是又很关键。 一旦溢出,从各项指标比如CPU, GC情况, 线程状态看都很正常,但是压力上不去,从服务端上看其他的请求的响应时间又很快(见上篇的pinpoint监控图)。
希望本文能够帮助大家理解TCP 全连接、半连接队列的概念,以及出现问题如何排查。
如文章有理解不足之处,也请大伙帮忙指出,感谢

相关文章

网友评论

      本文标题:一次HTTP connect-timeout的排查(下)

      本文链接:https://www.haomeiwen.com/subject/nxjvshtx.html