记一次关于TIME_WAIT TCP连接数的性能优化

标签: 架构与性能 TCP 性能优化 | 发表时间:2014-01-19 19:58 | 作者:juvenxu
出处:http://www.juvenxu.com

一天下午3点多,我们的OPS打电话给我(你懂的,OPS打电话过来不会是请你喝咖啡),我连忙接起电话,电话那头的声音很急迫:“content集群有报警,CPU负载已经到了50多,你快过来一起看看”,我挂了电话连忙赶过去,OPS坐在另一幢楼的同一层,好在两幢楼是相连的,过去很方便。边走我心里边嘀咕,content集群实际上是一个反向代理,上面配置了上百条apache http url rewrite 规则,每条规则的作用就是把一个url路径转换成另外一个url路径,发送到某个后端的集群上,然后把返回结果转出去,几乎没有计算量,不应该有高负载啊。 

来到OPS座位上,他已经在往content集群里加机器了,这个集群为网站提供图片读取服务,负载这么高,服务肯定不稳定了,因此当务之急肯定是让服务恢复稳定。 对任何线上故障来说,恢复服务肯定是第一优先级,问题排查的优先级排在后面。大概5分钟之后,新加的20%机器开始对外服务了,幸运的是,效果立竿见影,集群负载立刻降到了5以下,我输入了几条url测试了下,服务已经恢复稳定了,我俩都松了口气,这次故障没有造成长时间大范围的影响。

接下来我们开始分析造成CPU高负载的原因,这个集群是有监控的,报警也是监控系统发出的,于是我们一个个看监控项:CPU、内存、磁盘消耗、网卡流量、QPS(每秒处理请求数)、响应时间、TCP连接状态等等……然后我们发现了几个问题:
  1. QPS和响应时间没有监控,妈的,这意味着我们无法快速得到访问量和服务稳定性的直接数据。
  2. 从网卡流量我们发现,下午3点左右是访问高峰期,这很可能是诱发CPU负载过高的外因。
  3. TCP连接数中,TIME_WAIT连接数异常地高,扩容前最高到了30k多!扩容后降到了27k以下。

关于第一条,我给自己记了个任务,待当前的问题处理完毕之后,花点时间配合OPS把这个监控完善起来。

第三条关于TIME_WAIT的数据非常可疑,可惜的我我大学学习的那些网络知识早还给老师了,于是只能现补了,开Google查资料,花了点时间了解了TCP连接的TIME_WAIT是怎么回事。TCP连接建立起来传输数据,这个时候对两边来说,连接的状态是ESTABLISHED,数据传输完毕后,连接的其中一方会主动发起断开连接,断开连接的整个过程是四次握手,握手完毕后,断开连接发起方会进入TIME_WAIT的状态。默认情况下,TIME_WAIT的连接是不会被操作系统回收的,只有到了CLOSED状态后,操作系统才会回收,而且默认情况TIME_WAIT连接会持续两个MSL时间,然后通过如下命令我发现这个时间是60s:

cat /proc/sys/net/ipv4/tcp_fin_timeout

因此如果每一个请求都新建一个连接,服务完之后断开连接,一分钟内就会积累大量的TIME_WAIT连接。监控数据显示,单台机器在比较高峰时刻ESTABLISHED数为1.5k的时候,TIME_WAIT连接数接近30k。正是当TCP连接数超过这30k的时候,系统变得不稳定,CPU负载变得很高,为什么会连接数超过30k会使得系统不稳定,我还不清楚,但我需要让TIME_WAIT连接数降下来。
 
第一反应的解决办法是使用TCP的keep-alive模式,让出问题的content集群在访问后端服务器的时候,不要每次都新建连接并断开连接,而是采用keep-alive长连接模式,content集群和后端服务器集群都是内部机器,IP相对固定,长连接完全合情合理,重用连接不仅能够节省建立连接和断开连接的消耗,而且必然能大大降低TIME_WAIT连接的数量。我先看了后端服务器,都是支持keep-alive的,现在就看content怎么主动开启keep-alive请求了。
翻开配置看了下,发现content集群做反响代理是用了 apache的mod_rewrite模块,具体配置大概是这样的:

RewriteRule  ^/img/news/(.*)  http://back-end-server/download/www/news/$1 [L,P]

