[date: 2018-07-22 12:27] [visits: 15]

记一次WEB服务器性能优化

上个周末,自己做的一个业务后端,因为流量“过大”导致大部分请求在正常时间内都无法完成响应,故障持续了1个小时,中途只能调低nginx的连接上限,让部分人能够访问。

这个业务后端上线一个多月,那一天的流量大约占之前所有流量的1/3,而且集中在那么几个小时内,QPS压力应该是平时的几十倍,所以没抗住...

不管怎样,这是自己的锅,在实现这个业务后端的时候没有谨慎的考虑性能问题,所以当高峰流量过后就开始分析问题以给性能优化提供帮助,也为下次的流量高峰做准备。

业务后端概述

这个业务后端主要由Nginx、PM2、Node.js、MySQL、Redis、OSS组成,下面分别描述一下它们在系统中扮演的角色:

除了后台管理系统的静态文件,业务本身的静态文件都是放在OSS上,所以Nginx在这里的作用只是作为反向代理

Node.js的进程管理,使用默认配置,没有其他插件

没有使用WEB框架,主要就是业务实现,合计约100个接口

所有数据都在MySQL中,整个系统一共用到约30张表,表的数据量都还不大,最大的一张表约40W条记录

Redis在未优化之前,主要用来存session,以及之前一篇文章说讲述的的队列数据

静态资源都存放在阿里云的OSS上,这部分流量完全不经过机器,跟系统负载没有关系

以上系统组成中,MySQL用的阿里云RDS服务(基础版1核1G),其他除OSS外都部署在同一台阿里云ECS(1核2G)上。

整个后端系统大约花2-3周做出来的,没有压力测试,没有性能指标等,也没有特意去优化,并且自己心中知道有部分逻辑不合理,有量的时候应该会出问题。

分析现状

问题过后,要做的第一件事是分析现状,主要包括以下内容:

故障原因

导致此次故障的原因即此次的性能瓶颈,主要是MySQL性能问题,CPU 95%以上,基本处于死机状态,大量的SQL查询超时导致客户端请求超时。

导致MySQL性能跟不上,原因是SQL不合理、设计不合理这两个点,本次优化也就着重这两部分内容。

关于两种不合理的简单理解:

SQL不合理比较容易理解,也就是SQL语句写的不好,而SQL语句的好与不好又与表设计(索引、表结构、表关系)相关,一般表关系与表结构都不能轻易调整,所以更多的是优化索引与SQL语句

设计不合理主要是指业务逻辑实现不合理,这里主要是指跟MySQL有关的逻辑,比如最简单的翻页查询order by & limit,哪怕SQL优化的很好,在数据多一点场景下还是会对DB造成过多压力,而DB往往又是单点,所以整个系统的QPS就上不去

解决设计不合理的问题核心就是加缓存,减轻DB压力,加缓存是个非常细腻的活,马虎不得,需要仔细思考

这次优化,针对这两个点都有所作为,效果很不错,限于篇幅下次再来讲述。

量化本次高峰流量

知道故障原因还不能马上进行优化,因为有量化手段才能统计后续优化的效果,以及制定优化的目标。

前文提到过系统没有任何监控,所以最初收集到的信息只有:

问题出现的半小时前,Nginx的连接数约500-600,此时系统正常,CPU负载25%左右,MySQL没有收集到当时的负载信息

收到故障反馈时,Nginx的连接数大约1000,整个系统已经很难正常响应请求,MySQL CPU 95%+,由于是IO导致系统性能瓶颈,机器CPU保持在30%以下。

高峰期之后从访问日志中分析出当时系统的QPS峰值:124,我知道这个QPS很小,但系统就是没扛住,有经验的朋友不要笑~

极限QPS

统计和分析QPS的时候,按时段做了一张表:

时段 访问量 峰值时间 峰值QPS 平均响应时长(ms) 响应>1s 出错请求数
18-19 7761 18:02:50 12 23.4577 4(0.05%) 6
19-20 37320 19:40:41 41 26.4526 2(0.00%) 8
20-21 58060 20:01:40 42 30.581 60(0.10%) 253
21-22 61457 21:53:58 66 30.7309 24(0.03%) 41
22-23 116568 22:56:52 81 550.192 4910(4.21%) 1348
23-24 116506 23:51:33 124 3916.72 49725(42.68%) 8456

大约22:30的时候,我在那台机器上观察了一会Nginx连接数与CPU负载,比较正常,知道有一定的流量,然后在23点的时候,收到用户反馈说打不开应用。

从表中可以看到21-22点的QPS峰值是66,在22:56时QPS达到另一个峰值81,随后系统进入异常状态,可以猜测系统的正常QPS极限应该在66-81之间。

热点API

上面的流量高峰和极限QPS都是针对系统所有API的一个平均值,而实际上各API的访问频率、次数是有差异的,所以为了后续的优化,在这里统计了一下当天的API访问次数,选出前十作为后续重点优化对象。

