上面给大家介绍了排查问题的知识体系,大家在学习这部分理论的时候,务必一定要形成体系,相辅相成,结合起来驱动自己问题排查能力的提升。接下来我们看看实战怎么玩?
在收到生产环境报警之后,我们应该怎么做呢?一般都是三步走:
top、free、df 三连击是干什么的我这里就不多说了。
jstack 是线程跟踪工具,用于打印指定 Java 进程的线程堆栈信息。
jstack -l 5524 > /opt/jstack.txt
jstat 是 JVM Statistics Monitoring Tool,查看对内存各部分的使用量以及加载类的数量。
jstat -gc pid:垃圾回收统计
jstat -gccapacity pid:堆内存统计
jstat -gcnew pid:新生代垃圾回收统计
jstat -gcnewcapacity pid:新生代内存统计
jstat -gcold pid:老年代垃圾回收统计
jstat -gcoldcapacity pid:老年代内存统计
jstat -gcmetacapacity pid:元数据空间统计
jstat -gcutil pid:总结垃圾回收统计
jstat -gc pid 500 10 :每500毫秒打印一次 Java 堆状况,打印10次
jmap:Memory Map for Java,可以生成 Java 程序的 dump 文件,也可以查看对内对象的统计信息,包括 ClassLoader 以及 finalizer 队列。
jmap pid:查看进程的内存映像信息
jmap -heap pid:显示Java堆详细信息
jmap -histo:live pid:显示堆中对象的统计信息
jmap -clstats pid:打印类加载器信息
jmap -finalizerinfo pid:打印等待终结的对象信息
jmap -dump:format=b,file=heapdump.phrof pid:生成堆转储快照 dump 文件
一般面对生产问题时,不要慌,按照下面的顺序心平气和地去排查,一定可以定位到问题并合理解决掉。先从应用层面开始,然后对系统层面进行排查。
上面说过日志是问题排查最基本也是最重要的依据。当出现生产问题,首先要去看应用日志,绝大多数的问题通常都能在应用日志中直观反应的。如果没有应用日志,或者说无法反应问题,那我建议你和你的团队就要复盘反思你们的日志是否规范化。
如果从应用日志上看不出问题,那就接着往系统层面去排查,系统层面一般都是从机器、进程和线程三个维度去逐个排查。
“以下我仅列出具体使用的命令,对于命令操作后窗口显示内容以及各个字段的含义本文略去,有兴趣的同学可以自行学习对应内容。
一般在碰到生产问题的时候是有一个确定的原则的:第一要务是快速恢复止血,然后再进行问题的定位和解决,最后针对问题进行复盘总结制定防范措施。
CPU 使用率是衡量系统繁忙程度的重要指标,但这个安全阈值不是绝对的,它取决于系统类型,是 IO 密集型还是计算密集型。
那究竟在什么场景下是需要排查 CPU 使用率呢?
对于 CPU 异常的场景,往往是比较好定位的,原因一般包括:
一般引起 CPU 异常最常见的原因是业务逻辑的问题,可以使用 jstack 来分析对应的堆栈情况
针对 CPU 异常的问题排查步骤如下:
排查的方法可以选择根据你的兴趣和能力进行选择:
方法一:使用 jstack 进行 CPU 问题的分析
在分析堆栈信息的时候,比较常见的方式是对整个 jstack 文件进行分析,关注 WAITING、TIMED_WAITING、BLOCKED
方法二:使用第三方工具,推荐阿里的 Arthas Thread
Arthas 是阿里开源的,几乎包揽了所有线上排查问题的工作,提供了完整的工具集,推荐指数十星。
使用 vmstat 命令查看,然后用 pidstat -w pid 对特定的 pid 进行监控。
当你的业务系统突然收到报警或者你的用户侧反馈系统大量请求失败的提示。往往是需要考虑是否是线程池异常导致的问题。
在 Java 中,当新任务提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求。如果正在运行的线程数等于 corePoolSize 时,则新任务被添加到队列中,直到队列满。当队列满了后,会继续开辟新线程来处理任务,但不超过 maximumPoolSize。当任务队列满了并且已开辟了最大线程数,此时又来了新任务,ThreadPoolExecutor 会拒绝服务。
线程池异常,一般是可以通过查看应用日志找到原因的,平时经历过的原因一般有两类:
一般收到磁盘相关的报警后,首先要确认是否磁盘容量不够,然后确认 TPS 是否正常。详细的步骤描述如下:
内存异常的问题排查相对比较麻烦一些,场景也比较多,比如 OOM、GC、堆外内存等。碰到内存问题,不管三七二十一,先 free 一把。
内存问题大多数都是堆内内存,表现上主要是 OOM 和 StackOverflow
OOM 常见异常说明
Exception in thread "main" java.lang.OutOfMemoryError:Unable to create new native thread
没有足够的内存空间给线程分配栈,这类型的异常一般都是线程池代码问题,比如缺少 shutdown。这类问题要从代码层面查找问题,用 jstack 或者 jmap。如果一切正常,则可以使用指定 Xss 来减少单个 thread stack 的大小。
Exception in thread "main" java.lang.OutOfMemoryError:Java heap space
堆内存的占用超过 -Xmx 最大值,这也是最常见的 OOM 错误,这类问题的解决思路还是要先从代码层面去查找问题,确认是否存在内存泄漏,当然也是通过 jstack 和 jmap 来定位的。如果一切是正常的,那就调整 Xmx 的值来扩大内存。
Caused by:java.lang.OutOfMemoryError:Meta space
这是元数据区的内存占用达到 XX:MaxMetaspaceSize 最大值,排查的思路基本和排查思路和 heap space 一致,如果一切正常调整的是 XX 的参数。
Stack Overflow 常见异常说明
Exception in thread “main” java.lang.StackOverflowError
该问题是因为线程栈需要的内存大于 Xss 的值,碰到该问题,首先是排查具体原因,如果一切正常,调整 Xss 参数,但是不要太大,否则 OOM 就会来找你。在日常开发中,代码产生内存泄露是常见的现象,而且一般都比较隐蔽,对程序员来说要避免以下常见泄露的问题出现在你的代码中:
GC 问题会直接影响 CPU 和内存,排查思路基本一致
对于 GC 问题,往往通过 GC 日志,是基本能定位到 Young GC 和 Full GC 是否频繁或者耗时较长,然后针对性解决问题。
Young GC 频繁
Young GC 频繁一般是因为短周期小对象较多,先考虑是不是 Eden 区/新生代设置的太小了,确认能否通过调整 -Xmn、-XX:SurvivorRatio 等参数设置来解决问题。如果参数正常,但是 Young GC 频率还是太高,就需要使用 Jmap 和 MAT 对 dump 文件进行进一步排查定位。
Young GC 耗时过长
耗时过长问题就要看 GC 日志里耗时耗在什么地方。比如 Root Scanning 耗时长,就要确认线程数量和跨代引用的问题,如果 Object Copy 过长,就要去确认对象生存周期是否出现问题。
Full GC
一旦 Full GC 出现,那往往是肯定出了问题的。一般原因包括:
堆外内存溢出是一个非常不幸的场景,有的 Java 程序员可能一辈子都碰不到。
一般表现为:物理常驻内存增长快,报错的话根据使用方式不同都不确定,如果由于使用 Netty 导致的,那错误日志里可能会出现 OutOfDirectMemoryError 错误,如果直接是 DirectByteBuffer,那会报OutOfMemoryError: Direct buffer memory。
堆外内存溢出一般和 NIO 使用相关,排查步骤分享给大家:
堆外内存我们重点关注 Internal 的内存增长,如果增长十分明显的话那就是有问题了。如果依旧很难定位到具体问题点,还是要从错误日志栈开始,找到可疑的对象,弄清楚回收机制,然后再去分析这个对象。
涉及到网络层面的问题往往都比较复杂,场景多,定位难。
超时错误大部分应用发生在应用层面,可以分为连接超时和读写超时,平时关心最多的是读写超时。对于研发人员来说如何设置合理的接口超时时间是不能太随意的,如果过长,就有可能过多占用 TCP 连接,如果过短,接口超时会频繁发生。
队列溢出是个相对底层的错误,它可能会造成超时等更表层的错误,错误也很隐蔽,一般先用 netstat -s | egrep "listen | LISTEN" 查看队列相关的溢出次数,然后用 ss -Int 具体队列信息进行定位。
“网络异常的场景我所遇不多,这里就不再一一列举,有兴趣的同学欢迎补充和自我学习。
-b 批处理
-c 显示完整的命令
-I 忽略失效过程
-s 保密模式
-S 累积模式
-d<时间> 设置间隔时间
-u<用户名> 指定用户名
-p<进程号> 指定进程
-n<次数> 循环显示的次数
-a: 显示活跃和非活跃内存
-f: 显示从系统启动至今的fork数量
-m: 显示slabinfo
-n: 只在开始时显示一次各字段名称
-s: 显示内存相关统计信息及多种系统活动数量
-d: 显示磁盘相关统计信息
-p: 显示指定磁盘分区统计信息
-S: 使用指定单位显示。参数有 k 、K 、m 、M ,分别代表1000、1024、1000000、1048576字节(byte)。默认单位为K(1024 bytes)
-V: 显示vmstat版本信息
-b 显示内存的单位为字节
-k 显示内存的单位为 KB
-m 显示内存的单位为 M
-o 忽略缓冲区调节列
-t 总和信息
-s<时间> 每隔指定时间执行一次命令,单位为s
-h 以可读形式显示容量,需要free -V显示版本大于3.3
-V 版本信息
-a 列出包括BLOCK为0的文件系统
-h 用常见的格式显示出大小(例如:1K 234M 2G)
-H 同上,但是这里的1k等于1000字节而不是1024字节
-i 用信息索引点代替块表示使用状况
-k 指定块大小等于1024字节来显示使用状况
-l 只显示本地文件系统使用状况
-m 以指定块大小等于1048576字节(1M)来显示使用状况
--no-sync 在取得使用信息前禁止调用同步 (default)
-P 使用POSIX格式输出
--sync 在取得使用信息前调用同步
-t 只显示指定类型(TYPE)的文件系统
-T 输出每个文件系统的类型
-x 只显示指定类型(TYPE)之外的文件系统.
--help 输出该命令的帮助信息并退出
--version 输出版本信息并退出
-c 只显示CPU行
-d 显示设备(磁盘)使用状态
-k 以千字节为单位显示磁盘输出
-t 在输出中包括时间戳
-x 在输出中包括扩展的磁盘指标
-l 监测环路网络接口(lo)。
-a 监测能检测到的所有网络接口的状态信息。
-z 隐藏流量是无的接口,例如那些接口虽然启动了但是未用的
-i 指定要监测的接口,后面跟网络接口名
-s 等于加-d snmp:[comm@][#]host[/nn]] 参数,通过SNMP查询一个远程主机
-h 显示简短的帮助信息
-n 关闭显示周期性出现的头部信息
-t 在每一行的开头加一个时间戳
-T 报告所有监测接口的全部带宽
-w 用指定的列宽,而不是为了适应接口名称的长度而去自动放大列宽
-W 如果内容比终端窗口的宽度还要宽就自动换行
-S 在同一行保持状态更新(不滚动不换行)
-b 用kbits/s显示带宽而不是kbytes/s
-q 安静模式,警告信息不出现
-v 显示版本信息
-d 指定一个驱动来收集状态信息
-a 显示所有终端机下执行的进程,除了阶段作业领导者之外
-A 显示所有进程。
-c 显示CLS和PRI栏位。
-C<指令名称> 指定执行指令的名称,并列出该指令的进程的状况。
-d 显示所有进程,但不包括阶段作业领导者的进程。
-e 此参数的效果和指定"A"参数相同。
-f 显示UID,PPID,C与STIME栏位。
-g<群组名称> 此参数的效果和指定"-G"参数相同,当亦能使用阶段作业领导者的名称来指定
-G<群组识别码> 列出属于该群组的进程的状况,也可使用群组名称来指定。
h 不显示标题列。
-H 显示树状结构,表示进程间的相互关系。
-j 采用工作控制的格式显示进程状况。
-l 采用详细的格式来显示进程状况。
L 列出栏位的相关信息。
-m 显示所有的执行绪。
n 以数字来表示USER和WCHAN栏位。
-N 显示所有的进程,除了执行ps指令终端机下的进程之外。
-p<进程识别码> 指定进程识别码,并列出该进程的状况。
p<进程识别码> 此参数的效果和指定"-p"参数相同,只在列表格式方面稍有差异。
r 只列出现行终端机正在执行中的进程。
-s<阶段作业> 指定阶段作业的进程识别码,并列出隶属该阶段作业的进程的状况。
s 采用进程信号的格式显示进程状况。
S 列出进程时,包括已中断的子进程资料。
-t<终端机编号> 指定终端机编号,并列出属于该终端机的进程的状况。
-T 显示现行终端机下的所有进程。
-u<用户识别码> 此参数的效果和指定"-U"参数相同。
-U<用户识别码> 列出属于该用户的进程的状况,也可使用用户名称来指定。
v 采用虚拟内存的格式显示进程状况。
-V 显示版本信息。
-w 采用宽阔的格式来显示进程状况。
-x 显示所有进程,不以终端机来区分。
-y 配合参数"-l"使用时,不显示F(flag)栏位,并以RSS栏位取代ADDR栏位
-q 仅输出VM标识符,不包括class name,jar name,arguments in main method
-m 输出main method的参数
-l 输出完全的包名,应用主类名,jar的完全路径名
-v 输出jvm参数
-V 输出通过flag文件传递到JVM中的参数(.hotspotrc文件或-XX:Flags=所指定的文件
-Joption 传递参数到vm,例如:-J-Xms48m
-F 当进程挂起,执行jstack <pid> 命令没有任何输出后,将强制转储堆内的线程信息
-m 在混合模式下,打印 java 和 native c/c++ 框架的所有栈信息
-l 长列表。打印关于锁的附加信息,例如属于 java.util.concurrent 的 ownable synchronizers 列表
-h 打印帮助信息
–class 监视类装载、卸载数量、总空间及类装载所耗费的时间
–gc 监视Java堆状况,包括Eden区、2个Survivor区、老年代、永久代等的容量
–gccapacity 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大和最小空间
–gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
–gccause 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
–gcnew 监视新生代GC的状况
–gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大和最小空间
–gcold 监视老年代GC的状况
–gcoldcapacity 监视内容与-gcold基本相同,输出主要关注使用到的最大和最小空间
–gcpermcapacity 输出永久代使用到的最大和最小空间
–compiler 输出JIT编译器编译过的方法、耗时等信息
–printcompilation 输出已经被JIT编译的方法
-dump 输出jvm的heap信息
-finalizerinfo 打印正等候回收的对象的信息.
-heap 打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况.
-histo[:live] 打印每个class的实例数目,内存占用,类全名信息
-permstat 打印classload和jvm heap长久层的信息.
-F 强迫.在pid没有相应的时候使用-dump或者-histo参数. 在这个模式下,live子参数无效.
-h 打印辅助信息
-J 传递参数给jmap启动的jvm.
no option 打印命令行参数和系统属性
-flags 打印命令行参数
-sysprops 打印系统属性
-h 帮助
原文作者:灸哥漫谈;如有侵权,请联系管理员删除。