意思就是说把符合前面正则表达式的请求,转化url之后发到back-end-server,而由于历史原因,这样的配置有100条左右。那怎么配置mod_rewrite模块让它支持keep-alive呢?我捜了半天的Google,把mod_rewrite模块的文档前前后后翻了好几遍,得到的结论是:mod_rewrite不支持keep-alive。

这时候该怎么办?综合各方面意见,有三种比较直接的解决方案:
  1. 啥都不干,直接加机器让平均每台的TIME_WAIT达不到30k,简单粗暴,但显然会造成很大的机器浪费。
  2. 用Nginx替换Apache,很多人都这么和我推荐,但是,抛开Nginx是否合适这个场景不说(我还不太了解),光迁移这100条正则表达式转发规则就要命,更何况还有一大堆其他配置,成本和风险都很高的。
  3. 让操作系统主动回收TIME_WAIT连接,可以通过配置系统的tcp_tw_recycle和tcp_tw_reuse来实现,网上有一大堆这方面的资料。这么做也有缺点,运维成本增加了,每台机器都得去做这个特殊配置,重启后还会丢失,会比较麻烦,我总觉得这是治标不治本的方法。
抓破脑门想不到更好的办法,我就在扩容20%机器的基础上,再调节了tcp_tw_reuse,我没有调节tcp_tw_recycle是因为查阅文档发现 当后端有负载均衡的时候,开启它有潜在的风险,此事就此告一段落,然后我始终放不下。
 
一个月之后,由于同事遇到了类似的问题,激起了我想再次优化TIME_WAIT连接数的欲望。又花了半天看了很多文档及邮件列表之后,我想,作为一个完全合理的需求,为什么mod_rewrite不支持keep-alive呢?而与之功能类似(但不支持复杂正则表达式匹配)的mod_proxy模块,是能很好的支持keep-alive的。从apache的用户邮件列表的 一篇帖子中我发现,keep-alive功能是被有意地从2.2版本开始移除的,而且apache的维护者Igor Galić还说了这么一句:“你为什么要用mod_rewrite做代理?”,莫非我们之前的用法都是错误的?忽然间,思路有了,mod_rewrite的强项是url重写,mod_proxy的强项是代理,那就把代理交给mod_proxy,mod_rewrite只管url域名后面相对路径的重写,不就可以了吗?
 
尝试了一下,得到下面的配置:

ProxyPass  /fs1/  http://back-end-server/  keepalive=On
RewriteRule  ^/img/news/(.*)  /fs1/download/www/news/$1 [L,PT]

对比之前的配置,现在的RewriteRule用了PT模式而不是P模式,P模式是强制代理网络连接,而PT则只是做路径转换,另外代理后的路径以/fs1开头。在此基础上,这里又配置了ProxyPass(mod_proxy),所有/fs1路径开头的请求,转到back-end-server上,并开启keep-alive。拿几条规则做了测试之后,发现TIME_WAIT数有一些下降,于是我在集群中找了一台机器,调整了几条访问量比较大的规则,放线上观察。一天之后,没有收到任何问题,我就把全线的配置都更新了,效果非常明显,之前ESTABLISHED连接1.5k的时候,TIME_WAIT连接数近30k,现在由于keep-alive的开启,TIME_WAIT连接数最多只到6k左右,降到了原来的1/5,也几乎不再可能成为系统的瓶颈。之后我们在类似的场景中发现,如此优化能降低系统响应时间,这也是好理解的,因此不用重复建立连接了,响应自然更快。
 
至此,短时间内我们基本不用考虑content集群的扩容了,因为这是集群的主要作用是反向代理,之前遇到了网络瓶颈现在得以解决,而其他系统资源如CPU、内存之类都还是比较富足的。考虑到网站发展比较迅猛,流量每个月都在上涨,而这个集群的规模比较大,因此这次优化实际上节省了不小数量的机器,那可是白花花的银子啊。
 
