性能优化思路
性能优化的实质
- 去除大量重复、不必要的操作。
- 并发、批量操作。
重复操作:
- 重复创建同一个对象;
- 以相同参数重复调用同一个接口;
- 重复上传或下载同一个文件;
- 重复编译正则表达式;
- 重复获取一个规则集或数据集;
- 重复走一个流程。
性能优化的思路
- 过滤去重:除去不需要处理的,适合小成本
- 缓存:从更邻近的地方预先获取,预编译
- 分治:缩小搜索范围
- 并发:同时进行
- 批量:一次处理多个
- 非阻塞:避免无效等待
- 精简:去除重复和非必要
性能优化的“拦路石”
性能优化的“拦路石”:
- 高并发。比如检测任务创建去重。要应对高并发,就需要限流、加锁,就会导致阻塞,阻塞就会导致性能开销。这些加锁和限流只是针对少量极限场景,在大多数时候只是白白浪费资源。
- 少数场景。为了逻辑的完善,需要应对少量场景。比如 新上报的告警消息晚于上报的告警修复消息,需要走修复流程,会先把告警修复消息存在 redis, 然后在告警上报时访问 redis 去做修复。但是对于绝大部分场景而言,都用不到,白白增加一次 redis 访问开销。要检查是否有其它可替代方案,比如对于这种新上报的告警消息晚于上报的告警修复消息使用延迟队列消费。
- 代码编写。比如 lambda 会打断 arthas trace 的方法栈调用,导致工具无法分析完整的方法调用栈耗时。
- 网络波动。难以控制。
解决性能问题的途径
从根本层面来说,解决性能问题的途径有三种:
- 更好的数据结构与算法(时间复杂度的降低);
- 更少的等待时间(精简流程、优化 IO 访问);
- 更好的逻辑组织(串行改并发)。
绝大部分性能问题的求解,从底层来看,无非就是 数据结构与算法、等待时间、逻辑组织的优化改进,反映在应用层就是各种具体技术措施手段。
实战方法
工欲善其事,必先利其器。性能优化靠的是工具和洞察力。工具能够有效揭示异常迹象,而洞察力能够敏锐意识到问题之所在。
工具与测量
性能优化的首要步骤不是去做代码分析,而是去测量出“热点区域”,有针对性地优化,才能让 ROI 最大化。
全链路
- 开发工具,测量整个链路的所有组件(可包括可预见的耗时方法)的耗时。可粗略,但重点是建立一个整体的耗时视图。
- 单个流程:使用 AOP 实现。量化业务耗时:使用SpringAOP获取一次请求流经方法的调用次数和调用耗时;
- 多个微服务:采用分布式链路追踪系统。
arthas
- 对于 RT 持续较高的方法,使用 arthas trace 跟踪方法调用栈耗时,进一步分析和定位热点区域。
- 对于 RT 较低但波动较大且有明显规律的方法,使用 arthas tt 命令针对耗时大的调用进行分析,看看是否与特定情形有关。
- 对于 RT 较低但波动较大且无明显规律的方法使用, arthas monitor 监控方法的平均 RT,同时监控 DB 或 API 耗时,看看是否与DB或外部服务有关。
- 使用 top ,arthas thread 确定 CPU 高的代码区域。
可以有明显效果的方法(> 50%)
适合在完成初始设计和实现的情形下使用。通常是整体结构性改善。
- 优化存储结构。如果数据和存储设计不够好,在外围做再多优化,效果也是有限的。
(1)表的记录数量应当与表实体的维度匹配。比如文件上传依赖文件 MD5 ,就只与不重复MD5的数量有关。
(2)合适的分表、分库、分片、分区(详情与查询分离,按时间分表、冷热分表、按业务维度分表)。
(3)读写分离。数据库写,使用 ES 或 HBase 读。 - 检查和添加必要的索引。索引区分度、最左匹配准则、覆盖索引、索引命中率等。
- 加缓存,避免不必要的重复流程和工作量。缓存时间和大小需要仔细设定,需要考虑内存开销,避免缓存时间或大小过大。
- 数据结构与算法改进,多重循环降重。通常是构建一个Map,然后从Map中取值。
- 单个改批量。批量查询,提供批量接口,批量处理方式。
- 串行改并发。相互独立的子数据集的处理,可以并发执行然后汇总。
- 同步改异步。耗时较长的次要流程,可以改成异步,加速主流程的消费。
- 阻塞改非阻塞。
- 中间件优化。
- 添加更多的服务实例。动态扩容。服务支持无状态。
- 添加更多的CPU和内存。
可以只保留必要的流程,去掉那些 RT 波动比较大的方法,如此可以测试出性能的上限,作为一个可追求的性能目标。
性能微调的方法( < 30%)
适合在结构已经稳定的情形下使用。
- 简化流程,合并 IO 和 API 调用。比如获取告警详情,需要获取告警的各项信息。初期为了可维护性,每个方法都查询一次告警对象。实际上应该是在入口处查一次,然后在 Context 里传递。
- 去除重复的调用和计算。
- 去除不必要的操作和流程。比如病毒检测流程有一个告警去重过程,但阻断类的病毒检测流程也会走到这里,实际上并不需要。这是因为走到这个流程的条件比较宽泛,需要再限定一下。
- 耗时区域后移。如果一个操作必须在某个条件满足后才能执行,应该把这个操作移到条件判断满足之后。
- 查询数据库时,则只查询所需要的字段。避免 select *。
- 调节线程池参数。
- 调节中间件参数,比如数据库连接池、消息队列消费参数。
- 针对业务特殊性做特殊处理。
- 分布式锁之前加缓存【存疑】。
- 悲观锁改乐观锁。减少悲观锁的锁定范围;使用版本号机制替代直接加锁。
提升检测流程吞吐量的途径
- 存储设计优化,去掉不必要的字段和大对象,减少占用空间;
- 黑白名单,快速过滤;
- 快速去重过滤;
- 加缓存(有一定风险),减少 API 调用次数和查询数据源的次数;
- 尽可能去除 RT 波动较大的方法;
- 精简流程;
- 减少大对象序列化与反序列化。
辅助方法
- 拆分。拆分不会直接带来性能改善,但是能够分离出性能热点区域,为性能优化做好铺垫。
排查命令
查看 CPU 、内存、负载概况
top
top -Hp pid
查看 mongo 慢查和 CPU 高
db.setProfilingLevel(1, 50);
db.system.profile.find().sort({$natural:-1}).limit(5)
db.getProfilingStatus()
db.runCommand({shardConnPoolStats:1})
arthas 启动和停止
su -s /bin/sh -c 'java -jar arthas-boot.jar' titan
java -jar arthas-client.jar 127.0.0.1 3658 -c "stop"
方法 RT 监控(整体视角)
monitor -c 1 xxx.ids.components.DuplicateDetectionFilter process -n 10000
monitor -c 1 xxx.IdsCdcTaskScheduler$Consumer consume -n 10000
monitor -c 1 xxx.IdsCdcTaskScheduler$IdsCdcTaskSender run -n 10000
方法耗时跟踪(定位热点,耗时长定位)
trace com.qt.eventflow.definitions.BizComponent process -m 60 '#cost>20'
trace xxx.ids.components.DuplicateDetectionFilter process -n 10000 --skipJDKMethod false
tt -t xxx.ids.components.DuplicateDetectionFilter process -n 10000
tt -i t_index
耗时统计
awk -F'=' ' $4 > 50 {print } ' rt_costs.txt > rt_50_costs.txt
awk -F'=' ' {print $3} ' rt_costs.txt | sed 's/, cost_time//g' | sort | uniq -c | awk '{print $2" "$1}' | sort -rnk2