Part1
问题背景
手头本来有个很重要的性能优化工作,我也一直在提高它的优先级。毕竟按照四象限时间管理法则,重要的事情要先做。
问大家一个问题:给你一个「重要不紧急」的事情和「紧急不重要」的事情,你先做哪一个?
在一般的互联网公司,大家都非常忙碌。活儿是永远干不完的。这时候,我建议先做重要的事情。试想:一个人永远都在做「紧急不重要」的事情,他的产出必然是非常低的。这就是为什么「重要不紧急」在第二象限,仅仅排在「重要且紧急」后面。
道理总结起来容易,做起来难。特别是对于性能优化这种工作内容,需要静下心来不被打扰。所以真正开始分析的时候,已经有些晚了。这个要优化的问题,本来是有一个团队在负责的,人家的优化已经进入了尾声。我这时候才开始,如果最终分析结果是要大改,这就是我的责任,没有更早的介入,让大家做了很多的无用功。
所以对于这个性能优化,对我自身提出的硬性要求是:
1、效果要明显
2、改动要小,最好能用上之前同事做优化时添加代码的一些成果
我得知这个性能优化做完之后要给大家做一个分享。前段时间在做本地调试工具kt-connect。技术问题已经告一段落,目前是推广阶段。附加要求是:能否同时做一个本地调试工具的软推广植入?
目标有了,该怎么做呢?
Part 2
性能优化方法
在《性能之巅》这本书中,我印象最深的性能优化方法,包括三种推荐的方法和三种要避免的方法,我称之为:三正三反。
三正
科学法:采用以下框架 问题->假设->预测->实验->分析
USE方法:USE是utilization、saturation和error的首字母,意思是:对于每一个资源,检查使用率、饱和程度和错误情况。它的目的是尽早地进行性能检查,并发现系统瓶颈。
向下挖掘分析法:开始在高级别检查问题,然后依据之前的发现缩小关注的范围,忽视那些无关的部分,更深入发掘那些相关的部分。这种方法经常和5Why分析法配合使用。
三反
街灯讹:用户选择熟悉的观测工具来分析性能,这些工具可能是从互联网上找到的,或者是用户随意选择的,仅仅想看看会有什么结果出现。这样的方法可能命中问题,也可能忽视很多问题。
随机变动讹:用户随机猜测问题可能存在的位置,然后做改动,直到问题消失。这种方法非常耗时而且可能做出的调整不能保持长期有效。例如,一个应用程序的改动规避了一个数据库或者操作系统的bug,其结果是可以提升性能,但是当这个bug被修复后,程序这样的改动就不再有意义,关键是没有人真正了解这件事情。
责怪他人讹:步骤如下
1、找到一个不是你负责的系统或环境的组件
2、假定问题是与那个组件相关的
3、把问题扔给负责那个组件的团队
4、如果证明错了,返回步骤1
另外,在本地调试的一个极大的好处是可以集成工具,这次使用Intelij+JProfiler来做功能和性能诊断。
实际问题中,怎样运用这些方法和工具呢?
Part 3
分析过程
问题
有一个核心接口,一次调用涉及多次数据库查询,而且查询返回数据量在几百条的量级。网络正常情况下:第一次请求响应时间在1s~3s之间。
这段时间,负责团队进行了性能优化,在数据库操作的地方加了redis缓存,后面连续请求的话,响应时间在630ms~680ms之间。
因为这都是本机调试时的测试结果,在服务器上运行耗时要短很多。因为二者的网络架构和经过的网络节点都不同,耗时差异主要在网络延迟。但服务器上运行也要在150ms。对于性能本机与服务器上的结果可以粗略做一个换算:680/150≈4.5。
对于核心接口,我们TP90的目标是100ms,在我本机执行至少要低于450ms。怎样进一步优化响应耗时呢?
假设
之前和负责团队聊过,负责团队分析认为:之前的性能瓶颈主要在数据库。所以采用Redis访问代替数据库查询的优化方案。从效果上看响应耗时降低到了原来的1/3~1/4。通过DBA的给力配合,在DEV环境,把慢查询日志从之前记录1s以上的慢查询修改为30ms以上的慢查询。通过以下SQL可以查询到慢查询日志情况:
select DATE_ADD(start_time, INTERVAL 8 HOUR)as start_time,db,user_host,query_time,lock_time,rows_sent,rows_examined,CONVERT(sql_text USING utf8mb4) AS slow_log from mysql.slow_log where db='XXX' order by start_time DESC limit 1000
值得注意的是默认时间记录的是UTC时间,与北京时间相差8小时,所以SQL中包含了对显示时间的转换。
从截图可以看出,慢查询的SQL耗时在50ms左右,都是一条SQL,请求中包含的其他SQL耗时都在30ms以下。
从程序端来看:对这条50ms的SQL调用记录时间,发现第一次请求走数据库时耗时在500ms左右,后续走redis缓存时调用耗时在200ms左右。换算成服务器的耗时执行数据库SQL耗时约为110ms,redis缓存耗时约为45ms。我之前见过缓存中间件做的好的,从Redis里取数据加上网络开销也能维持在1ms之内。这个耗时太长了,我怀疑开销花在网络上。
预测
为了降低网络开销,可以将使用Redis集中式缓存改成使用本地内存缓存。通过观察日志计算,抛去从数据库和缓存取数据的开销,其他时间开销在20多毫秒。据此预测,如果用内存缓存响应延迟可维持在35ms以下。
实验程序中用了Spring的Cacheable做缓存。
@Cacheable(value = "get#300#30",
key = "",
cacheManager = "autoRefreshRedisCacheManager")
修改时只需要修改cacheManager的实现即可。
原来的实现方式是使用redis缓存:
public RedisCacheManager autoRefreshRedisCacheManager(
@Qualifier("cacheRedisTemplate") RedisTemplate<String, Object> redisTemplate) {
RedisCacheWriter redisCacheWriter =
RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
return new AutoRefreshRedisCacheManager(redisTemplate, redisCacheWriter, redisCacheConfiguration);
}
这里就涉及到「面向对象设计原则」中的「里氏替换原则」了。这里返回值不建议直接用实现类,而应该返回接口,方便进行替换:
public CacheManager autoRefreshRedisCacheManager() {
return new ConcurrentMapCacheManager();
}
运行看效果:
从效果上看响应时间从几百毫秒降低至几十毫秒,有数量级上的质变,效果还是很显著的。
分析
本地缓存效果这么好,为什么业界偏向于用集中式缓存呢?
集中式缓存可以提供更好的数据一致性,因为所有应用程序实例都访问同一个缓存服务器,减少了数据不一致的可能性;它还通常支持数据的持久化,可以将数据存储在硬盘上,即使缓存服务器重启也能恢复数据;如果数据量大,或者一直不断增长,本地缓存就不合适,甚至可能会服务的运行造成影响,这种情况下,集中式缓存可以通过专用的缓存服务器来处理大量数据,并具有更好的扩展性和性能。
本次的场景是否需要较强的数据一致性和持久化存储呢?
改造之前本身也是定时任务来刷新的,业务场景允许一定的数据不一致。如果数据由于重启等原因失效,只需要从数据库重新拉取,不需要持久化存储。
本次的场景有没有什么副作用?比如缓存数据太多,占用大量内存?
为了尽早发现问题,可以使用USE方法,观察系统的指标。针对本次的场景,因为接口相当于返回的配置数据。如果对数据库的数据只缓存全量,总缓存数据量通过计算吃掉的内存不超过1M,问题不大。但是目前的修改方式,会根据入参做不同的缓存。缓存数据是要成笛卡尔积增长的。加上用户输入非法数值。会不会打满内存?还是默认的内存淘汰策略就可以搞定?
对此我进行了测试,打开JProfiler的录制功能。添加一个测试类,类里将各种参数的笛卡尔积都调用一遍,还加了一些随时数作为入参的来模拟用户输入错误的情况。观察内存和响应时间。下面两张图中,绿色线是CPU占用的情况,蓝色是内存占用的情况。
这张图是修改前使用redis缓存:
这张图是修改后使用本地缓存:
从结果看,对内存并没有很大影响。先运行这个测试类,因为把所有的情况都已经缓存了,所以运行速度也特别快。
但是如果没有运行测试类,改变一个参数,第一次调用因为缓存不存在,还是会慢的。为了减少缓存占用,同时提高用户不同参数输入时的响应时间,建议将这些配置信息不要按照参数每次去取,而是一次性取出并缓存。其他参数使用内存过滤。
还有没有其他可以提高性能的优化点?
程序中还存在着与入参无关的获取数据操作,耗时也不短,可以进行异步获取,提高响应速度。
有没有发现其他问题?
其中有一步要分成几百个子任务,经同事实验发现,使用异步时,启用6个线程速度最快。在此基础上增加线程效率反而下降,这个需要调查原因,找到竞争的资源。
Part 4
总结
这次性能优化整体采用科学法的框架,结合USE方法排查隐患和向下挖掘分析法进行问题分析。过程中使用了业界常用的分析工具比如慢日志、JProfiler来做数据支撑。很多工作可以本地编写完了直接扔到服务器上测试。但是使用本地调试和扔到服务器上测试,养成的习惯和解决问题的方式,思考问题的缜密程度都截然不同。这些最终都决定了一个人的编程能力。
本篇文章中缓存使用的是ConcurrentMap。这个从性能上要比咱们耳熟能详的本地缓存大咖:Caffeine、Guava Cache要好。但只适用于测试,不建议生产环境使用。因为生产环境要考虑长期运行时的内存管理等问题。ConcurrentMap将存储所有存入的数据(本次测试场景入参是笛卡尔积之后也只有不到1千种情况,所以可以用),如果不引进额外的管理机制会导致OOM等问题。
以下是生产环境常用的本地缓存类库性能对比:
性能最好的Caffeine,业界是这样评价的:
Caffeine是基于Java 1.8的高性能本地缓存库,由Guava改进而来,而且在Spring5开始的默认缓存实现就将Caffeine代替原来的Google Guava,官方说明指出,其缓存命中率已经接近最优值。实际上Caffeine这样的本地缓存和ConcurrentMap很像,即支持并发,并且支持O(1)时间复杂度的数据存取。二者的主要区别在于:
ConcurrentMap将存储所有存入的数据,直到你显式将其移除;
Caffeine将通过给定的配置,自动移除“不常用”的数据,以保持内存的合理占用。
因此,一种更好的理解方式是:Cache是一种带有存储和移除策略的Map。
即:业界生产使用的最好的本地缓存组件的性能标杆是ConcurrentMap。文章来源:https://www.toymoban.com/news/detail-860983.html
文章来源地址https://www.toymoban.com/news/detail-860983.html
到了这里,关于性能优化实践:一行代码性能提升几十倍?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!