一天下午3点多,我们的OPS打电话给我(你懂的,OPS打电话过来不会是请你喝咖啡),我连忙接起电话,电话那头的声音很急迫:“content集群有报警,CPU负载已经到了50多,你快过来一起看看”,我挂了电话连忙赶过去,OPS坐在另一幢楼的同一层,好在两幢楼是相连的,过去很方便。边走我心里边嘀咕,content集群实际上是一个反向代理,上面配置了上百条apache http url rewrite 规则,每条规则的作用就是把一个url路径转换成另外一个url路径,发送到某个后端的集群上,然后把返回结果转出去,几乎没有计算量,不应该有高负载啊。
来到OPS座位上,他已经在往content集群里加机器了,这个集群为网站提供图片读取服务,负载这么高,服务肯定不稳定了,因此当务之急肯定是让服务恢复稳定。
对任何线上故障来说,恢复服务肯定是第一优先级,问题排查的优先级排在后面。大概5分钟之后,新加的20%机器开始对外服务了,幸运的是,效果立竿见影,集群负载立刻降到了5以下,我输入了几条url测试了下,服务已经恢复稳定了,我俩都松了口气,这次故障没有造成长时间大范围的影响。
接下来我们开始分析造成CPU高负载的原因,这个集群是有监控的,报警也是监控系统发出的,于是我们一个个看监控项:CPU、内存、磁盘消耗、网卡流量、QPS(每秒处理请求数)、响应时间、TCP连接状态等等……然后我们发现了几个问题:
- QPS和响应时间没有监控,妈的,这意味着我们无法快速得到访问量和服务稳定性的直接数据。
- 从网卡流量我们发现,下午3点左右是访问高峰期,这很可能是诱发CPU负载过高的外因。
- 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。
这时候该怎么办?综合各方面意见,有三种比较直接的解决方案:
- 啥都不干,直接加机器让平均每台的TIME_WAIT达不到30k,简单粗暴,但显然会造成很大的机器浪费。
- 用Nginx替换Apache,很多人都这么和我推荐,但是,抛开Nginx是否合适这个场景不说(我还不太了解),光迁移这100条正则表达式转发规则就要命,更何况还有一大堆其他配置,成本和风险都很高的。
- 让操作系统主动回收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、内存之类都还是比较富足的。考虑到网站发展比较迅猛,流量每个月都在上涨,而这个集群的规模比较大,因此这次优化实际上节省了不小数量的机器,那可是白花花的银子啊。
这是我第一次在机器量级比较大的系统上做性能优化,有几点可能已经被前人说烂了的总结:
- 系统出问题的时候,第一优先级的任务是恢复系统服务。
- 系统监控必须要完善,否则做优化的时候简直是抓瞎。
- 找到瓶颈,优化才有意义,否则基本是浪费精力。
- 计算机系统的基本功还是必须的,该例中就需要对TCP协议有了基本的理解。
- 虽然计算机越来越便宜,但是在该例的分布式的服务场景中,一堆便是数十台高性能的物理机,加上运维成本,加上性能差给用户体验带来的损害,其实性能差的代价非常昂贵。因此,设计系统、编写代码的时候,还是要想着节省计算资源。