4.寻找性能瓶颈
-
资源主要消耗在CPU,文件IO,网络IO以及内存方面,机器的资源是有限的,当某资源消耗过多时,通常会造成系统的响应速度慢。
外部处理的性能不够主要是所调用的其他系统提供的功能或数据库操作的响应速度不够。
资源消耗不多,但程序的响应速度仍达不到要求的主要原因是程序代码运行效率不够高,未充分使用资源或程序结构不合理。 -
CPU消耗分析:
在Linux中,CPU主要用于中断、内核以及用户进程的任务处理。优先级为中断>内核>用户进程
上下文切换: 每个CPU在同一时间只能执行一个线程,Linux采用的是抢占式调度。即为每个线程分配一定的执行时间,当达到执行时间、线程中有IO阻塞或高优先级线程要执行时,就将切换执行的线程,在切换时要储存目前线程的执行状态,并恢复要执行的线程的状态。 对于Java应用而言,在进行文件IO、网络IO、锁等待或线程sleep时,当前线程会进入阻塞或休眠状态,从而触发上下文切换,上下文切换过多会造成内核占据较多的CPU使用,使得应用的响应速度下降。
运行队列: 系统的load主要由CPU的运行队列来决定,运行队列值越大,就意味着线程会消耗越长的时间才能执行完,建议为1-3个。
利用率: CPU利用率为CPU在用户进程、内核、中断处理、IO等待、空闲5个部分使用百分比。
在Linux中,可使用top或pidstat方式来查看进程中线程的CPU的消耗状况。
相较于top命令而言,pidstat的好处为可查看每个线程的具体CPU利用率的状况。
us表示用户进程,sy表示内核线程,ni表示被nice命令改变优先级的任务,id(idle)表示CPU的空闲时间,wa(wait)表示在执行的过程中等待IO,hi(hardware interrupt)表示硬件中断,si表示软件中断
1.当us值过高时,表示运行的应用消耗了大部分的CPU。重要的是找到具体消耗所执行的代码。
解决方法:首先通过linux提供的命令找到消耗cpu严重的线程及其ID,将此ID转化为十六进制的值,之后通过kill -3或jstack的方式dump出引用的java线程信息,通过之前的值找到对应的nid值的线程。在采样时须多执行几次上诉的过程,以确保找到真实的消耗cpu的线程。
2.造成us高的原因主要是线程一直处于可运行状态,通常是这些线程在执行无阻塞、循环、正则或纯粹的计算等动作造成的,也可能是频繁的GC。如每次请求都需要分配较多内存,当访问量高的时候就将导致不断地GC,系统响应速度下降,进行造成堆积的请求更多,消耗的内存更严重。
3.由于jstack需要时间,因此基于jstack并不一定能分析出真正的耗cpu的代码是哪行。
CPU us高的解决方法:
1.原因主要是执行线程无任何挂起动作,且一直执行,导致cpu没有机会去调度执行其他的线程,造成线程饿死的现象。对于这种情况,常见的一种方式是对这种线程的动作增加Thread.sleep,以释放cpu的执行权,降低cpu的消耗。 当然,这种修改方式是以损失单次执行性能为代价的,但由于降低了cpu的消耗,对于多线程的应用而言,反而提高了总体的平均性能。
2.还有一种经典的场景是状态的扫描,例如某线程要等其他线程改变了值后才可继续执行,最佳的方式是改用wait/notify机制。
3.对于其他类似循环次数太多、正则、计算等造成的us过高的状况,则要结合业务需求来进行调优。
对于GC频繁造成的us高的现象,则要通过JVM调优或程序调优,降低GC的执行次数。2.当sy值过高时,表示Linux花费了更多的时间在进行线程切换。最重要的是找出线程要不断切换状态的原因。可通过kill -3或jstack的方式dump出线程信息,找出等待状态或锁竞争过多的线程。通过对应的堆栈信息。
CPU sy高的解决方法:
1.sy高的原因主要是线程的运行状态要经常切换,最简单的优化方法时减少线程数。
2.使用线程池避免要不断地创建线程,如应用要支撑大量的并发,在减少线程数的情况下最好是增加一个缓冲队列,避免因为线程数的减少造成系统出错率上升。
3.还有一个重要的原因是线程之间锁竞争激烈,造成了线程状态经常要切换。锁竞争更有可能造成系统资源消耗不多,但系统性能不足的现象。
4.除了以上2种情况外,还有一种典型现象是应用中有较多的网络IO操作或缺失需要一些锁竞争机制(数据库连接池),但为了能够支撑高的并发量,在应用中又只能借助启动更多的线程来支撑。这样,当并发量增长到一定程度后,可能会造成cpu sy高的现象,对于这种现象,可采用协程来支撑更高的并发量,避免造成sy消耗严重,系统load迅速上涨和系统性能下降。
5.当线程中有任何的阻塞动作,这个线程就会被挂起,但仍然占据着线程的资源,当线程中的阻塞动作完成时,就由操作系统来恢复线程的上下文,并调度执行。当并发量上涨后,有可能导致启动的大量线程都处于浪费状态。一方面导致了其他的请求只能放在缓冲队列中等待执行,性能下降;另一方面是造成系统中线程切换频繁,cpu运行队列过长,协程要改变的就是不浪费相对宝贵的原生线程资源。
6.采用协程后,能做到当线程等待数据库执行结果时,就立即释放此线程资源给其他请求,等到数据库执行结果返回后才继续执行。 -
文件IO消耗分析
1.Linux在操作文件时,将数据放入文件缓存区,直到内存不够或系统要释放内存给用户进程使用,因此在查看内存状况时经常会发现可用(free)的物理内存不多,但cached用了很多,这是Linux提升文件IO速度的一种做法。在这样的做法下,如果物理空闲内存够用,通常只有写文件和第一次读取文件时会产生真正的文件IO。
2.在Linux中,要跟踪线程的文件IO的消耗,主要是通过pidstat来查找。iostat只能查看整个系统的文件IO消耗情况,无法跟踪到进程的文件IO消耗状况。Device表示设备卷标名或分区名,tps表示每秒的IO请求数。
3.iostat:await表示平均每次IO操作的等待时间,avgqu-sz表示等待请求的队列的平均长度,svctm表示平均每次设备执行IO操作的时间,util表示一秒之中有百分之几用于IO操作。在使用iostat查看IO的消耗情况时,首先要关注的是cpu中iowait%所占的百分比,当iowait占据了主要的百分比时,就表示要关注IO方面的消耗状况了,这时可以再通过iostat -x来详细地查看具体状况。
4.当文件IO消耗过高时,最重要的是找到造成文件IO消耗高的代码,方法:通过pidstat直接找到文件IO操作多的线程,之后结合jstack找到对应的Java代码。
5.造成文件IO消耗严重主要是多个线程需要进行大量内容写入、磁盘设备本身的处理速度慢、文件系统慢、操作的文件本身已经很大。
文件IO消耗严重的解决方法
1.从程序角度而言,造成文件IO消耗严重的原因主要是多个线程在写大量的数据到同一文件,导致文件很快变得很大,从而写入速度越来越慢,并造成各线程激烈争抢文件锁。解决方法:
1.异步写文件:将写文件的同步动作改为异步动作。
2.批量读写:频繁的读写操作对IO消耗会很严重,批量操作将大幅度提升性能。
3、限流:将文件IO消耗控制到一个能接受的范围。
4.限制文件大小:对于每个输出的文件,都应做大小的限制,在超出最大值后可生成一个新的文件。
5.尽可能采用缓冲区等方式来读取文件内容,避免不断与操作系统交互。 -
网络IO消耗分析
1.对于分布式Java应用而言,网络IO的消耗非常值得关注,尤其要注意网卡中断是不是均衡地分配到各cpu的。linux可采用sar来分析网络IO的消耗状况。
2.由于没办法分析具体每个线程所消耗的网络IO,因此当网络IO消耗高时,只能对线程进行dump,查找产生了大量网络IO操作的线程。这些线程的特征是读取或写入网络流,在Java实现网络通信时,通常要将对象序列化为字节流进行发送或读取字节流,并反序列化为对象。这个过程要消耗JVM堆内存,JVM堆的内存大小通常是有限的,所以一般不会造成网络IO消耗严重。
网络IO消耗严重的解决方法:
1.从程序角度而言,主要原因是同时需要发送或接收的包太多,常用的调优方法为进行限流,通常是限制发送packet的频率,从而在网络IO消耗可接受的情况下来发送packet。 -
内存消耗分析
1.只有在创建线程和使用Direct ByteBuffer才会操作堆外的内存。对于堆以外的内存方面的消耗,最值得关注的是swap的消耗以及物理内存的消耗,可基于os提供的命令来查看。
2.vmstat:swpd表示虚拟内存已使用的部分,swap下的si表示每秒从硬盘读至内存的数据量,so表示每秒从内存中写入硬盘的数据量。 swap值过高通常是物理内存不够用了,os将物理内存中的一部分数据转为放入硬盘进行储存,以腾出足够的空间给当前运行的程序使用。在情况变化后,即从硬盘重新读取数据到内存中,以便恢复程序的运行,这个过程会产生swap IO,因此看swap的消耗情况主要要关注的是swap IO的状况。
由于Java应用是单进程应用,因此只要JVM的内存设置不要过大,是不会操作到swap区域的。物理内存消耗过高可能是由于JVM内存设置过大,创建的Java线程过多或通过Direct ByteBuffer往物理内存中放置了过多的对象造成的。
3.sar相比vmstat的好处是可以查询历史状况,以更加准确地分析趋势状况,vmstat和sar的共同弱点是不能分析进程所占用的内存量。
4.top:通过top可查看进程所消耗的内存量,不过看到的Java进程的消耗内存是包括了JVM已分配的内存加上Java应用所耗费的JVM以外的物理内存,这导致top中看到的内存大小可能超过-Xmx加上MaxPermSize设置的内存大小。
5.pidstat: 通过pidstat也可查看进程所消耗的内存量。
6.结合top和jstat观察到的状况,堆一旦使用后,即使进行了GC,进程中仍然会显示之前其所消耗的内存大小,因此JVM内存中具体的消耗状况必须通过jdk提供的命令才能准确分析。
7.在Java程序出现内存消耗过多,GC频繁或OOM的情况后,要首先分析其耗费的是JVM外的物理内存还是堆,如是物理内存,则要分析程序中线程的数量以及Direct ByteBuffer的使用情况;如为堆区,则要结合JDK提供的工具或外部的工具来分析具体对象的内存占用状况。
对于内存消耗严重的情况
1.释放不必要的引用:代码持有了不需要的对象引用,造成这些对象无法被GC,从而占据了堆内存。如在复用线程的情况下使用ThreadLocal,由于线程复用,ThreadLocal中存放的对象如未做主动释放的话不会被GC。
2.使用对象缓存池:创建对象的实例要耗费一定的cpu以及内存,使用对象缓存池可降低堆内存的使用。
3.采用合理的缓存失效算法:如果太多的对象放在缓存池中,反而会造成内存的严重消耗。同时由于缓存池一直对这些对象持有引用,从而会造成Full GC增多。
控制缓存池的大小的问题在于当达到缓存池的最大容量后,如果要加入新的对象该如何处理。有一些经典的缓存失效算法来清除缓存池中的对象,例如FIFO,LRU,LFU等,可避免缓存池中的对象数量无限上涨。
4.合理使用软引用和虚引用 -
程序执行慢原因分析
1.有些情况是资源消耗不多,但程序执行仍然慢,多出现于访问量不是很大的情况。原因:
1.锁竞争激烈:直接就会导致程序执行慢。如数据库连接池提供的连接数是有限的。
2.未充分使用硬件资源:如机器上有双核CPU,但程序中都是单线程串行的操作。
3.数据量增长
对于资源消耗不多,但程序执行慢的情况
1.锁竞争激烈:线程多了后,锁竞争的状况会比较明显,这时线程很容易处于等待锁的状况,从而导致性能下降以及CPU sy上升。降低线程间锁竞争的方法:
1.使用并发包中的类
2.使用Treiber算法:只要用于实现无阻塞Stack
3.使用Michael-Scott非阻塞队列算法,同上也是基于CAS以及AtomicReference来实现队列的非阻塞操作。
4.尽可能少用锁
5.拆分锁,即把独占锁拆分为多把锁,常见的有读写锁拆分以及ConcurrentHashMap中默认拆分为16把锁的方法。很大程度上能提高读写的速度,但全局性质会变得比较复杂。
6.去除读写操作的互斥锁,在修改时加锁,并复制对象进行修改,修改完毕后切换对象的引用,而读取时则不加锁。对于读多写少的应用非常适合,但由于写操作每次都要复制一份对象,会造成更多的内存消耗。
2.未充分使用硬件资源
1.未充分使用cpu,主要是在能并行处理的场景中未使用足够的线程
2.未充分使用内存,如数据的缓存,耗时资源的缓存,在内存资源消耗可接受的情况下,应充分使用内存来缓存数据,提升系统的性能
在寻找到系统的性能瓶颈后,接下来的步骤就是调优,以提高系统性能,调优通常可以从硬件、操作系统、JVM、程序四个方面来着手。