记一次线上Bug的查找过程和思路

前言

简单说下问题情况,Proxy即作为客户端又有服务端功能,其接受QS(Query Server)的请求,之后向BS(索引服务)发送请求,然后根据BS的返回结果Merge后返回给QS。不能泄露太多东西,所以本文主要是整理一些知识点和问题查找思路。

问题和现象:

  • Proxy机器上出现大量的CLOSE_WAIT。
  • Proxy失去服务能力,内存使用不为0,CPU使用率为0。
  • 机器报警,TcpListenOverFlows。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
命令查看:
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
结果:
TIME_WAIT 31927
CLOSE_WAIT 1570
ESTABLISHED 40
TIME_WAIT:表示主动关闭,通过优化系统内核参数可容易解决。
CLOSE_WAIT:表示被动关闭,需要从程序本身出发。
ESTABLISHED:表示正在通信
注:以上数据非真实线上情况,只为举例

一些知识点

TCP握手图



TIME_WAIT:

Linux系统下,TCP/IP连接断开后,会以TIME_WAIT状态保留一定的时间(2MSL:max segment lifetime),默认为4分钟,然后才会关闭回收资源。当并发请求过多的时候,就会产生大量的 TIME_WAIT状态的连接,无法及时断开的话,会占用大量的端口资源和服务器资源。
状态保持2MSL的原因:
1.防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)
2.可靠的关闭TCP连接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短时间内接受大量请求或者受到攻击。
一些解决方法:
通过修改/etc/sysctl.conf文件,服务器能够快速回收和重用那些TIME_WAIT的资源 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
#表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间
net.ipv4.tcp_fin_timeout=30
生效命令:
/sbin/sysctl -p

CLOSE_WAIT:

CLOSE_WAIT表示被动关闭,出现这种问题,基本是就是客户端连接异常或者自己没有迅速回收资源。
什么情况下,连接处于CLOSE_WAIT状态呢?

  • 在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。通常来讲,CLOSE_WAIT状态的持续时间应该很短,正如SYN_RCVD状态。但是在一些特殊情况下,就会出现连接长时间处于CLOSE_WAIT状态的情况。
  • 出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙与读或者写,没有关闭连接(CLOSE_WAIT应该是默认2小时才会关闭)。代码需要判断socket,一旦读到0,断开连接,read返回负,检查一下errno,如果不是AGAIN,就断开连接。
  • scoket阻塞,无超时或非阻塞处理。

EPOLL:

  • EPOLLIN:连接,对端发送普通数据,对端socket正常关闭。
  • EPOLLPRI:带外数据,文件描述符有紧急的数据可读。
  • EPOLLOUT:数据可写。
  • 对端正常关闭(close(),shell下kill或ctr+c),触发EPOLLIN和EPOLLRDHUP,但是不触发EPOLLERR和EPOLLHUP。//重要
  • 对端异常断开连接(如网线),不会触发任何事件。判断方式是向已经断开的socket写或者读,会发生EPOLLERR错误。

socket接口的几个知识点:

  • read总是在接收缓冲区有数据时立即返回,而不是等到给定的read buffer填满时返回。
    只有当receive buffer为空时,blocking模式才会等待,而nonblock模式下会立即返回-1(errno = EAGAIN或EWOULDBLOCK)
  • blocking的write只有在缓冲区足以放下整个buffer时才返回(与blocking read并不相同)
    nonblock write则是返回能够放下的字节数,之后调用则返回-1(errno = EAGAIN或EWOULDBLOCK)
    对于blocking的write有个特例:当write正阻塞等待时对面关闭了socket,则write则会立即将剩余缓冲区填满并返回所写的字节数,再次调用则write失败(connection reset by peer)

Bug查找

通过以上的知识点,我们就可以很好的定位问题原因了,因为Proxy即作为客户端,又有服务端功能,其转发QS的结果到BS之后,再将BS的结果merge后回复给QS。仔细排查代码,发现2处引起Proxy大量出现CLOSE_TIMEWAIT。

<1>、Proxy将BS的结果merge后,发送QS时,socket是阻塞的,也未做超时处理。

<2>、QS超时(3S),会主动关闭socket,而Proxy这层没有关闭fd,可能原作者认为会触发EPOLLERR和EPOLLHUP(这里面做了处理)。

参考链接:

1、http://www.cnblogs.com/promise6522/archive/2012/03/03/2377935.html
2、http://originlee.com/2015/04/22/nonblocking-tcp-socket/