以下部分内容整理自《分布式Java应用:原理与实践》——林昊
随着系统自身数据量的增长,访问量增加,系统的响应通常会越来越慢,或者是新的功能在性能上无法满足修去,这个时候需要对系统进行性能调优。调优是一个复杂的过程,涉及的方面有:硬件,操作系统,运行环境软件和应用本身。
调优步骤:
调优前,首先要做的是衡量系统现状,这也是判断调优结果的依据。衡量系统状态一般包含以下几个方面:
例如A系统目前每秒钟请求次数1000次,平均响应时间为1秒。
在有了系统现状后,可设定调优目标。通常调优目标是根据用户所能接受的响应速度或者系统所拥有的机器以及所支撑的用户量制定出来的。因此通常会设定出调优目标:所有请求要在500ms内返回。
在设定了调优目标后,要做的就是寻找性能瓶颈,这一步通常是最难的,可以结合一些工具来找出造成瓶颈点的代码。
找到瓶颈点代码后,通常要分析其需求场景结合一些优化技巧,制定优化策略。优化策略通常也会有多个,如何选择?一般要收益比较高的(优化后的预期效果/优化需要付出的代价)。
优化后,部署项目,检查时候达成目标。如果已经达成,则结束本次调优;如果没有达成,则需要继续寻找瓶颈点,或者考虑之前没有考虑的优化策略,继续优化,直到达成目标。
性能瓶颈的表现一般有资源消耗过多,外部处理系统的性能不足,或者资源消耗不多,但程序响应速度达不到要求。资源一般消耗在CPU,文件IO,网络IO以及内存方面。机器的资源是有限的,当某个资源消耗过多时,通常会造成系统的响应速度慢。
在Linux中,CPU主要用于中断,内核以及用户进程的任务处理,优先级为中断>内核>用户进程。
每个CPU(或者多核CPU中的每核CPU)在同一时间通常只能执行一个线程,Linux采用的是抢占式调度,即为每个线程分配一定的执行时间,当到达执行时间、线程中有IO阻塞或者有高优先级线程要执行时,Linux将切换执行的线程,在切换时要存储当前线程的执行状态,并恢复要执行线程的状态,这个过程被称为上下文切换。对于Java应用,典型的是在文件IO操作、网络IO操作、锁等待或者线程Sleep时,当前线程会进入阻塞或者休眠状态,从而触发上下文切换,上下文切换过多会造成内核占据较多的CPU使用,使得应用相应速度下降。
每个CPU核都维护了一个可运行的线程队列,例如一个4核的CPU,Java应用中启动了8个线程,且这8个线程都处于可运行状态,那么在分配平均的情况下,每个CPU核运行队列里就会有两个线程。通常而言,系统的load主要由CPU的运行队列来决定,假设以上情况维持了一分钟,那么这一分钟系统的load就是2。但load是一个复杂的值,因此这也不是绝对的,运行队列值越大,意味着线程要消耗越长对的时间才能执行完。Linux System and NetWork performance Monitoring 中建议控制在每个CPU核上的运行队列为1-3个。
CPU利用率为CPU在用户进程、内核、中断处理、IO等待以及空闲五个部分使用百分比,这五个值是用来分析CPU消耗情况的关键指标。Linux System and NetWork performance Monitoring 中建议用户进程的CPU消耗/内核的CPU消耗的比率在65%-70%/30%-35%左右。
在Linux中,可通过top或者pidstat命令来查看CPU消耗状况。
输入top命令后即可查看CPU的消耗情况,CPU的汇总信息如下:
top - 15:16:34 up 728 days, 1:33, 19 users, load average: 0.39, 0.43, 0.59
Tasks: 576 total, 2 running, 567 sleeping, 5 stopped, 2 zombie
Cpu(s): 14.9%us, 0.7%sy, 0.0%ni, 84.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 264484524k total, 111250372k used, 153234152k free, 13148064k buffers
Swap: 16777208k total, 0k used, 16777208k free, 68996980k cached
此处,需要关注的是第三行信息,其中
对于多个或者多核CPU,上面的显示则会是多个CPU所占用的百分比综合,因此会出现180%us这样的现象。如果需要查看每个核心的消耗情况,可进入top后按 1,就会按核心来显示消耗情况。如下:
top - 15:31:12 up 23:49, 1 user, load average: 0.03, 0.02, 0.00
Tasks: 211 total, 1 running, 210 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 3050276 total, 1505612 free, 417000 used, 1127664 buff/cache
KiB Swap: 3176444 total, 3176444 free, 0 used. 2324580 avail Mem
默认情况下,top是按照进程来显示CPU消耗的,如果需要按照线程查看,可进入top后按 shift+h
,就会按照线程查看CPU消耗情况。如下:
top - 15:39:06 up 23:57, 1 user, load average: 0.08, 0.06, 0.02
Threads: 357 total, 1 running, 356 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.1 sy, 0.0 ni, 99.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 3050276 total, 1505032 free, 416576 used, 1128668 buff/cache
KiB Swap: 3176444 total, 3176444 free, 0 used. 2324896 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1844 mysql 20 0 1239012 136296 15476 S 0.3 4.5 0:04.47 mysqld
2344 root 20 0 647140 38368 27164 S 0.3 1.3 0:15.53 dockerd
2422 root 20 0 647140 38368 27164 S 0.3 1.3 0:13.78 dockerd
pidstat 是 sysstat 中的工具,要使用 pidstat ,需要先安装 sysstat。
输入 pidstat 1 2,在console上将会每隔1秒输出目前活动进程的CPU消耗情况,共输出2次。
如果想查看某进程中线程的CPU消耗情况,可以输入 pidstat -p pid -t 1 5
总结:当CPU消耗严重时,主要体现在us 、sy 、wa 或hi 的值变高,wa的值是IO等待造成的,hi 的值变高主要是硬件中断造成的,例如网卡接收数据频繁。
对于java应用而言,CPU消耗严重主要体现在us 、sy两个值上。
当us值过高时,表示运行的应用消耗了大部分的CPU。在这种情况下,对于Java应用而言,最重要的是找到具体消耗CPU的线程所执行的代码。可采用如下方法。
首先通过Linux命令找到CPU消耗严重的线程及ID,将次ID转为十六进制,之后通过kill -3 javapid
或 jstack pid | grep 'nid=ID'
找出具体信息,前面两个命令需要多次使用,以确定找到真实消耗CPU的线程和代码。
当sy 值高时,表示 Linux花费了太多的时间在上下文切换上,Java应用造成这种情况的原因是启动的线程比较多,且这些线程多数处于要不断阻塞(例如锁等待,IO等待)和执行状态变化过程中,这就导致了操作系统需要不断地切换执行线程,产生大量的上下文切换。
Linux在操作文件时,将数据放入文件缓冲区,直到内存不够或系统要释放内存给用户进程使用。因此在查看Linux内存状况时,经常会发现可用(free)的物理内存不多,但是 cached 用了很多,这是Linux提升文件IO速度的一种做法。在这种做法下,如果物理空闲的内存够用,通常只有在写文件和第一次读取文件时才会产生真正的文件IO。
查看进程文件IO还是使用 pidstat 命令:pidstat -d -t -p pid 1 100
,其中,KB_rd/s 表示每秒读取的KB数,KB_WR/s 表示每秒写入的KB数。
对于分布式java应用或使用微服务的应用,网络IO的消耗也值得关注。
在Linux中可采用sar来分析网络IO的消耗状况。sar -n ALL 1 2
执行后以1秒为频率一共输出两次网络IO的情况。
Java内存的消耗主要是在JVM堆内存上,在正式环境中,多数Java应用都会讲-Xms和-Xmx设置为相同的值,避免运行期间不断地申请内存。对于java内存消耗状况的分析,我会单独再写。
需要注意的是,在应用启动时设置的-Xmx的值对堆外内存是没有效果的。你可以使用DirectByteBuffer
实现对堆外内存的操作,对应java的类为ByteBuffer
。
有些情况是资源消耗不多,但是程序仍然执行慢,这种现象多出现于访问量不是很大,消耗多是应用内耗产生的。原因主要有以下三种。
例如数据库连接池提供的连接数通常是有限的,假设提供了10个连接,但如果此时有50个 线程要进行数据库操作,那么就会造成另外的40个线程处于等待状态。
例如机器是四核的,但程序都是单线程串行操作,并没有充分发挥硬件资源的作用,此时可以进行优化来充分使用硬件资源,提升程序执行速度。
例如数据库中单表的数据由100w上涨到了10000w,数据库读写速度大幅度下降。
对于以上情况,可以使用jprofile等商业工具进行分析,从而找到执行耗时比率较大的代码。
找到系统性能瓶颈后,接下来就是调优了。调优可以从硬件、操作系统、JVM和程序四个方面着手。硬件和操作系统的调优可以参考计算机系统等相关书籍,接下来主要探讨的是JVM和程序方面的调优。
JVM调优主要是内存管理方面你的调优,包括各个代大小,GC策略等。由于GC会影响应用线程,严重影响性能,这些调优对于应用而言还是很重要的。根据应用的情况选择不同的内存管理策略有些时候能够降低GC导致的应用暂停时间,大幅度地提升应用性能,尤其是对于内存消耗较多的应用。
在不采用G1(JDK1.7提供的不区分年轻代和老年代的垃圾收集器)的情况下,通常Minor GC会远快于Full GC,因为Minor GC 采用的是标记复制算法,Full GC采用的是标记整理/标记清除算法,后者的复杂度比前者的复杂度高。
在代大小调优上,最关键的参数有以下几点:
串行GC性能较差,像 Serial 在收集和复制时都是一个线程,Parallel 在收集是多个线程,复制时是一个线程,一般都不会使用Serial GC。一般情况下,我们使用CMS收集器,使用方式如下:
-XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSMaxAbortablePrecleanTime=5 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplication-StoppedTime
当us 高说明执行线程无任何挂起动作,且一直执行,对于这种情况,可以在方法中增加Thread.sleep,释放CPU的执行权
当sy 高说明线程切换太过频繁,最简单的优化就是减少线程数。也可以减少线程间通信,降低锁竞争,或者采用协程代替线程。
限流,限制发送packet的频率。
容易导致CPU sy,即系统上下文切换频繁导致内核线程运行占比较高
解决方法:
多用并行处理
在jvm启动参数中加入以下参数
// 以下含义分别是 输出GC信息,输出GC简要信息,输出GC详细信息,输出GC的时间信息,输出GC造成的应用暂停时间,将GC日志输出到gc.log文件中
-verbose:gc -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -Xloggc:gc.log
-heap
打印heap的概要信息,GC使用算法,heap的配置及使用情况
jmap -heap pid
在Heap Configuration
参数中,MinHeapFreeRatio对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40),MaxHeapFreeRatio对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
-histo
打印堆的对象统计,包括对象数,内存大小等。
jmap -histo:live pid | grep more
附 - jmap输出中class name非自定义类的说明:
BaseType Character | Type | Interpretation |
---|---|---|
B | byte | signed byte |
C | char | Unicode character |
D | double | double-precision floating-point value |
F | float | single-precision floating-point value |
I | int | integer |
J | long | long integer |
L; | reference | an instance of class |
S | short | signed short |
Z | boolean | true or false |
[ | reference | one array dimension,[I表示int[] |
-gc
垃圾回收统计
jstat -gc pid
*-gcutil*
总结垃圾回收统计
jstat -gcutil pid
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有