url 访问量 平均响应时长(ms) 响应>1s
/fsfl/api/client/user/detail 101872 651.378 14542(14.27%)
/fsfl/api/client/star/time-history 52766 2.49104 0(0%)
/fsfl/api/client/complex/home?size=50 40925 865.471 5270(12.87%)
/fsfl/api/client/star/week-items?page=0&size=20 40749 5536.49 8668(21.15%)
/fsfl/api/client/user/update 27944 370.269 2834(10.14%)
/fsfl/api/client/star/week-detail/16523?size=20 22448 3073.31 7091(31.58%)
/fsfl/api/client/star/week-detail/32807?size=20 19000 709.773 1433(7.54%)
/fsfl/api/user/login 10778 2026.86 2513(23.31%)
/fsfl/api/client/user/check-state 9536 325.902 603(6.32%)
/fsfl/api/client/user/share-award 7480 572.353 707(9.45%)

QPS那张和这张热点API表中的数据,都是自己用人工方式从访问日志中一个个提取出来的,后续也单独拿出来讲讲如何提取,但这里自己在提取的时候,忘了统计单个API峰值QPS,过了一周也不想回头再去统计。

表中/fsfl/api/client/star/time-history这个接口容易引起注意,这个接口是一个非常简单的代码执行接口,不涉及任何IO,后续也选它作为测试的基准。

wrk单独测了测这个接口,在那台机器上的QPS极限大约600-700,后面把机器升级成4核4G后,极限QPS在2000左右,作为基准,所有其他业务API在优化后,以这个数值为参考,能达到1/5我觉得都算是及格,实际情况是优化的大部分接口能达到1/3,也就是600-700的QPS。

优化点

整个系统的优化点主要有四个:

缓存与SQL优化

优化的核心是缓存与SQL,也就是解决前文的SQL不合理与设计不合理,寻找的过程是从API的访问次数入手,优先解决热点。

按热点API逐个review其背后的逻辑、SQL并设计缓存,这部分工作我扎扎实实做了三四天,抽空单独找些例子出来写成文章讲述具体如何做。

在这里稍微提一下自己在做缓存优化时的基本原则,所有数据依旧以DB为准,缓存可自动初始化,缓存可随时被清除而业务依旧保持正常,同时触发缓存再次初始化,这里由于不是做非常高可靠服务,暂时没有考虑缓存穿透等高级内容。

硬件升级

1核的机器,再怎么优化也会达到机器的CPU瓶颈,尤其是减轻DB压力后,应用这边的压力也会有所增加,所以机器以及MySQL的硬件都选择进行升级,最后选择机器升级成4核4G,MySQL升级成2核4G。

机器升级成4核,单API的QPS并不会提升4倍,就比如前文QPS最高的接口就只是由650提升到2000,这个是正常的,因为Nginx,PM2这些在多进程模式下“调度”本身会带来CPU开销,还有就是操作系统层面的问题,进程不是跟CPU绑定在一起的,执行的时候会涉及更多的上下文切换等工作(这部分内容我没深入考究,只是凭经验所写)。

水平扩展

这个由于使用Node.js实现业务的时候就就只依赖两个单点:MySQL、Redis,所以实现水平扩展比较容易,但靠这一次的缓存优化+SQL优化+升级硬件就可以满足预期,所以就暂时没有考虑部署多台机器。

优化成果

大部分API在优化之前的QPS都是2位数,经过SQL优化与设计缓存,最终保证了热点API的QPS大致在400-700之间,优化后的API,QPS上不去的原因都是是带宽与机器CPU

优化后系统整体QPS提升估计在4-5倍,而且减缓了DB的压力,当再出现性能问题时就考虑用加机器与加带宽的方式解决CPU瓶颈。

剩余问题与优化空间

问题

也许QPS再次超出这次极限两三倍,可以用加机器和带宽的方式解决,但随即可能冒出其他问题,因为这次的优化着重是考虑所有热点API其背后的逻辑优化,但剩余的大部分非热点API在整体访问QPS的的提升过程中所面临的问题依旧是严峻的。

比如系统在一秒内完成了200个请求,也许这200请求里面有70%是热点API,剩下的30%是几个非热点API,假设其中一个非热点API占30%中的40%,也就是这个API的QPS最起码要达到24,系统才能正常运转,如果这个API依赖DB,当平均QPS上升到400时,DB有可能就垮掉了,然后整个系统也就处于异常状态了。

依赖DB并且QPS不高,这种接口目前肯定是还存在系统当中,比如有几个接口背后用了事务与select for update,如果量继续创新高的话,是有可能带来问题的。

要解决这个问题,依赖一定的预知判断力,以及反馈机制,比如多观察MySQL的负载情况,机器的负载情况等,有精力、能力的话是时候考虑一下监控这一块,简单实现,满足自己需求即可。

优化空间

上述提到的问题,其实就是优化空间存在的地方,我觉靠这个模式做到几千QPS应该不是什么大问题。

除了上面提到优化方案,我还打算用一下Nginx上的Proxy Cache,这个只能针对部分接口去做,可能还需要前端配合。

从热门接口中区分出不需要非常实时的一类,也许就是延迟1s,然后利用Nginx上的Proxy Cache,这个能带来的性能提升是非常显著的,具体可以参考这篇Nginx文章

总结

自己日常遇到这种问题的几率特别少,所以一般都不注重应用整体性能,这次遇到些问题,收获颇多,也让自己明白性能,监控的重要性,同时自己有机会去做这种事情,挺有兴趣和热情,而且有信心做好。

这篇文章更重要的是纪录,不涉及到什么技术相关,后面抽空找些技术例子与大家分享。