【网络】关于应用网络层面的问题排查

一、背景


涉及到网络层面的问题一般都比较复杂,场景多,定位难,成为了大多数开发的噩梦,应该是最复杂的了。

这里会举一些例子,并从tcp层、应用层以及工具的使用等方面进行阐述。


二、现象


1.超时

超时错误大部分处在应用层面,所以这块着重理解概念。

超时大体可以分为连接超时读写超时,某些使用连接池的客户端框架还会存在获取连接超时空闲连接清理超时


读写超时。readTimeout/writeTimeout,有些框架叫做so_timeout或者socketTimeout,均指的是数据读写超时。注意这边的超时大部分是指逻辑上的超时。soa的超时指的也是读超时。读写超时一般都只针对客户端设置。

连接超时。connectionTimeout,客户端通常指与服务端建立连接的最大时间。服务端这边connectionTimeout就有些五花八门了,jetty中表示空闲连接清理时间,tomcat则表示连接维持的最大时间。

其他,包括连接获取超时connectionAcquireTimeout和空闲连接清理超时idleConnectionTimeout。多用于使用连接池或队列的客户端或服务端框架。


我们在设置各种超时时间中,需要确认的是尽量保持客户端的超时小于服务端的超时,以保证连接正常结束。

在实际开发中,我们关心最多的应该是接口的读写超时了。

如何设置合理的接口超时是一个问题。如果接口超时设置的过长,那么有可能会过多地占用服务端的tcp连接。而如果接口设置的过短,那么接口超时就会非常频繁。

服务端接口明明rt降低,但客户端仍然一直超时又是另一个问题。

这个问题其实很简单,客户端到服务端的链路包括网络传输、排队以及服务处理等,每一个环节都可能是耗时的原因。


2.TCP队列溢出

tcp队列溢出是个相对底层的错误,它可能会造成超时、rst等更表层的错误。

因此错误也更隐蔽,所以我们单独说一说。



如上图所示,这里有两个队列:syns queue(半连接队列)accept queue(全连接队列)

三次握手,在server收到client的syn后,把消息放到syns queue,回复syn+ack给client;

server收到client的ack,如果这时accept queue没满,那就从syns queue拿出暂存的信息放入accept queue中,否则按tcp_abort_on_overflow指示的执行:

tcp_abort_on_overflow 0表示如果三次握手第三步的时候accept queue满了那么server扔掉client发过来的ack。

tcp_abort_on_overflow 1则表示第三步的时候如果全连接队列满了,server发送一个rst包给client,表示废掉这个握手过程和这个连接,意味着日志里可能会有很多connection reset / connection reset by peer。


那么在实际开发中,我们怎么能快速定位到tcp队列溢出呢?

netstat命令,执行netstat -s | egrep "listen|LISTEN"

如上图所示:

overflowed表示全连接队列溢出的次数

sockets dropped表示半连接队列溢出的次数。


ss命令,执行ss -lnt

上面看到Send-Q 表示第三列的listen端口上的全连接队列最大为32768,第一列Recv-Q为全连接队列当前使用了多少。


接着我们看看怎么设置全连接半连接队列大小:

全连接队列的大小取决于min(backlog, somaxconn)。backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数。而半连接队列的大小取决于max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。

在日常开发中,我们往往使用servlet容器作为服务端,所以我们有时候也需要关注容器的连接队列大小。在tomcat中backlog叫做acceptCount,在jetty里面则是acceptQueueSize


3.RST异常

TCP有6种表示位: SYN(建立联机) ACK(确认) PSH(传送) FIN(结束) RST(重置) URG(紧急)


RST包表示连接重置,用于关闭一些无用的连接,通常表示异常关闭,区别于四次挥手。

在实际开发中,我们往往会看到connection reset / connection reset by peer错误,这种情况就是RST包导致的。


那么有哪些原因可能导致RST呢?


目标端口不存在

如果像对不存在的端口发出建立连接SYN请求,那么服务端发现自己并没有这个端口则会直接返回一个RST报文,用于中断连接。


主动用RST代替FIN终止连接

一般来说,正常的连接关闭都是需要通过FIN报文实现,然而我们也可以用RST报文来代替FIN,表示直接终止连接。

实际开发中,可设置SO_LINGER数值来控制,这种往往是故意的,来跳过TIMED_WAIT,提供交互效率,不闲就慎用。


客户端或服务端有一边发生了异常,该方向对端发送RST以告知关闭连接。

我们上面讲的tcp队列溢出发送RST包其实也是属于这一种。这种往往是由于某些原因,一方无法再能正常处理请求连接了(比如程序崩了,队列满了),从而告知另一方关闭连接。


接收到的TCP报文不在已知的TCP连接内

比如,一方机器由于网络实在太差TCP报文失踪了,另一方关闭了该连接,然后过了许久收到了之前失踪的TCP报文,但由于对应的TCP连接已不存在,那么会直接发一个RST包以便开启新的连接。


一方长期未收到另一方的确认报文,在一定时间或重传次数后发出RST报文

这种大多也和网络环境相关了,网络环境差可能会导致更多的RST报文。


RST报文多会导致程序报错,在一个已关闭的连接上读操作会报connection reset,而在一个已关闭的连接上写操作则会报connection reset by peer。

通常我们可能还会看到broken pipe错误,这是管道层面的错误,表示对已关闭的管道进行读写,往往是在收到RST,报出connection reset错后继续读写数据报的错,这个在glibc源码注释中也有介绍。


我们在排查故障时候怎么确定有RST包的存在呢?

当然是使用tcpdump命令进行抓包,并使用wireshark进行简单分析了。

tcpdump   -i en0   tcp  -w  xxx.cap,en0表示监听的网卡。


