/ tornado

tornado未处理已关闭的连接

应用架构

nginx作为反向代理工作在客户端和后端服务之间:

  • 客户端的请求来到nginx
  • nginx再把请求转发到后端
  • 最终把后端的结果送回给客户端完成一次交互

问题

最近总是遇到棘手的问题,上周主要解决Queue和Redis,这周又遇到老问题——一个接口服务偶尔响应变慢,导致使用方积压大量请求,严重影响业务正常运行。但每次遇到这个问题,重启一遍接口服务就正常。

猜测是接口连接的数据库变慢导致接口运行变慢,这很直接,但在接口变慢的时候,单独测试数据库,非常快。

接口连接的数据库是Oracle,驱动是oracle instant client,接口用的Python库是cx_Oracle,测试发现我们调用了cx_Oracle库提供的ping方法来检测连接是否正常,如果此时网络断掉,ping会一直阻塞住直到网络恢复才返回。测试方法是:

  • 正常建立连接,调用ping,未出现异常;
  • close连接,再调用ping,出现了异常;
  • 但如果未close连接,直接拔掉网线(切断网络),调用ping,一直阻塞住,插上网线(恢复网络),ping返回,未出现异常。

~~这个方法不能传timeout导致在这种情况下不能用。

~~问题找到了,但如果网络恢复了,服务也应该恢复正常才对,现在服务变得很慢,难以恢复。

~~变慢时查看过netstat,可以看到有不少close_wait、不少Recv-Q都有数值而不是0Recv-Q不为0表示有不少连接的数据未收下;close_wait表示对方主动断开连接,但这边还未做出进一步动作,所以停留在close_wait状态。

连接未断开时网络遭到破坏,例如网线被拔,调用需要联网的操作会触发tcp重传,重传是退活机制,我的机器重传16次后抛出连接超时异常,大约14分钟。

nginx的问题?

由于是我们的另一个系统调用接口服务,可以很容易调试、排查问题。

调用方在调用时设置了两个timeout,一个是connect timeout,一个是read timeout,如果和接口服务建立socket超过一个时间还未成功,那么会抛出异常;如果建立了,但在read timeout秒之后还会得到任何数据,也会抛出异常。

在出问题时,调用方出现的都是read timeout

我们思考调用方遇到错误的过程:在等待了read timeout秒之后还未得到任何数据,那么断开连接,这边抛出异常,结束。因为中间有nginx作为反向代理,此时断开的是调用方和nginx之间的连接。

nginx还会和后端服务建立连接获取结果,我们猜想是不是nginx在获知客户端与自己断开连接后没有让自己和后端服务之间的连接断开?

nginx有一个配置proxy_ignore_client_abort,默认off,即nginx不会ignore客户端关闭连接,也就是说,客户端关闭了和nginx连接,nginx也会关闭和后端服务的连接,这其实是我们期望的,如果是这样,那为何在后端服务这里还能看到很多Recv-Qclose_wait呢?

目前只能猜测tornado未处理已关闭的连接,虽然我们不想这样猜,但只能看tornado源码了。

问题根源

nginx配置了proxy_next_upstream error,在遇到error时nginx会尝试下一个upstream,为了防止nginx频繁尝试,添加了proxy_next_upstream_tries 2

主要原因在于tornado的handler执行太慢,慢到让调用方达到read timeout而主动断开连接,从而nginx也断开,但tornado并未因为nginx和自己断开而不再处理handler,事实上tornado会不会执行handler得看连接在什么时候断开。

解决方法:在执行handler之前检查连接是否正常,如果确定连接已断开,则不执行handler。

判断连接是否正常不能检查request上的connection或者iostream是否关闭,因为可能tornado已经处理到了某个不会再需要从连接上read数据的地方。只能检查iostream上的socket状态。

def is_socket_abnormal(sock):
    try:
        chunk = sock.recv(1, socket.MSG_PEEK)
    except (socket.error, IOError, OSError) as e:
        if isinstance(e, socket.error) and e.args[0] in _ERRNO_WOULDBLOCK:
            return False
        if errno_from_exception(e) == errno.EINTR:
            return False
        return True
    else:
        if not chunk:
            return True
        return False

检查方法是从socket上读一个字节,在非阻塞socket上读取数据,如果连接正常但没有数据可读,会遇到EAGAIN或EWOULDBLOCK异常;如果连接不正常,会读到空。使用MSG_PEEK可使得其他程序读取时仍然从之前位置开始读,而不会因为这里读取了1字节,从下一字节位置开始读。