主题
调优策略
三步走: 测试 -> 分析 -> 调优
基准测试
微基准 + 宏基准
微基准性能测试:可以精准定位到某个模块或者某个方法的性能问题,特别适合做一个功能模块或者一个方法在不同实现方式下的性能对比。例如,对比一个方法使用同步实现和非同步实现的性能。
宏基准性能测试:是一个综合测试,需要考虑到测试环境、测试场景和测试目标。
1、看测试环境,需要模拟线上的真实环境。
2、看测试场景。需要确定在测试某个接口时,是否有其他业务接口同时也在平行运行,造成干扰。需重视这种干扰,否则测试结果会出现偏差。
3、看测试目标。可以通过吞吐量以及响应时间来衡量系统是否达标。不达标,就进行优化;达标,就继续加大测试的并发数,探底接口的 TPS(最大每秒事务处理量),这样做,可以深入了解到接口的性能。除了测试接口的吞吐量和响应时间以外,还需要循环测试可能导致性能问题的接口,观察各个服务器的 CPU、内存以及 I/O 使用率的变化。
热身(冷启动)问题
在 Java 编程语言和环境中,.java 文件编译成为 .class 文件后,机器还是无法直接运行 .class 文件中的字节码,需要通过解释器将字节码转换成本地机器码才能运行。
为了节约内存和执行效率,代码最初被执行时,解释器会率先解释执行这段代码。随着代码被执行的次数增多,当虚拟机发现某个方法或代码块运行得特别频繁时,就会把这些代码认定为热点代码(Hot Spot Code)。
为了提高热点代码的执行效率,在运行时,虚拟机将会通过即时编译器(JIT compiler,just-in-time compiler)把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后存储在内存中,之后每次运行代码时,直接从内存中获取即可。
所以在刚开始运行的阶段,虚拟机会花费很长的时间来全面优化代码,后面就能以最高性能执行了。
优化策略包括:
预加载和缓存机制:对于需要大量初始化或加载的数据,可以在系统启动时预先加载到内存中,或者利用缓存技术(如Redis、Memcached等)将常用数据提前存储,减少实际请求时的加载时间。
服务启动优化:检查应用服务启动过程中的资源初始化、数据库连接建立、配置加载等环节是否可以并行化处理或异步执行,避免串行操作带来的延迟。
代码层面优化:减少不必要的初始化工作,例如模块级别的懒加载,仅在真正需要时才进行初始化。
容器化与云原生优化:利用容器技术(如Docker)和Kubernetes等编排工具,实现快速拉起和销毁服务实例,缩短服务上线及恢复的时间。
基础设施优化:提高硬件资源配置,比如提升CPU、内存性能,使用SSD等高速存储设备,优化网络环境,降低IO等待时间。
性能测试结果不稳定问题
虽然每次测试处理的数据集都是一样,但测试结果却有差异。这是因为测试时,伴随着很多不稳定因素,比如机器其他进程的影响、网络波动以及每个阶段 JVM 垃圾回收的不同等等。
可以通过多次测试,将测试结果求平均,或者统计一个曲线图,只要保证我们的平均值是在合理范围之内,而且波动不是很大,这种情况下,性能测试就是通过的。
可以从以下几个方面进行具体优化:
环境一致性:
- 确保每次测试运行在相同的硬件配置、软件版本以及系统资源条件下。
- 清理测试前后的系统状态,比如关闭不必要的后台服务,保证测试环境纯净。
并发模型和线程模型:
- 检查并合理设置并发用户数、事务混合模式等参数,避免因并发策略不当引起的不稳定性。
- 对于多线程应用,检查是否存在线程同步问题或伪共享现象,并进行相应优化。
负载生成器与目标服务器网络条件:
- 确保负载生成器(压测工具)与被测服务器之间的网络连接稳定,减少网络波动对测试结果的影响。
- 如果条件允许,尽量让负载生成器与目标服务器部署在同一内网环境中。
资源限制与监控:
- 在测试过程中持续监控CPU、内存、磁盘I/O、网络带宽等系统资源使用情况,确保资源未达到饱和或瓶颈状态。
- 根据实际情况调整资源分配策略,如增加系统资源,或者优化程序以减少资源消耗。
数据准备与清理:
- 在测试开始前准备好充足的测试数据,确保测试期间不会因为数据不足而影响性能表现。
- 测试结束后及时清理临时产生的数据,防止对下一次测试产生干扰。
长尾效应处理:
- 分析是否存在部分请求响应时间过长(即长尾效应),针对性地优化这些慢操作,比如SQL查询优化、代码逻辑简化等。
多次采样与统计分析:
- 增加测试样本数量,执行多次性能测试取平均值,提高测试结果的可信度。
- 使用统计学方法分析测试结果,识别异常值,从而更准确地定位性能瓶颈。
固定时间窗口测试:
- 选择在业务低峰期进行测试,避开业务高峰期可能带来的外部干扰。
- 对于分布式系统,考虑各个节点间的时钟同步问题,以减少由于时钟漂移带来的不确定性。
硬件层优化
升级硬件配置:如增加内存容量以减少页面交换,提升CPU速度或核心数量来提高并发处理能力,使用高速SSD替代HDD改善I/O性能。
网络设备升级:提升网络带宽及降低网络延迟,优化网络架构设计以减少数据传输瓶颈。
数据存储优化:采用RAID技术提高磁盘读写性能,或者考虑使用分布式存储系统。
操作系统层优化
资源分配与限制:根据应用需求合理分配CPU、内存、IO等资源,对进程进行优先级设置,以及设定资源限制(如 ulimit)。
根据自己的业务场景,合理地设置 JVM 的内存空间以及垃圾回收算法可以提升系统性能。 例如,如果我们业务中会创建大量的大对象,我们可以通过设置,将这些大对象直接放进老年代。这样可以减少年轻代频繁发生小的垃圾回收(Minor GC),减少 CPU 占用时间,提升系统性能。
调整内核参数:例如调整TCP/IP栈参数、文件系统缓存大小、调度器策略等。
优化虚拟内存管理:合理设置swappiness值,确保合理的内存交换策略。
防止伪共享:通过缓存行对齐等方式避免多线程间的缓存一致性问题。
软件层优化
应用程序代码优化:包括代码优化、算法优化、优化设计、数据结构优化、时间换空间、减少不必要的计算、避免冗余操作、减少内存分配次数等。
代码优化:例如,某段代码导致内存溢出后导致JVM 中的内存占用不断增加,而 JVM 频繁发生垃圾回收,又会导致 CPU 100% 以上居高不下。
例如,使用 for 循环获取LInkedList集合中元素,会降低读的效率。因为在每次循环获取元素时,都会去遍历一次 List。应该改用 Iterator (迭代器)迭代循环该集合。
优化设计:面向对象有很多设计模式,可以帮助我们优化业务层以及中间件层的代码设计。优化后,不仅可以精简代码,还能提高整体性能。 例如,单例模式在频繁调用创建对象的场景中,可以共享一个创建对象,这样可以减少频繁地创建和销毁对象所带来的性能消耗。
时间换空间:省空间方案,有时候系统对查询时的速度并没有很高的要求,反而对存储空间要求苛刻,这个时候可以考虑用时间来换取空间。
例如, String 对象的 intern 方法,可以将重复率比较高的数据集存储在常量池,重复使用一个相同的对象。
并发与多线程优化:合理设计并发模型,充分利用多核CPU资源,减少锁竞争和上下文切换开销。
缓存策略优化:利用缓存(如Redis、Memcached等)减少数据库访问,提高缓存命中率。
数据库层优化
SQL查询优化:分析慢查询日志,优化SQL语句,减少全表扫描,建立合适的索引。
数据库架构优化:分库分表、读写分离、数据分区等手段减轻单点压力。
分库分表使用存储空间来提升访问速度。即将表数据通过某个字段 Hash 值或者其他方式分拆到不同的表,避免单表在存储千万数据以上时,读写性能会明显下降的问题。
调整数据库配置参数:如缓冲池大小、连接数限制、事务隔离级别等。
中间件与服务端优化
使用负载均衡:在服务器集群中部署负载均衡器,实现请求的均匀分布和故障转移。
异步处理与消息队列:将非实时任务异步化,使用消息队列解耦系统组件,提高系统吞吐量。
服务容器化与微服务化:采用容器技术如Docker提高资源利用率,通过微服务架构降低耦合度,便于独立扩展和运维。
监控与分析
定期进行性能测试和基准测试,识别瓶颈。
使用监控工具收集系统指标,并基于这些数据进行分析和决策。
日志分析:通过查看系统和应用日志发现问题,及时作出相应调整。
兜底策略
限流 + 动态扩容
第一,限流,对系统的入口设置最大访问限制。这里可以参考性能测试中探底接口的 TPS 。同时采取熔断措施,友好地返回没有成功的请求。
第二,实现智能化横向扩容。可以保证当访问量超过某一个阈值时,系统可以根据需求自动横向新增服务。
第三,提前扩容。这种方法通常应用于高并发系统,
例如,瞬时抢购业务系统。这是因为横向扩容无法满足大量发生在瞬间的请求,即使成功了,抢购也结束了。
目前很多公司使用 Docker 容器来部署应用服务。这是因为 Docker 容器是使用 Kubernetes 作为容器管理系统,而 Kubernetes 可以实现智能化横向扩容和提前扩容 Docker 服务。
探底接口的TPS
想要确定一个接口能够处理的最大事务数(Transactions Per Second,每秒交易数或每秒请求处理数)。要准确地探查一个接口的TPS极限,可以按照以下步骤进行:
负载测试:
- 使用性能测试工具(如Apache JMeter、LoadRunner、Locust等)模拟不同级别的并发用户访问该接口。
- 设定逐步增加的压力等级,从较低的并发用户数开始,逐渐提升到预期的高并发量。
监控系统资源:
- 在执行负载测试时,密切关注服务器资源使用情况,包括CPU、内存、磁盘I/O和网络带宽等。
- 同时也要关注数据库或其他后台服务的性能指标,确保了解整个系统的瓶颈所在。
分析响应时间和吞吐量:
- 观察随着压力增大,接口响应时间的变化以及整体的吞吐量(即TPS)是否还能继续增长。
- 当响应时间明显变长或者出现错误率上升时,说明系统可能已经接近或达到其性能瓶颈。
稳定期判断:
- 当TPS在某一并发级别上不再显著增加,并且响应时间持续恶化,则可认为进入了饱和区或瓶颈区。
- 这时候记录下的TPS值可以视作是当前配置下接口的理论最大TPS。
优化与调整:
- 根据测试结果找出性能瓶颈,可能是代码层面的问题,也可能是硬件资源不足,针对瓶颈进行优化。
- 优化后再次进行测试,直至达到预期的性能目标或者系统资源无法进一步提升为止。
安全阈值设定:
- 在实际应用中,通常不建议让系统长期运行在最大TPS的状态下,因为这可能会导致服务质量下降或者系统崩溃的风险加大。
- 因此,应当设定一个低于探底TPS的安全阈值,以保证在高负载下仍有足够的缓冲余地。