关于性能调优
为什么要进行性能调优?
很多产品在初期业务量较小,几乎不需要关注性能问题。但是随着时间的积累系统愈发复杂,系统负载也会逐渐增大,如果不关注应用性能,那么应用迟早会崩溃,就像是一个定时炸弹一样。同时进行性能优化还能带来一个显著的好处就是降本增效,可以缩减服务器规模的同时任然保持现有的服务能力。
性能指标
一、硬件层
CPU
CPU大家都很熟悉,如果CPU占用率过高必然导致程序卡慢甚至崩溃。常见的问题比如频繁FULL GC、代码死循环等。
RAM
RAM就是我们常说的内存,过度使用内存也会导致服务器运行缓慢。常见的问题比如内存泄漏、内存溢出等。
磁盘 I/O
磁盘的速度远低于内存,如果程序大量使用磁盘I/O可能会导致程序出现性能瓶颈,索然SSD的普及加速了磁盘I/O的效率但是依然是远不及内存的I/O效率。常见的问题有依赖磁盘I/O导致的系统响应变慢甚至是发生阻塞导致系统吞吐量大幅降低等。
网络 I/O
网络性能对于用户体验的影响是巨大的,如果服务器网络带宽太低,数据传输量还较大就会导致应用出现严重的性能瓶颈。后端应用服务器的带宽是很昂贵的,不要使用后端应用服务器传输资源文件(比如表格、压缩包、音视频等)。比如需要上传文件,一般都是直接请求文件服务器,而应用服务器只处理文件服务器返回的文件ID。
其他
硬件层的性能指标远不及上面几个,比如部分服务器会有GPU等其他硬件。但是对于Java应用来说基本只会使用到CPU、RAM、磁盘和网络。
二、软件层
数据库
主流程序几乎没有不依赖于数据库的,所以应用性能也极大的依赖于数据库的性能。
锁竞争
目前大多数服务器和服务器软件都是多核心、多线程运行的。多线程环境不可避免的会出现锁竞争,这绝不仅限于JVM内部的锁竞争。如果未处理好锁竞争问题可能会出现死锁、饥饿、阻塞等严重的性能问题。
其他
软件层的性能指标还有很多,严格意义上服务器操作系统本身也属于软件,这里只列举几个常用且具有代表性的。
应用层
响应时间(RT)
响应时间就是一次请求从客户端发出到收到响应的总耗时。响应时间是衡量系统性能的重要指标之一,响应时间越短,性能越好,一般接口的响应时间是在毫秒级。
响应时间细分:
- 数据库响应时间:数据库操作所消耗的时间,往往是整个请求链中最耗时的。
- 服务端响应时间:服务端包括Nginx分发的请求所消耗的时间以及服务端程序执行所消耗的时间。
- 网络响应时间:这是网络传输时,网络硬件需要对传输的请求进行解析等操作所消耗的时间。
- 客户端响应时间:对于普通的 Web、App 客户端来说,消耗时间是可以忽略不计的。
吞吐量(TP)
吞吐量指服务器在单位时间内处理的请求数量。较高的吞吐量通常表示服务器具有更好的性能。比如磁盘吞吐量、网络吞吐量等。
- TPS(每秒事务处理量):表示系统每秒钟能成功处理的事务数量。
- QPS(每秒查询量):衡量系统每秒钟能够响应的查询次数,尤其适用于Web服务或数据库查询场景。
并发用户数(CCU)
并发用户数是指在同一时间段内同时连接或访问系统的用户数量。这个指标通常用于评估系统在处理多用户同时请求时的性能表现和承载能力。
成功率(SR)
- 请求成功率(RSR):在一定时间内失败请求占总请求的比例。
- 事务成功率(TSR):衡量系统或服务在处理事务过程中的成功率(比如一次支付过程)。
稳定性与可用性
- 服务级别协议(SLA):检查系统是否达到预定义的服务级别目标。
- 平均无故障时间(MTTF):系统在两次故障之间的平均工作时间。
- 平均修复时间(MTTR):系统发生故障后恢复到正常运行状态所需的平均时间。
调优步骤
微基准性能测试
微基准性能测试可以精准定位到某个模块或者某个方法的性能问题,用于测试某个小模块或方法的性能。比较适用于系统迭代增量部分的性能测试。比如系统新增了一个新版接口,可以通过微基准性能测试来验证新接口的性能。
宏基准性能测试
宏基准性能测试是一个综合测试,需要考虑到测试环境、测试场景和测试目标等。
- 测试环境:一般需要模拟出线上的环境才能得到精确的性能数据。
- 测试场景:同一个功能会出现很多不同的场景,拿个最简单的例子。测试支付接口的时候正常情况下测试和大促场景(同时需要处理大量支付请求)的测试得出的性能数据是截然不同的。所以测试需要贴合实际场景。
- 测试目标:可以是TPS、QPS、RT等组合指标,如果达不到目标就需要进行优化。
干扰因子:
- 热身问题:在Java编程语言和环境中,
.java
文件编译成为.class 文件后
,机器还是无法直接运行.class
文件中的字节码,需要通过解释器将字节码转换成本地机器码才能运行。为了节约内存和执行效率,代码最初被执行时,解释器会率先解释执行这段代码。随着代码被执行的次数增多,当虚拟机发现某个方法或代码块运行得特别频繁时,就会把这些代码认定为热点代码(Hot Spot Code
)。为了提高热点代码的执行效率,在运行时,虚拟机将会通过即时编译器(JIT compiler,just-in-time compiler
)把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后存储在内存中,之后每次运行代码时,直接从内存中获取即可。所以在刚开始运行的阶段,虚拟机会花费很长的时间来全面优化代码,后面就能以最高性能执行了。这就是热身过程对测试数据的干扰。 - 性能测试结果不稳定:同样的测试结果可能不同,因为测试期间有很多不稳定因素。比如网络波动、Jvm是否正处于GC阶段等等。因此想要得到相对正确的性能数据需要多次测试取平均值。
分析结果
完成性能测试之后,需要输出一份性能测试报告,帮我们分析系统性能测试的情况。其中测试结果需要包含测试接口的平均、最大和最小吞吐量、响应时间、CPU、RAM、磁盘 I/O、网络 IO 、JVM内部指标等等。
通过观察这些调优标准,可以发现性能瓶颈,我们再通过自下而上的方式分析查找问题。首先从操作系统层面,查看系统的 CPU、RAM、I/O、网络的使用率是否存在异常,再通过命令查找异常日志,最后通过分析日志,找到导致瓶颈的原因。还可以从JVM层面,查看JVM的垃圾回收频率以及内存分配情况是否存在异常,分析日志,找到导致瓶颈的原因。
分析查找问题是一个复杂而又细致的过程,某个性能问题可能是一个原因导致的,也可能是几个原因共同导致的结果。我们分析查找问题可以采用自下而上的方式,而我们解决系统性能问题,则可以采用自上而下的方式逐级优化。
调优方法
- 代码优化:应用层的问题代码往往会因为耗尽系统资源而暴露出来。例如我们某段代码导致内存泄漏等。这类问题比较容易定位到有Bug的代码。还有一些非Bug引起的性能问题,比如使用
+
拼接大量的字符串,或是使用for
循环遍历LinkedList
(因为LinkedList是链表结构没有索引)等都会导致系统性能低下。 - 设计优化:可以通过合理的设计模式优化代码设计,提高整体性能。例如当需要创建大量相似对象时,可以使用享元模式来共享对象以节省内存和提高性能。
- 数据库优化:包括索引优化、表结构设计优化、分库分表读写分离等物理优化等。
- JVM调优:通过调整JVM参数,找到最适合当前业务的JVM配置,提升整体性能。
- 硬件优化:可以根据当前的性能瓶颈针对性的调整硬件资源,比如可以根据业务场景降低CPU核心数增加网络带宽等方式优化,避免木桶效应。