首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

一次真实的性能调优实践

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

一:功能简单介绍

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

图一:初期架构图

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

二:开始优化

2.1定位痛点

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

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

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

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

2.2分析问题

这个问题慢的原因很显然,就是大批量的调用RPC接口导致的。一次RPC调用按照10ms估算,1000次也得10s。RPC接口调用的时间主要包括服务方处理时间和网络消耗,由于我们使用接口采用了批量调用的方式,即每次入参输入一批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取模,根据结果去获取相应的锁,因为同一个id的hash值一定一样,所以操作同一id下的数据时,就能达到互斥的效果。

然后,算法预处理模块的查询RPC全部换成查询DB,由于每次需要查询1000个sku,查1000次显然效率不是太高,我们采用的是批量查询,将sku分批,然后使用MySQL的in操作进行批量查询,批量的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的两层缓存结构。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180804G1F4ZP00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券