接下来我们通过wireshark打开抓到的包,可能就能看到如下图所示,红色的就表示RST包了。



TIME_WAIT和CLOSE_WAIT

TIME_WAIT和CLOSE_WAIT是啥意思相信大家都知道。

在线上时,我们可以直接用命令netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'来查看time-wait和close_wait的数量,用ss命令会更快ss -ant | awk '{++S[$1]} END {for(a in S) print a, S[a]}'


# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# ss -ant | awk '{++S[$1]} END {for(a in S) print a, S[a]}'

命令"netstat -na"查看到的相关TCP状态解释:

LISTEN:       监听来自远方的TCP端口的连接请求;

SYN-SENT:     在发送连接请求后等待匹配的连接请求;

SYN-RECEIVED: 在收到和发送一个连接请求后等待对方对连接请求的确认;

ESTABLISHED:  代表一个打开的连接;

FIN-WAIT-1:   等待远程TCP连接中断请求, 或先前的连接中断请求的确认;

FIN-WAIT-2:   从远程TCP等待连接中断请求;

CLOSE-WAIT:   等待从本地用户发来的连接中断请求;

LAST-ACK:     等待原来的发向远程TCP的连接中断请求的确认;

TIME-WAIT:    等待足够的时间以确保远程TCP接收到连接中断请求的确认;

CLOSED:        没有任何连接状态;


TCP四次挥手

下面简单解释下什么是TIME-WAIT和CLOSE-WAIT ?

通常来说要想解决问题,就要先理解问题。

有时遇到问题,上网百度个解决方案,临时修复了问题,就以为问题已经不在了, 其实问题不是真的不存在了,而是可能隐藏在更深的地方,只是我们没有发现,或者以现有自己的的知识水平无法发现而已。

众所周知,由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的:

1) 主动关闭连接的一方,调用close();协议层发送FIN包,进入入FIN_WAIT_1状态 ;

2) 被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方等待被动关闭一方的应用程序调用close操作 ;

3) 被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态;

4) 主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态 ;

5) 等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态 ;

通过上面的一次socket关闭操作,可以得出以下几点:

1) 主动关闭连接的一方 – 也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态 ;

2) 被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接 ;

3) TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;

4) 在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的!


所以说这里凭直觉看,TIME_WAIT并不可怕,CLOSE_WAIT才可怕,因为CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;

要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。


那么这里又出现两个问题:

1) 上面提到的连接重用,那连接到底是个什么概念?

2) 协议层为什么要设计一个TIME_WAIT状态?这个状态为什么默认等待2MSL时间才会进入CLOSED?


4分钟就是2个MSL,每个MSL是2分钟。MSL就是maximium segment lifetime——最长报文寿命。

这个时间是由官方RFC协议规定的。至于为什么是2个MSL而不是1个MSL,我还没有看到一个非常满意的解释。

有读者提醒,RFC里建议的MSL其实是2分钟,但是很多实现都是30秒。


TIME_WAIT

time_wait的存在一是为了避免丢失的数据包被后面连接复用,二是为了在2MSL的时间范围内正常关闭连接。

它的存在其实会大大减少RST包的出现。

过多的time_wait在短连接频繁的场景比较容易出现。这种情况可以在服务端做一些内核参数调优:


#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭

net.ipv4.tcp_tw_reuse = 1

#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭

net.ipv4.tcp_tw_recycle = 1


当然我们不要忘记在NAT环境下因为时间戳错乱导致数据包被拒绝的坑了,另外的办法就是改小tcp_max_tw_buckets,超过这个数的time_wait都会被干掉,不过这也会导致报time wait bucket table overflow的错。


CLOSE_WAIT

close_wait往往都是因为应用程序写的有问题,没有在ACK后再次发起FIN报文。

close_wait出现的概率甚至比time_wait要更高,后果也更严重。

往往是由于某个地方阻塞住了,没有正常关闭连接,从而渐渐地消耗完所有的线程。

想要定位这类问题,最好是通过jstack分析线程堆栈来排查问题,具体可参考上述章节。


开发同学说应用上线后CLOSE_WAIT就一直增多,直到挂掉为止,jstack后找到比较可疑的堆栈是大部分线程都卡在了countdownlatch.await方法,找开发同学了解后得知使用了多线程但是却没有catch异常,修改后发现异常仅仅是最简单的升级sdk后常出现的class not found。




三、参考


TCP的6种标志位

https://blog.csdn.net/wuanwujie/article/details/71439551


JAVA线上故障排查全套路

https://fredal.xin/java-error-check


跟着动画来学习TCP三次握手和四次挥手

https://juejin.cn/post/6844903625513238541


TCP协议:三次握手过程详解

https://baijiahao.baidu.com/s?id=1618114723935605183&wfr=spider&for=pc


计算机网络基础(三次握手|TCP/IP协议|五层协议栈|网络安全)

https://blog.csdn.net/weixin_36474809/article/details/97539457


TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

https://blog.csdn.net/kexuanxiu1163/article/details/107148025


TCP连接的TIME_WAIT和CLOSE_WAIT 状态解说

https://www.cnblogs.com/kevingrace/p/9988354.html


记一次 TCP 全队列溢出问题排查过程

https://blog.csdn.net/weixin_43970890/article/details/111866688


TCP连接状态与2MSL等待时间

https://www.cnblogs.com/mddblog/p/4565562.html


TCP协议详解

https://www.cnblogs.com/qdhxhz/p/10267932.html

https://www.cnblogs.com/qdhxhz/p/8470997.html


再谈 TCP 的 CLOSE_WAIT

https://www.easyice.cn/archives/320

你可能感兴趣的:(【网络】关于应用网络层面的问题排查)