性能调优的目的:
1) 用压力测试暴露响应延迟问题,然后解决客户请求响应的问题。
2)性能优化可以降低服务器数量,同时提升性能指标。
没有必要在意性能优化,否则会降低开发进度,同时不会提升性能。我们只要在代码层面保证有效编码,比如减少IO操作,降低竞争锁的使用,高效的算法,已经设计模式(比如在设计商品价格的时候,有很多折扣,红包活动,这时候可以用装饰者模式去设计这个业务实现)。
根据产品经理提供的线上预期数据,我们可以通过压测,性能分析,统计工具来统计各项性能指标是否在预期范围之内。
上线之后,我们要通过监控系统来判断线上的性能问题,最后根据日志来判断问题并解决。
CPU: 有些计算任务会长时间,不间断的占用CPU资源,导致其它任务无法争夺到CPU资源,这种情况有下面几种:
内存:JVM中的堆内存来存储类对象,如果对象没有及时收回,就会导致内存溢出的问题。
磁盘I/O: 磁盘IO的读写速度慢于内存,频繁的大量的磁盘IO会导致响应变慢。
网络:网络带宽不够也会造成性能瓶颈。
异常:java异常的捕获和处理是非常消耗资源的,如果程序高频率地在异常处理也会影响系统。
数据库:大量的数据库读写操作会导致磁盘I/O性能瓶颈,进而导致数据库响应的延迟。
锁竞争:
在并发编程中,我们经常会需要多个线程,共享读写操作同一个资源,这个时候为
了保持数据的原子性(即保证这个共享资源在一个线程写的时候,不被另一个线程修改),我们就会用到锁。锁的使用可能会带来上下文切换,从而给系统带来性能开销。Java 为了降低锁竞争带来的上下文切换,对 JVM 内部锁做了多次优化,例如,新增了偏向锁、自旋锁、轻量级锁、锁粗化、锁消除等。而如何合理地使用锁资源,优化锁资源,就需要你了解更多的操作系统知识、Java 多线程编程基础,积累项目经验,并结合实际场景去处理相关问题。
响应时间越短,性能越好,一般一个接口的响应时间是在毫秒级。在系统中,我们可以把响应时间自下而上细分为以下几种:
TPS是业务层面衡量吞吐量的最重要指标,表示每秒事务处理量。TPS 越大越好,吞吐量自下而上分两种:磁盘吞吐量和网络吞吐量。
磁盘吞吐量,也有两个指标:
一个是IOPS(Input/Output Per Second), 指每秒读写次数,单位时间内系统能处理的IO请求数,IO请求通常为读或写数据操作请求,关注随机读写性能。适用于随机读写频繁的应用。如,小文件存储(图片,视频等),OLTP数据库,邮件服务等。
另一个是数据吞吐量,单位时间呃逆可以成功传输的数据量。对应大量顺序读写频繁的应用,传输大量连续数据,如,视频点播,数据吞吐量是关键指标。
网络吞吐量:
指网络传输时没有帧丢失的情况下,设备能够接受的最大数据速率。网络吞吐量不仅仅跟带宽有关系,还跟 CPU 的处理能力、网卡、防火墙、外部接口以及 I/O 等紧密关联。而吞吐量的大小主要由网卡的处理能力、内部程序算法以及带宽大小决定。
上述指标整体来看就要按木桶理论来考虑,如果其中任何一块木板出现短板,任何一项分配不合理,对整个系统性能的影响都是毁灭性的
当系统压力上升时,系统响应时间的上升曲线是否平缓。如果响应时间出现了突然暴涨,那么可能就是系统所能承受的负载极限,例如,当你对系统进行压测时,系统的响应时间会随着系统并发数的增加而延长,直到系统无法处理这么多请求,抛出大量错误时,就到了极限。
迭代过程中,充分保证系统的稳定性给你延伸一个方法,就是将迭代之前版本的系统性能指标作为参考标准,通过自动化性能测试,校验迭代发版之后的系统性能是否出现异常,这里就不仅仅是比较吞吐量、响应时间、负载能力等直接指标了,还需要比较系统资源的 CPU 占用率、内存使用率、磁盘I/O、网络 I/O 等几项间接指标的变化。
调优策略三个阶段:测试-分析-调优
针对某一个模块或一个方法在不同实现方式下的性能对比。比如,对比一个方法在同步和非同步实现的性能对比。
这是一个综合测试场景,需要考虑到测试环境,测试环境和测试目标。
首先测试环境需要模拟线上的真实环境。
选择测试场景时,我们需要确定在测试某个接口时,是否有其他接口也同时有请求,造成对这个接口测试的干扰。如果有必须考虑到,否则测试结果会有偏差。
最后设定测试目标:包括吞吐量,响应时间来衡量系统服务是否达标。不达标就要进行优化,达标就要加大测试并发数,探底接口的TPS。这样做可以了解接口性能。同时要观察各个服务器的CPU,内存已经I/O使用率的变化。
先简单介绍一下 java 应用运行原理
我们知道java class文件编译后会生成 .class 文件,但.class 文件并不是能够直接执行的机器码。java 应用在运行的时候,并不是把.class 都转换为机器码文件,而是运行应用的时候通过解释器实时把.class 文件转换为机器码文件,然后执行机器码。这样做是为了节约内存和执行效率。而当jvm观察到有些.class 文件执行的频率比较频繁,那么就会把.class 文件编译转换为机器码并加载到内存中,这样就可以实时运行机器码了,这样就加快的执行效率。
所以,我们要考虑到当第一个请求过来的时候,可能会造成应用执行比较慢,这时就需要我们优化一下。
当我们用同样的测试数据集进行性能测试的时候,会出现每次测试结果不一致,这个现象很正常,比如被机器其它进程影响,网络波动,以及被不同阶段的垃圾回收影响等。
如果我们的一个机器上部署了多个应用,比如部署在不同的 tomcat 上,那么不同的 JVM 直接肯定会有一定的影响。所以,建议不要部署多个 JVM 在一个机器上进行性能测试。
完成性能测试后,会输出一份性能测试报告,包括测试接口的平均,最大和最小吞吐量,响应时间,服务器的CPU, 内存,网络 I/O 使用率,JVM 的 GC 频率。
分析查找问题时,需要自下而上的方式定位问题,首先从操作系统层面,查看系统的 CPU, 内存,I/O, 网络的使用率是否有异常。再找异常日志来定位具体问题。
看完操作系统层面,再来分析 JVM 层面的问题,查看垃圾回收频率,以及内存分配情况,分享日志,确定问题。
如果操作系统和 JVM 都没有问题,那么我们就要看具体的java应用了,例如 java 编程问题,读写数据瓶颈等。
分析问题是从下而上的,而具体的调优是采用自上而下的。下面几种方法是从应用到操作系统层面的优化策略:
一种很容易发现并暴露出来。比如,我们某段代码导致内存溢出,往往是 JVM 中的内存用完了, 这个时候会引发 JVM 频繁的进行垃圾回收,那么频繁的进行垃圾回收会导致 CPU 100% 以上高居不下。
另一种很难暴露出来。需要我们依靠经验来判断,比如 LinkedList 集合,如果使用 for 循环遍历该容器,将大大降低读的效率,但这种效率的降低很难导致系统性能参数异常。如果改用 Iterator (迭代器)迭代循环该集合会好很多,这是因为 LinkedList
是链表实现的,如果使用 for 循环获取元素,在每次循环获取元素时,都会去遍历一次List,这样会降低读的效率。
面向对象有许多设计模式,可以帮助优化业务层以及中间层的代码设计。不仅能精简代码还能够提升整体的性能。比如,单例模式可以共享一个对象,这样在频繁调用创建对象的场景中可以减少创建和销毁对象所带来的性能消耗。
好的算法可以帮助我们大大提升系统性能,比如,用合适的查找算法可以降低时间复杂度。
应用对查询的速度没有太高的要求,但是对于内存空间要求比较高,这时就需要用时间来换空间。
如,用 String 对象的intern 方法, 可以将重复率比较高的数据集存储在常量池,重复的使用一个相同的对象,这样可以大大节省内存存储空间。但常量池使用的是 HashMap 数据结构类型, 如果我们存储数据过多,查询的性能就会下降,所以在这种对存储量要求比较苛刻,但对查询速度不做要求的场景,可以用时间来换空间。
比如 mysql 对于千万以上的数据量响应的速度很慢,我们这时就可以考虑用分库分表的方法把 mysql 单库单表的数据切分为多库多表的数据。
数据通过某个字段 Hash 值或者其他方式分拆,系统查询数据时,会根据条件的 Hash 值判断找到对应的表,因为表数据量减小了,查询性能也就提升了。
JVM、Web 容器以及操作系统的优化也是非常重要的。
根据自己的业务场景,合理地设置 JVM 的内存空间以及垃圾回收算法可以提升系统性能。
例如,如果我们业务中会创建大量的大对象,我们可以通过设置,将这些大对象直接放进老年代。这样可以减少年轻代频繁发生小的垃圾回收(Minor GC),减少 CPU 占用时间,提升系统性能。
Web 容器线程池的设置以及 Linux 操作系统的内核参数设置不合理也有可能导致系统性能瓶颈。
当上面所有的策略都做了,我们仍然需要兜底策略来防范外部请求的变化造成的不稳定因素。
限流:以性能测试中 TPS 的结果为参考,设置系统的最大访问限制。
智能化横向扩容:智能化横向扩容可以保证当访问量超过某一个阈值时,系统可以根据需求自动横向新增服务。
提前扩容:这种方法通常应用于高并发系统,例如,瞬时抢购业务系统。这是因为横向扩容无法满足大量发生在瞬间的请求,即使成功了,抢购也结束了。目前很多公司使用 Docker 容器来部署应用服务。这是因为 Docker 容器是使用
Kubernetes 作为容器管理系统,而 Kubernetes 可以实现智能化横向扩容和提前扩容Docker 服务。