这是我第一次在机器量级比较大的系统上做性能优化,有几点可能已经被前人说烂了的总结:
  1. 系统出问题的时候,第一优先级的任务是恢复系统服务。
  2. 系统监控必须要完善,否则做优化的时候简直是抓瞎。
  3. 找到瓶颈,优化才有意义,否则基本是浪费精力。
  4. 计算机系统的基本功还是必须的,该例中就需要对TCP协议有了基本的理解。
  5. 虽然计算机越来越便宜,但是在该例的分布式的服务场景中,一堆便是数十台高性能的物理机,加上运维成本,加上性能差给用户体验带来的损害,其实性能差的代价非常昂贵。因此,设计系统、编写代码的时候,还是要想着节省计算资源。

相关 [time wait tcp] 推荐:

wait、sleep、yield区别

- - CSDN博客推荐文章
1、属于Object的本地方法. 2、暂停当前线程,并释放锁. 3、调用notify()或notifyAll()方法唤醒线程. 1、Thread类的静态方法. 2、当前线程休眠,但不释放锁. 3、其他线程可以继续执行,无论该线程优先级高与否. 4、休眠一段时间后,自动执行. 1、Thread类的静态方法.

Java多线程之wait()和notify()

- - CSDN博客推荐文章
直接看测试代码吧,细节之处,详见注释.  * Java多线程之wait()和notify()的妙用 .  * @see 问题:同时启动两个线程和同时启动四个线程,控制台打印结果是不同的 .  * @see      同时启动两个线程时,控制台会很规律的输出1010101010101010 .  * @see      同时启动四个线程时,控制台起初会规律的输出10101010,一旦某一刻输出一个负数,那么后面的输出就会"一错再错" .

java多线程设计wait/notify机制

- - CSDN博客推荐文章
  当线程A获得了obj锁后,发现条件condition不满足,无法继续下一处理,于是线程A就wait() , 放弃对象锁..   之后在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A:.   # 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {…} 代码段内.

Java多线程之wait和notify

- - ITeye博客
最近在看Java特种兵,看到多线程部分,对wait和notify不是很理解,于是写了代码来帮助理解.              wait方法通过参数可以指定等待的时长. 如果没有指定参数,默认一直等待直到被通知. notify方法是通知某个正在等待这个对象的控制权的线程可以继续运行. 调用wait方法时候,必须加上synchronized同步块,不然会抛出java.lang.IllegalMonitorStateException异常.

tcp/ip调优

- Lucseeker - 在路上
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接. 第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SEND状态,等待服务器确认;. 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;.

Long time no see,英式中文

- cow - 刁民公園
昨天「星期日檔案」探討港式英語. 節目中好幾個嘉賓包括楊鐵樑先生都說long time no see是港式英語. 楊官的英文程度不會令人懷疑,但long time no see有其背景,不能算作港式英語.

被忽视的time命令

- - 火丁笔记
如果要选 Linux 下最容易被忽视的命令,time 应该算一个. 简单来说,它是一个用来计算命令运行时间的工具,之所以说它容易被忽视,一方面很多人根本不知道 time 的存在,而是习惯在命令启动前后记录两个时间戳,然后手动计算命令运行时间;另一方面很多人虽然知道 time 的存在,但是却并没有真正理解它的含义.

浅谈TCP优化

- - 火丁笔记
很多人常常对 TCP优化有一种雾里看花的感觉,实际上只要理解了TCP的运行方式就能掀开它的神秘面纱. Ilya Grigorik 在「 High Performance Browser Networking」中做了很多细致的描述,让人读起来醍醐灌顶,我大概总结了一下,以期更加通俗易懂. 传输数据的时候,如果发送方传输的数据量超过了接收方的处理能力,那么接收方会出现丢包.

TCP报文结构

- - 互联网 - ITeye博客
一、TCP报文结构如下:.  固定首部长度为20字节,可变部分0~40字节,各字段解释:. source port number:源端口,16bits,范围0~65525. target port number:目的端口,16bits,范围同上. sequence number:数据序号,32bits,TCP 连接中传送的数据流中的每一个字节都编上一个序号.

TCP 状态变化

- - 互联网 - ITeye博客
关闭socket分为主动关闭(Active closure)和被动关闭(Passive closure)两种情况. 前者是指有本地主机主动发起的关闭;而后者则是指本地主机检测到远程主机发起关闭之后,作出回应,从而关闭整个连接. 将关闭部分的状态转移摘出来,就得到了下图:. 通过图上,我们来分析,什么情况下,连接处于CLOSE_WAIT状态呢.