1. Load
在Linux中体现的是整体系统负载,即CPU负载 + Disk负载 + 网络负载 + 其余外设负载,并不能完全等同于CPU使用率(这种情况只出现在Linux中,其余系统比如Unix,Load还是只代表CPU负载)。
2. CPU使用率
通常而言,泛指的整体CPU使用率为User Time 和 Systime占比之和
-
us:用户进程空间中未改变过优先级的进程占用CPU百分比
-
sy:内核空间占用CPU百分比
-
ni:用户进程空间内改变过优先级的进程占用CPU百分比
-
id:空闲时间百分比
-
wa:空闲&等待I/O的时间百分比
-
hi:硬中断时间百分比
-
si:软中断时间百分比
-
st:虚拟化时被其余VM窃取时间百分比
这8类中,除wa和id外,其余CPU都处于工作态。
3. Load高 & CPU高
这是最常遇到的一类情况,即load上涨是CPU负载上升导致。根据CPU具体资源分配表现,可分为以下几类:
3.1. 资源&瓶颈分析
Load Average和CPU使用率可被细分为不同的子域指标,指向不同的资源瓶颈。总体来说,指标与资源瓶颈的对应关系基本如下图所示。
3.2. CPU sys高
这种情况CPU主要开销在于系统内核,可进一步查看上下文切换情况。
-
如果非自愿上下文切换较多,说明CPU抢占较为激烈,大量进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。
-
如果自愿上下文切换较多,说明可能存在I/O、内存等系统资源瓶颈,大量进程无法获取所需资源,导致的上下文切换。
3.3. CPU si高
这种情况CPU大量消耗在软中断,可进一步查看软中断类型。一般而言,网络I/O或者线程调度引起软中断最为常见:
-
NET_TX & NET_RX。NET_TX是发送网络数据包的软中断,NET_RX是接收网络数据包的软中断,这两种类型的软中断较高时,系统存在网络I/O瓶颈可能性较大。
-
SCHED。SCHED为进程调度以及负载均衡引起的中断,这种中断出现较多时,系统存在较多进程切换,一般与非自愿上下文切换高同时出现,可能存在CPU瓶颈。
3.4. CPU us高
这种情况说明资源主要消耗在应用进程,可能引发的原因有以下几类:
-
死循环或代码中存在CPU密集计算。这种情况多核CPU us会同时上涨。
-
内存问题,导致大量FULLGC,阻塞线程。这种情况一般只有一核CPU us上涨。
-
资源等待造成线程池满,连带引发CPU上涨。这种情况下,线程池满等异常会同时出现。
3.5. Load高 & CPU低
这种情况出现的根本原因在于不可中断睡眠态(TASK_UNINTERRUPTIBLE)进程数较多,即CPU负载不高,但负载较高。可进一步定位是磁盘I/O还是网络I/O导致。
4. 排查策略
4.1. 资源瓶颈定位
这一阶段通过全局性能检测工具,初步定位资源消耗异常位置。
常用的工具有:
-
top、vmstat、sar(历史)、mpstat、uptime、htop
-
中断:/proc/softirqs、/proc/interrupts
-
I/O:iostat、dstat
top 命令详解:
VIRT:Virtual memory usage 虚拟内存
1、进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
2、假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量
RES:Resident memory usage 常驻内存
1、进程当前使用的内存大小,但不包括swap out
2、包含其他进程的共享
3、如果申请100m的内存,实际使用10m,它只增长10m,与VIRT相反
4、关于库占用内存的情况,它只统计加载的库文件所占内存大小
SHR:Shared memory 共享内存
1、除了自身进程的共享内存,也包括其他进程的共享内存
2、虽然进程只使用了几个共享库的函数,但它包含了整个共享库的大小
3、计算某个进程所占的物理内存大小公式:RES – SHR
4、swap out后,它将会降下来
PR: Priority 优先级
NI nice值。
负值表示高优先级,正值表示低优先级m
S : Process status 进程状态。
D=不可中断的睡眠状态
R=运行
S=睡眠
T=跟踪/停止
Z=僵尸进程
第一行:
top - 15:23:08 up 3days, 31min, 17 users, load average: 2.41, 2.14, 1.89
15:23:08 :系统当前时间
up 3days, 31min :系统开机到现在经过了3天31分钟
17 users:当前17用户在线
load average: 2.41, 2.14, 1.89:系统1分钟、5分钟、15分钟的CPU负载信息.
备注:load average后面三个数值的含义是最近1分钟、最近5分钟、最近15分钟系统的负载值。这个值的意义是,单位时间段内CPU活动进程数。如果你的机器为单核,那么只要这几个值均<1,代表系统就没有负载压力,如果你的机器为N核,那么必须是这几个值均<N才可认为系统没有负载压力。
第二行解释:
Tasks: 675 total, 1 running, 665 sleeping, 9 stopped, 0 zombie
675 total,:当前有675个任务
1 running:1个任务正在运行
665 sleeping,:665个进程处于睡眠状态
9 stopped:停止的进程数
0 zombie:僵死的进程数
第三行解释:
Cpu(s): 2.8%us, 0.4%sy, 0.0%ni, 96.7%id, 0.1%wa, 0.0%hi, 0.0%si, 0.0%st
2.8%us:用户态进程占用CPU时间百分比
0.4%sy,:内核占用CPU时间百分比
0.0%ni:用户进程空间内改变过优先级的进程占用cpu百分比
96.7%id:空闲CPU时间百分比
0.1%wa:等待I/O的CPU时间百分比
0.0%hi:CPU硬中断时间百分比
0.0%si:CPU软中断时间百分比
st 虚拟机偷取时间
第四行解释:
Mem: 52762425+ total, 2888668+ used, 29199456+ free, 23274099 buffers/cache
32959108k total:物理内存总数
32783520k used: 使用的物理内存
175588k free:空闲的物理内存
291084k buffers:用作缓存的内存
第五行解释:
Swap: 0 total, 0 used, 0 free, 23300156 avail
0 total:交换空间的总量
0 used: 使用的交换空间
0 free:空闲的交换空间
23300156 avail:剩余空间
vmstat 命令详解:
Procs(进程)
- r: 运行队列中进程数量,这个值也可以判断是否需要增加CPU。(长期大于1)一般r>3是比较高,r>5是高,r>10就不正常了,服务器就很危险。
- b: 等待IO的进程数量。(阻塞)
Memory(内存)
- swpd: 虚拟内存已使用大小,如swpd的值不为0,但SI,SO的值长期为0,这种情况不影响系统性能。如大于0,表示机器物理内存不足,如不是程序内存泄露的原因,应升级内存或把耗内存任务迁移到其他机器。
- free: 空闲物理内存大小。
- buff: 用作缓冲的内存大小,Linux/Unix系统用来存储,目录里有什么内容,权限等的缓存。
- cache: 用作缓存的内存大小,可使用命令清空cache:echo 3 > /proc/sys/vm/drop_caches。如果cache的值大时,说明cache处的文件数多,如频繁访问到的文件都能被cache处,那么磁盘的读IO bi会非常小。(Linux/Unix的聪明之处,把空闲物理内存的一部分拿来做文件和目录的缓存,为了提高程序执行性能,当程序使用内存时,buffer/cached会很快地被使用。)
Swap
- si: 每秒从交换区写到内存的大小,由磁盘调入内存(每秒从磁盘读入虚拟内存的大小)。如si>0,表示物理内存不够用或内存泄露,要查找耗内存进程解决掉。
- so: 每秒写入交换区的内存大小,由内存调入磁盘(每秒虚拟内存写入磁盘的大小)。如so>0,表示物理内存不够用或内存泄露,要查找耗内存进程解决掉。
注意:内存够用时,这2个值都是0,如这2个值长期大于0时,系统性能会受到影响,磁盘IO和CPU资源都会被消耗。有时候看到空闲内存(free)很少的或接近于0时,就认为内存不够用了,不能光看这一点,还要结合si和so,如果free很少,但是si和so也很少(大多时候是0),那么不用担心,系统性能这时不会受到影响的。
IO(现在的Linux版本块的大小为1kb)
- bi: 每秒读取的块数
- bo: 每秒写入的块数,如读取文件,bo就要大于0。
注意:随机磁盘读写的时候,这2个值越大(如超出1024k),能看到CPU在IO等待的值也会越大。
system(系统)
- in: 每秒中断数,包括时钟中断。
- cs: 每秒上下文切换数。调用系统函数,要进行上下文切换。线程的切换,需进程上下文切换,cs值要越小越好,太大则考虑调低线程或进程数,在apache和nginx这种web服务器中,一般做性能测试时进行几千并发甚至几万并发测试,选择web服务器的进程可由进程或线程的峰值一直下调,压测,直到cs到一个较小的值,这进程和线程数就是比较合适的值。系统调用也是,每次调用系统函数,代码就会进入内核空间,导致上下文切换,这很耗资源,需尽量避免频繁调用系统函数。上下文切换次数过多表示CPU大部分浪费在上下文切换,导致CPU干正经事的时间少了,CPU没有充分利用,是不可取的。
注意:上面2个值越大,会看到由内核消耗的CPU时间会越大。
CPU(以百分比表示)
- us: 用户进程执行时间百分比(user time)
us的值比较高时,说明用户进程消耗的CPU时间多,但是如果长期超50%的使用,那么就该考虑优化程序算法或者进行加速。
- sy: 内核系统进程执行时间百分比(system time)
sy的值高时,说明系统内核消耗的CPU资源多,这并不是良性表现,应该检查原因。
- wa: IO等待时间百分比
wa的值高时,说明IO等待比较严重,这可能由于磁盘大量作随机访问造成,也有可能磁盘出现瓶颈(块操作)。
- id: 空闲时间百分比
4.2. 热点进程定位
定位到资源瓶颈后,可进一步分析具体进程资源消耗情况,找到热点进程。
常用工具有:
-
上下文切换:pidstat -w
-
CPU:pidstat -u
-
I/O:iotop、pidstat -d
-
僵尸进程:ps
上下文切换主要查看
- Cswch/s:每秒主动任务上下文切换数量
- Nvcswch/s:每秒被动任务上下文切换数量
4.3. 线程&进程内部资源定位
找到具体进程后,可细化分析进程内部资源开销情况。
常用工具有:
-
上下文切换:pidstat -w -p [pid]
-
CPU:pidstat -u -p [pid]
- top -Hp [pid]
4.4. 热点事件&方法分析
获取到热点线程后,可用jstack 工具,将问题范围定位到具体方法&堆栈。
常用的工具有:
-
perf:Linux自带性能分析工具,基于事件采样原理,以性能事件为基础,支持针对处理器相关性能指标与操作系统相关性能指标的性能剖析。
-
jstack [pid] | grep 线程id(16进制)
-
结合ps -Lp或者pidstat -p一起使用,可初步定位热点线程。
-
tcpdump:抓包分析,常用于网络I/O瓶颈定位。
- traceroute:帮助定位局域网不同段ip间的问题。
5. 频繁的gc也会导致cpu高
先来看下有哪些场景会发生 OOM
5.1. 栈异常
-
StackOverflowError 异常这种情况主要是因为单个线程请求栈深度大于虚拟机所允许的最大深度(如常用的递归调用层级过深等),单个线程定义了大量的本地变量,导致方法帧中本地变量表长度过大等也会导致 StackOverflowError 异常,
一句话:在单线程下,当栈桢太大或虚拟机容量太小导致内存无法分配时,都会发生 StackOverflowError 异常。
-
虚拟机在扩展栈时无法申请到足够的内存空间,会抛出 OOM 异常
会抛出「java.lang.OutOfMemoryError: unable to create new native thread」的异常
每个线程都会被分配对应的虚拟机栈大小,所以总可创建的线程数肯定是固定的,像以上代码这样不断地创建线程当然会造成最终无法分配,不过这也给提供了一个新思路,如果是因为建立过多的线程导致的内存溢出,而又想多创建线程,可以通过减少最大堆(-Xms)和减少虚拟机栈大小(-Xss)来实现。
5.2. 堆溢出 (java.lang.OutOfMemoryError:Java heap space)
主要原因有两点
-
1.大对象的分配,最有可能的是大数组分配
,所以对于由于大对象分配导致的堆溢出这种 OOM,一般采用增大堆内存的方式来解决
-
2.内存泄漏
在 Java 中,开发者创建和销毁对象是不需要自己开辟空间的,JVM 会自动帮我们完成,在应用程序整个生命周期中,JVM 会定时检查哪些对象可用,哪些不再使用,如果对象不再使用的话理论上这块内存会被回收再利用(即GC),如果无法回收就会发生内存泄漏
对于这种内存泄漏导致的 OOM, 单纯地增大堆大小是无法解决根本问题的,只不过是延缓了 OOM 的发生,最根本的解决方式还是要通过 heap dump analyzer 等方式来找出内存泄漏的代码来修复解决
5.3. java.lang.OutOfMemoryError:GC overhead limit exceeded
Sun 官方对此的定义:超过98%的时间用来做 GC 并且回收了不到 2% 的堆内存时会抛出此异常
导致的后果就是由于经过几个 GC 后只回收了不到 2% 的内存,堆很快又会被填满,然后又频繁发生 GC,导致 CPU 负载很快就达到 100%
另外GC 会引起 「Stop The World 」的问题,阻塞工作线程,所以会导致严重的性能问题,产生这种 OOM 的原因与「java.lang.OutOfMemoryError:Java heap space」类似,主要是由于分配大内存数组或内存泄漏导致的, 解决方案如下:
-
检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
-
dump 内存(后文会讲述如何 dump 出内存),检查是否存在内存泄露,如果没有,可考虑通过 -Xmx 参数设置加大内存。
5.4. java.lang.OutOfMemoryError:Metaspace space
元数据区,存放了被虚拟机加载的类,常量,静态变量,JIT 编译后的代码等信息,所以如果错误地频繁地使用 String.intern() 方法或运行期间生成了大量的代理类都有可能导致永久代(元数据区)溢出,解决方案如下
-
是否永久代设置的过小,如果可以,适应调大一点
-
检查代码是否有大量的反射操作
-
dump 之后通过 mat 检查是否存在大量由于反射生成的代码类
5.5. java.lang.OutOfMemoryError:Requested array size exceeds VM limit
该错误由 JVM 中的 native code 抛出。JVM 在为数组分配内存之前,会执行基于所在平台的检查:分配的数据结构是否在此平台中是可寻址的,平台一般允许分配的数据大小在 1 到 21 亿之间,如果超过了这个数就会抛出这种异常
碰到这种异常一般只需要检查代码中是否有创建超大数组的地方即可。
5.6. java.lang.OutOfMemoryError: Out of swap space
Java 应用启动的时候分被分配一定的内存空间(通过 -Xmx 及其他参数来指定), 如果 JVM 要求的总内存空间大小大于可用的本机内存,则操作系统会将内存中的部分数据交换到硬盘上
如果此时 swap 分区大小不足或者其他进程耗尽了本机的内存,则会发生 OOM, 可以通过增大 swap 空间大小来解决,但如果在交换空间进行 GC 造成的 「Stop The World」增加大个数量级,所以增大 swap 空间一定要慎重,所以一般是通过增大本机内存或优化程序减少内存占用来解决。
5.7. Out of memory:Kill process or sacrifice child
在内核的调度任务中,有一个「Out of memory killer」的调度器,它会在系统可用内存不足时被激活,然后选择一个进程把它干掉,哪个进程会被干掉呢,简单地说会优先干掉占用内存大的应用型进程
解决这种 OOM 最直接简单的方法就是升级内存,或者调整 OOM Killer 的优先级,减少应用的不必要的内存使用等等
看了以上的各种 OOM 产生的情况,可以看出:GC 和是否发生 OOM 没有必然的因果关系!
GC 主要发生在堆上,而 从以上列出的几种发生 OOM 的场景可以看出,空间不足无法再创建线程,或者存在死循环一直在分配对象导致 GC 无法回收对象或者一次分配大内存数组(超过堆的大小)等都可能导致 OOM, 所以 OOM 与 GC 并没有必然的因果关系
6. OOM 问题排查的一些常用工具
jmc、 jvisualvm、jconsole、jmap、jstat
jprofile 如何使用 JProfiler 分析线上 Java 服务性能瓶颈
mat 分析 https://jifa.corp.kuaishou.com/
6.1. jvm参数
1、跟踪参数:跟踪、监控JVM状态,用于程序员JVM调优及故障排查
2、堆、栈分配参数:分配堆内存
3、垃圾回收器相关参数:使用哪种垃圾回收器以及相关参数
4、编译优化参数:jvm调优
- -verbose:class /-XX:+TraceClassLoading 打印加载的类
- -verbose:gc /XX:+PrintGC 打印简要gc
- -XX:+PrintGCDetails 打印gc详细信息
- -XX:+PrintGCDateStamps 打印gc时的时间日期
- -XX:+PrintGCTimeStamps 时间秒
- -Xloggc:log/gc.log gc日志打印到外部文件
- -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/liuxuchao/tmp/heapdump.hprof 发生OOM时dump 内存
- -Xms 堆初始化值
- -Xmx 堆最大值
- -XX:MaxDirectMemorySize 堆外直接内存
- -XX:MetaspaceSize 元数据区内存
- -Xxs:线程栈内存大小
-XX:MaxGCPauseMillis=200 回收预计暂停时间
-XX:+UseG1GC 使用G1垃圾回收器
-XX:G1HeapRegionSize 单个region的大小 2048个region
-XX:G1HeapWastePercent 触发Mixed GC的可回收空间百分比
-XX:PretenureSizeThreshold 内存达到多少时直接在年老代分配
-XX:MaxTenuringThreshold gc年龄达到多少时晋升为老年代 默认15
-XX:+UseCompressedOops 使用指针压缩
-XX:+AggressiveOpts 加速编译
-XX:+UseStringDeduplication 字符串去重 任何一个String对象在内存中最少占用24个字节,启用这个参数以后如果有很多短的重复字符串对象的话会大大的节省内存。
-XX:CompileThreshold JIT预编译阈值 方法调用次数达到多少次之后会将其编译成机器码