1. 问题背景

    线上服务变得卡顿。发现 open too many files错误。

2. 定位经过

   首先查看linux服务器负载是否高,CPU,MEM,磁盘读写IOPS(发现高,但是很快排除了,因为是错误日志打印太多到本地磁盘)使用netstat 查看当时的连接情况,有条件的可以看监控软件。发现整体负载不高,事后回顾,如果整体负载不高,那么应该考虑是否有系统设置的限制。

 查看linux tcp配置参数, cat/etc/sysctl.conf 或者sysctl -a,发现有人修改了net.ipv4.tcp_max_tw_buckets默认参数,有人把它改小了。当时没有留意,后面发现才是它是一种错误。为啥会把它改小。发现网上有一些人云亦云的博客,说为了提高性能,限制time_wait数量,把数目改小了,所以大家也跟风改。这是正确的吗?

   使用netstat -antp 命令,使用shell awk uniq 去重,统计IP,发现每个IP发现100.xxx.xxx.xx地址的time_wait特别多,而且地址不固定。100开头的是内部地址。

   使用tcpdump命令查看里面的发送内容,发现就是app的接口服务,还有是一些健康检测的报文。原来是公司使用了阿里云的SLB均衡负载。通过分析报文得知,每次请求,服务都主动关闭了连接,发送FIN,从而产生了time_wait。 所以纠正了我之前的认知。time_wait并不一定是请求方产生的,而是主动关闭连接方产生的。这个通过tcp状态转移图可以看出。

   怀疑阿里云SLB均衡负载是不是有问题。在阿里云提交工单,发问有没有配置可以让均衡负载IP固定下来。问了好几次得到答非所问,最后一次技术支持人员才回答到正轨上,回答说不支持。查看文档,SLB http 7层均衡负载,经过tengine,性能有损耗(相对tcp4层均衡负载),会把长连接转为短连接。然后文档说在压测的情况下会导致后端服务器有大量time_wait.

  怀疑代码框架有问题,但是在本地测试,发现正常,都是客户端主动发起断开请求,服务端没有产生time_wait。通过tcpdump获取报文,与线上的发出报文对比。发现线上的请求方http header 带有connect:close。 本地测试加上了header,问题就复现了。

  换个代码框架测,使用python flask,发现不能复现。通过tcpdump报文分析,发现flask默认使用http 1.0协议进行应答。使用http 1.1 需要设置,设置完毕后,python flask框架也能复现了。所以,每个框架实现http,而http是一种协议,所以成熟的框架在http请求里面的表现都应该是一致的。

  怀疑是客户端请求有问题。让客户端请求头 加上keep-alive: timeout=3,max=1 , 经过测试,3s超时后并没有关闭,后面看文章,发现客户端的超时设置只是一种没有约束力的通知,最终由服务端来决定超时时间。

   后面通过排查,客户端的同事说请求没有带上connect:close 的头,但是线上tcpdump看有这个头,怀疑是其他地方加上的,并通过发工单给阿里云,确认了connect:close是SLB均衡负载产品主动给请求加上的,以达到实现变成短连接的目的。这个也是http 1.1协议的定义,http1.1默认使用长连接,要带上这个参数来关闭长连接。

   如果长连接的话,服务端可以复用连接,不用每次三次握手。如果是客户端主动关闭的话,服务端不会产生time_wait,但是会产生close_wait,而通过tcp状态转移图,close_wait在对端发回应答,即可马上回收(后续描述又是一个问题)。而time_wait需要等待2MSL,这个时间比较久(这个没有按照RFC协议规定时间,一般几分钟左右),回收连接比较慢,会造成堆积。

 但是目前系统负载不高,time_wait数量不多。所以尽管time_wait产生堆积,也不足以产生性能问题。反复排查,time_wait数量很固定,与想tcp_max_tw_buckets这参数阈值很接近。怀疑是设置的问题。通过查看官方说法:

    This limit exists only to prevent simple DoS attacks, you _must_ not lower the limit artificially,but rather increase it (probably, after increasing installed memory),if network conditions require more than default value.

   所以网上的那些优化性能的文章,误导了很多人,不应该随便调tcp的参数。把tcp_max_tw_buckets恢复默认值,后续观察得到缓解。

  但是open too many files的问题,还是时不时出现,这个导致服务阻塞。所以后续文章分析一下close_wait的问题,其中再分析一下tcp连接连接,释放连接过程中遇到的问题。

  后续更新:time_wait 不会占用句柄,因为是调用关闭接口后是马上释放句柄,无需等待,但是会消耗内核内存,影响性能,但不会是“open too many files”的原因

https://oroboro.com/file-handle-leaks-server/

Myth: Sockets in TCP TIME_WAIT are holding file handles Hostage
When you close a TCP/IP socket the operating system does not release the socket right away. For complex reasons, the socket structure must be kept out of circulation for a few minutes because there is a small chance that an IP packet may arrive on that socket after it has been closed. If the operating system re-used the socket then the new user of that connection would have their session affected by someone else’s lost packets.

But this does not hold a file handle open. When you close the socket’s file descriptor, the file descriptor itself is closed. You will not get the “Too many files open” error. If you have too many sockets open, then your server may stop accepting new connections. There are ways to deal with that ( allowing sockets to be re-used, or lowering TCP TIME_WAIT )- but raising the file handle limit isn’t one of them.

Myth: It takes time for file handles to be released
This is related to the TCP TIME_WAIT myth. The mistaken belief that when you close a file handle that you must wait some time for the operating system to release the handle.

Closing a file handle will call into whatever os method releases the resource, and the OS will release that resource either immediately, or sometimes later as in the case with sockets, but close() will release the file handle in the file handle table immediately. Your process is in complete control of its file handle table, and doesn’t need to wait for anything to free a slot in its own file descriptor table.