一次真实的性能调优实践

Posted 海涛技术漫谈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一次真实的性能调优实践相关的知识,希望对你有一定的参考价值。

文章主要分享了本人上周工作中的一次真实性能调优实践,希望对大家有所启发。为了保护相关隐私,部分的说明进行了脱敏。


一:功能简单介绍


本文涉及的系统是一个千人千面的系统,简单说就是针对不同的用户,通过算法生成不同的数据,并进行推送。系统的简单数据流如下图:


图一:初期架构图


我们大概需要处理将近两千万的用户,希望能在几小时内将数据推送完成。最后项目预发布试跑的时候,我们发现单条用户的处理时间大概2s,按照这个时间,我们简单估算了下2000万的用户需要多少资源和多少时间:2000 0000 * 2 /60/60 = 11111小时/core,如果每台机器是16核的机器,就是 11111/16= 694小时/台,就是说10016核的机器都得要7小时,这个效率和资源的浪费显然是我们无法接受的。


二:开始优化


2.1 定位痛点


忘记哪位大师说过的一句经典的话:在没有遇到性能问题的时候去优化,那就是灾难。既然遇到性能问题,开始优化的第一步就是找到性能慢的关键原因,优化必须针对最痛的那个点,花80%努力去提升那20%,不如花20%的努力去优化那影响了80%效率的问题点。首先,我需要明白这2S时间到底花在了哪里。


根据经验:咱们得尤其注意以下两点:(1)访问第三方介质(DB, rpc接口调用等); (2) 循环,尤其是循环里面调用第三方介质。


经过相关的监控和日志,我们很容易的定位到这个系统问题出在算法预处理模块的RPC调用方法。


这个方法主要工作就是将用户关联的sku(大概1000)查询价格,商品,促销等接口,获取基础数据。

 

2.2 分析问题


这个问题慢的原因很显然,就是大批量的调用RPC接口导致的。一次RPC调用按照10ms估算,1000次也得10sRPC接口调用的时间主要包括服务方处理时间和网络消耗,由于我们使用接口采用了批量调用的方式,即每次入参输入一批sku,节省网络开销,才得以控制在2S左右。


然后,我们继续分析发现,用户有2000万,但是涉及的sku大概只有60万,这也就意味着目前处理方式,大量的RPC调用是重复的,这是最大的问题点,大Boss终于找到了,下面就开始解决了。

 

2.3 解决问题


像这种大规模多次访问第三方介质,我们最先想到的是批量处理,但我们已经是这样做了。这个时候就得请出优化界的三驾马车: 缓存,异步,集群。


因此,我们将现有方案进行改进:定时worker多线程刷数据 + 缓存


缓存的话一般有如下几种考虑,从快到慢:(1) jvm缓存;(2) NoSQL(3) DB


综合考虑现有系统启动后,剩余内存的大小,然后估算了下基础数据对象需要的空间大小,我们发现内存不够,虽然最快,但是暂时不适用。


第二种NoSQL方案,考虑到项目时间比较紧急,而且暂时的系统没有引入redis,引入后还得考虑容灾问题,时间不够,我们也选择了放弃。


因此,我们打算直接将数据进行落库,最简单,如果后续发现性能还不够,再进行下一步的优化。因为数据量不大,我们采用的是单库单表。


因此系统的简单架构变成如下:


图二:改进的架构设计


对于刷数据的定时worker,我们采用了线程池进行并行处理,由于操作都是IO任务,线程池的大小可以适当的开大点,因为线程大部分时间都在IO等待。我们的使用的机器是8核的机器,初始化和最大线程都是开的20,队列50000,拒绝策略采用的CallerRunsPolicy,数据库的最大连接池大小设置的20


其次,我们往数据库写促销和优惠券数据的时候,会有可能操作同一行数据的,往促销和优惠券下的sku集合添加sku,我们需要先拿出当前的数据,简单处理,然后添加新的sku,再放回。由于涉及拿数据和写数据两步操作,多线程环境下,会发生问题,因此必须考虑加锁互斥的问题。


最简答的方法就是直接在方法外加个synchronized 关键字,但这个方式并不高效,会极大的限制并发的性能。


我们采取的方案是:dao层的相关类启动时(spring init-method)初始化20把锁,促销和优惠券各自持有10把,放进一个ConcurrentHashMap中。每次写促销或者优惠券数据时,跟进促销id或者优惠券id进行hash,然后对10取模,根据结果去获取相应的锁,因为同一个idhash值一定一样,所以操作同一id下的数据时,就能达到互斥的效果。


然后,算法预处理模块的查询RPC全部换成查询DB,由于每次需要查询1000sku,查1000次显然效率不是太高,我们采用的是批量查询,将sku分批,然后使用mysqlin 操作进行批量查询,批量的sku不宜太多,因为太多的话,涉及的数据较多,这些数据网络传输消耗和MySQL内部缓存无法发挥优势等问题会凸显,最后批量查询的效果可能还会变差,甚至直接timeout。因此,批量的大小,建议读者自己多次试验进行尝试,找到一个最合理的值,我们使用的是100。最后友情提醒:涉及的字段一定记得加索引,加索引,加索引,重要的事情说三遍。


经过上面一顿猛如虎的操作和折腾,最终将单条任务的处理时间从2s优化到了50ms,性能提升了40倍。结合现有的资源,简单估算了下,时间已经达到要求,考虑项目时间进度也比较赶,就没有在进行后续的优化了。


三:后续优化建议


后续有时间或者效率再次成为瓶颈的时候,可以考虑如下优化:


(1) JVM缓存:考虑到sku是有热商品和冷商品的,其实热的sku不会太多,后面可以考虑将热度较大的sku数据缓存至JVM缓存,例如使用Guava。


(2) Redis:DB毕竟是磁盘操作,性能上肯定没法和内存级别的redis相比较,所以后期可以考虑引入redis


读者可以结合开发时间和系统的独特性,考虑 JVM+redis+DB 的三级缓存结构,或者 JVM+redis, JVM+DB的两层缓存结构。


以上是关于一次真实的性能调优实践的主要内容,如果未能解决你的问题,请参考以下文章

JVM性能调优1:JVM性能调优理论及实践(收集整理)

面试必备:深入 Java 应用性能调优实践

一文教会你数据库性能调优(附某大型医院真实案例)

如何轻松搞定 Java 性能调优?层层调优的实践总结

教会你数据库性能调优(附某大型医院真实案例)

一文教会你数据库性能调优(附某大型医院真实案例)