整体设计详解
在我看来,能提高服务器应对并发的能力的方式无非两种:
接下来我们围绕这两个方面谈谈在1号店抽奖系统中所做的工作和遇到的坑。
整体架构如下图:
我们的负载服务器使用的是A10,商业的负载均衡硬件,相比Nginx,虽然花不少钱,但在使用配置等方面简单,便于维护,Web服务器自然是Tomcat。这里我们优化了两件事情。
a) 防cc
负载均衡作为分布式系统的第一层,本身并没有好说的。唯一值得一提的是针对此类大流量场景,我们特意引入了防cc机制,策略为单ip限制200/每分钟的最高访问次数,超出频率的请求直接拒绝,防止用户使用脚本等方式刷请求。这个在我们使用的负载均衡器A10上可以自行配置,如果是Nginx也有限制连接模块可以使用,这也是流量削峰的第一层。
b) Tomcat并发参数
我们之前线上的Tomcat是使用默认的参数maxThreads=500,在流量没有上来之前没什么感觉,但大流量情景下会抛出不少异常日志。在通过性能压测后发现,在并发请求超出400+后,响应速度明显变慢,后台开始出现数据库,接口等链接超时,因此将maxThread改为了400,限制tomcat处理量,进一步削减流量。
从这里开始,请求就进入应用代码中了,在这一层,我们可以通过代码来进行流量削峰工作了,主要包括信号量,用户行为识别等方式。
a)信号量
前面谈到了通过Tomcat并发线程配置来拦截超出的流量,但这里有一个问题是超出的请求要么被阻塞,要么被直接拒绝的,不会给出响应。在客户端看到的是长时间没有响应或者请求失败,然后不断重试,我们更希望在这个时候响应一些信息,比如说直接给出提示没有中奖,通知客户端不再请求,从而提高用户体验。因此在这里我们使用了Java并发包中的Semaphore,伪代码如下:
由于通过压测得出的Tomcat最大线程数配置为400,这里的信号量我们设成了350,剩下50个线程用来响应超出的请求。在这种情景下,我们曾用800个并发做过测试,由于请求还未抵达复杂的业务逻辑中,客户端可以在10ms内收到错误响应,不会感到延迟或请求拒绝的现象。
b) 用户行为识别
Tomcat及信号量进行的并发控制我称之为硬削峰,并不管用户是谁,超出设置上限直接拒绝。但我们更想做的是将非法的请求拦截掉,比如脚本,黄牛等等,从而保证正常用户的访问,因此,在公司风控等部门同学的协助下,引入一些简单的用户行为识别。
下图一个接入用户行为识别前后的一个流量对比图。
可以明显的看到,两天的同一时刻,在未接入识别时流量峰值为60w ,接入识别后流量降为30w 。也就意味着有人通过脚本等工具贡献了超过一半的请求量;另一个比对是,在没有接入识别时,我们一个活动数万奖品,在活动开始3秒钟就已经被抽光,而接入之后,当活动结束时刚好被抽完。
所以,如果没有行为识别的拦截,不少正常用户根本抽不到奖品,这点跟春节抢火车票是一样的场景。
c) 其他规则
其他规则包括缓存中的活动限制规则等等,根据一些简单的逻辑,也起到一定作用的流量削峰。
至此,我们所有的流量削峰思路都已经解释完了,接下来是针对性能优化做的一些工作。
性能优化是一个庞大的话题,从代码逻辑,缓存,到数据库索引,从负载均衡到读写分离,能谈的事情太多了。在我们的这个高并发系统中,性能的瓶颈在于数据库的压力,这里就聊下我们的一些解决思路。
a) 缓存
缓存是降低数据库压力的有效手段,我们使用到的缓存分为两块。
b) 无事务
对于并发的分布式系统来说,数据的一致性是一个必须考虑的问题。
在我们抽奖系统中,数据更需要保证一致,活动奖品是1台iPhone,就绝不能被抽走两台。常见的做法便是通过事务来控制,但考虑到我们业务逻辑中的如下场景。
在JDBC的事务中,事务管理器在事务周期内会独占一个connection,直到事务结束。
假设我们的一个方法执行100ms,前后各有25ms读写操作,中间向其他SOA服务器做了一次RPC,耗时50ms,这就意味着中间50ms时connection将处于挂起状态。
前面已经谈到了当前性能的瓶颈在于数据库,因此这种大事务等于将数据库链接浪费一半,所以我们没有使用事务,而是通过以下两种方式保证数据的一致性。
4. 数据库及硬件
再往下就是基础层了,包括我们的数据库和更底层的硬件,之所以单独列一节,是为了聊聊我们踩的一个坑。
当时为了应对高并发的场景,我们花了数周重构,从前台服务器到后台业务逻辑用上了各种优化手段,自认为扛住每分钟几十万流量不成问题,但这都是纸上谈兵,我们需要拿数据证明,因此用JMeter做了压测。
首先是流量预估,我们统计了过往的数据,预估的流量是15w/分钟,单次请求性能指标是100ms左右,因此吞吐量为150000/60~2500tps,每次请求100ms,即并发数为250,这只是平均的,考虑活动往往最开始几秒并发量最大,所以峰值并发估计为平均值的3-5倍。
第一次我们用50个并发做压测:
压测结果简直难以置信,平均耗时超600ms,峰值轻松破1000ms,这连生产上日常流量都扛不住,我们做了这么多手段,不应该性能反而降低了,当时都有点怀疑人生了,所以我们着手开始排查原因。
首先查看日志发现数据库链接存在超时:
排查发现配置的数据库链接数为30,50个线程并发情景下会不够,将最大链接数设为100.数据库链接超时问题没有了,但问题没这么简单,测试下来还是一样的结果。
然后通过VisualVM连上压测的JVM,我们查看了线程的快照。
如图,发现在几个数据库写方法以及一个RPC接口上的耗时占比最大。
所以一方面我们自己着手查原因,另一方面也推动接口提供方减少耗时。
首先是一些常规的排查手段:
当时花了两天时间毫无进展,代码上没发现任何问题,也请教了很多同事,感觉已经陷入了思维误区,然后有位同事说这不是我们程序的问题,会不会是数据库本身或者硬件问题。我们马上找了DBA的同事,查看测试数据库的执行情况,如图:
log file sync的Avg wait超过了60ms,查阅资料后了解到这种情况的原因可能有:
然后我们一看,压测环境的服务器的硬盘是一块老的机械硬盘,而其他环境早已SSD遍地了。
我们连夜把压测环境切换到了SSD,问题解决了,最后压测结果:
单机441个并发, 平均响应时间136ms,理论上能扛住19w/分钟的流量,比起第一次压测有了数十倍的提升,单机即可扛住预估流量的压力,生产上更不成问题了,可以上线了。
至此,整个抽奖系统的架构,以及我们限流削峰和调优的所有手段已经介绍完了,接下来展开下其他的优化想法和感悟吧。
其他优化想法
这里还有一些曾经考虑过的想法供参考,可能由于时间,不适用等原因没有做,但也是应对高并发场景的思路。
几点思考