ITPub博客

首页 > 数据库 > 数据库开发技术 > 史上最全的 Java 应用程序线上问题排查全手册:从知识体系到实战指南(二):实战指南

史上最全的 Java 应用程序线上问题排查全手册:从知识体系到实战指南(二):实战指南

原创 数据库开发技术 作者:灸哥漫谈 时间:2024-04-13 00:02:00 0 删除 编辑

上面给大家介绍了排查问题的知识体系,大家在学习这部分理论的时候,务必一定要形成体系,相辅相成,结合起来驱动自己问题排查能力的提升。接下来我们看看实战怎么玩?

二、问题排查实战指南

实战三步走

在收到生产环境报警之后,我们应该怎么做呢?一般都是三步走:

  • top、free、df 三连击
  • jstack、jstat、jmap 依次伺候
  • 具体问题具体分析。

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 文件

问题排查步骤

一般面对生产问题时,不要慌,按照下面的顺序心平气和地去排查,一定可以定位到问题并合理解决掉。先从应用层面开始,然后对系统层面进行排查。

应用排查

上面说过日志是问题排查最基本也是最重要的依据。当出现生产问题,首先要去看应用日志,绝大多数的问题通常都能在应用日志中直观反应的。如果没有应用日志,或者说无法反应问题,那我建议你和你的团队就要复盘反思你们的日志是否规范化。

系统排查

如果从应用日志上看不出问题,那就接着往系统层面去排查,系统层面一般都是从机器、进程和线程三个维度去逐个排查。

“

以下我仅列出具体使用的命令,对于命令操作后窗口显示内容以及各个字段的含义本文略去,有兴趣的同学可以自行学习对应内容。

机器层面排查

  • top 命令查看机器 CPU 占用情况
  • free -h 命令查看机器内存使用情况
  • df -h 命令查看机器磁盘使用情况
  • iostat 命令查看机器网络 IO 情况

进程层面排查

  • 获取进程 ID:ps -ef |grep 应用名称
  • 查看进程所占用的 CPU 和内容:ps -aux|grep 进程 ID

线程层面排查

  • 获取线程快照,要知道线程正在干什么:jstack 进程 ID
  • 查看 JVM 老年代、新生代对应的使用情况:jmap -heap 进程 ID
  • 查看哪个类使用内存最多:jmap -histo:live 进程 ID
  • 查看详细的 GC 情况:jstat -gcutil 进程 ID

常见问题场景的解决策略

一般在碰到生产问题的时候是有一个确定的原则的:第一要务是快速恢复止血,然后再进行问题的定位和解决,最后针对问题进行复盘总结制定防范措施。

CPU 异常场景

CPU 使用率是衡量系统繁忙程度的重要指标,但这个安全阈值不是绝对的,它取决于系统类型,是 IO 密集型还是计算密集型。

  • IO 密集型:CPU 使用率偏低,load 偏高
  • 计算密集型:CPU 使用率偏高,load 偏低

那究竟在什么场景下是需要排查 CPU 使用率呢?

  • 访问接口的响应速度很慢
  • 系统崩溃无响应
  • 压测时要查看 CPU、内存、load、RT、QPS 等指标

对于 CPU 异常的场景,往往是比较好定位的,原因一般包括:

  • 业务逻辑问题,比如死循环、线程阻塞、io wait 等
  • 序列化和反序列化
  • 正则表达式:Java 正则表达式使用的引擎实现是 NFA 自动机,它将在字符匹配期间执行回溯
  • 线程上下文切换过多:有许多已启动的线程,这些线程的状态在 Blocked 和 Running 之间发生变化。当锁争用激烈时,这种情况很容易发生
  • 经常 Young GC
  • 频繁 GC:如果访问量很高,可能会导致频繁的 GC 甚至 Full GC。当调用量很大时,内存分配将如此之快以致于 GC 线程将连续执行,这将导致 CPU 飙升

一般引起 CPU 异常最常见的原因是业务逻辑的问题,可以使用 jstack 来分析对应的堆栈情况

针对 CPU 异常的问题排查步骤如下:

  • 定位进程 top
  • 定位线程 top -Hp 进程号
  • 定位代码位置 jstack

排查方法

排查的方法可以选择根据你的兴趣和能力进行选择:

方法一:使用 jstack 进行 CPU 问题的分析

  • 用 top 命令查找哪个进程占用比较高
  • 用 ps 命令先找到对应进程的 pid
  • 用 printf '%x\n' pid 进行进程 id 转换
  • 用 jstack pid | grep 'nid' -C5 -color 找到对应的堆栈信息
  • 分析堆栈信息

在分析堆栈信息的时候,比较常见的方式是对整个 jstack 文件进行分析,关注 WAITING、TIMED_WAITING、BLOCKED

方法二:使用第三方工具,推荐阿里的 Arthas Thread

Arthas 是阿里开源的,几乎包揽了所有线上排查问题的工作,提供了完整的工具集,推荐指数十星。

频繁 GC 的应对策略

  • 第一,查看 GC 情况。可以使用 jstat -gc pid 1000 来对 gc 分代变化情况进行观察,不过首先要先确认你的生产环境是否开启了 GC 日志,很多应用生产环境没有开启 GC 日志的。
  • 第二,分析 GC 日志。可以使用 GC easy 帮助分析 GC 日志,得到可视化的分析结果
  • 第三,GC 原因定位。

频繁上下文切换的应对策略

使用 vmstat 命令查看,然后用 pidstat -w pid 对特定的 pid 进行监控。

线程池异常的应对策略

当你的业务系统突然收到报警或者你的用户侧反馈系统大量请求失败的提示。往往是需要考虑是否是线程池异常导致的问题。

在 Java 中,当新任务提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求。如果正在运行的线程数等于 corePoolSize 时,则新任务被添加到队列中,直到队列满。当队列满了后,会继续开辟新线程来处理任务,但不超过 maximumPoolSize。当任务队列满了并且已开辟了最大线程数,此时又来了新任务,ThreadPoolExecutor 会拒绝服务。

线程池异常,一般是可以通过查看应用日志找到原因的,平时经历过的原因一般有两类:

  • 下游依赖的服务的 RT 过长或者超时,这种情况下,研发人员一定要设置合理的下游依赖服务的超时时间以及熔断降级机制。同时对应的监控机制、日志打印一定要完善细致,这样才可以主动发现问题,而不是让用户来告诉你系统出问题了
  • 数据库慢 SQL 或者数据死锁

磁盘异常场景

一般收到磁盘相关的报警后,首先要确认是否磁盘容量不够,然后确认 TPS 是否正常。详细的步骤描述如下:

  • 使用 df -hl 来查看文件系统状态
  • 用 iostatiostat -d -k -x 来分析磁盘的性能情况
  • 用 iotop 定位文件读写来源,即需要知道是哪些进程正在读写
  • 用 readlink -f 把 tid 转换成 pid
  • 用 cat/proc/pid/io 查看这个进程具体的读写情况
  • 用 Isof -p pid 来确认具体的文件读写情况

内存异常场景

内存异常的问题排查相对比较麻烦一些,场景也比较多,比如 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 就会来找你。在日常开发中,代码产生内存泄露是常见的现象,而且一般都比较隐蔽,对程序员来说要避免以下常见泄露的问题出现在你的代码中:

  • 每次请求 new 新对象,导致大量重复创建
  • 文件流操作但未正确关闭
  • 手动触发 GC 不合理

GC 问题

GC 问题会直接影响 CPU 和内存,排查思路基本一致

  • 用 jstat 查分代变化情况
  • 通过 GC 日志进行问题排查

对于 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 出现,那往往是肯定出了问题的。一般原因包括:

  • 并发阶段失败:在并发标记阶段,Mix GC 之前老年代就被填满了,那么这时候 G1 就会放弃标记周期。这种情况下,可能就需要增加堆大小,或者调整并发标记线程数 -XX:ConcGCThreads
  • 晋升失败:在 GC 的时候没有足够的内存供存活/晋升对象使用,触发 Full GC,可以调整 -XX:G1ReservePercent 来增加预留内存百分比,减少 -XX:InitiatingHeapOccupancyPercent 来提前启动标记,-XX:ConcGCThreads 来增加标记线程数也可以
  • 大对象分配失败:大对象找不到合适的 region 空间进行分配,就会进行 Full GC,可以通过或者增大 -XX:G1HeapRegionSize 来解决
  • 程序主动执行 System.gc():不要随便写就对了

堆外内存

堆外内存溢出是一个非常不幸的场景,有的 Java 程序员可能一辈子都碰不到。

一般表现为:物理常驻内存增长快,报错的话根据使用方式不同都不确定,如果由于使用 Netty 导致的,那错误日志里可能会出现 OutOfDirectMemoryError 错误,如果直接是 DirectByteBuffer,那会报OutOfMemoryError: Direct buffer memory。

堆外内存溢出一般和 NIO 使用相关,排查步骤分享给大家:

  • 先通过 pmap 查看进程占用的内存情况,然后过一段时间后再跑一次命令看内存增长情况,或者和正常机器对比,发现可疑的内存段在哪。然后确定有可疑的内存段
  • 通过 gdb 进行分析
  • 获取 dump 文件后用 headxdump 查看

堆外内存我们重点关注 Internal 的内存增长,如果增长十分明显的话那就是有问题了。如果依旧很难定位到具体问题点,还是要从错误日志栈开始,找到可疑的对象,弄清楚回收机制,然后再去分析这个对象。

网络异常场景

涉及到网络层面的问题往往都比较复杂,场景多,定位难。

网络超时

超时错误大部分应用发生在应用层面,可以分为连接超时和读写超时,平时关心最多的是读写超时。对于研发人员来说如何设置合理的接口超时时间是不能太随意的,如果过长,就有可能过多占用 TCP 连接,如果过短,接口超时会频繁发生。

TCP 队列溢出

队列溢出是个相对底层的错误,它可能会造成超时等更表层的错误,错误也很隐蔽,一般先用 netstat -s | egrep "listen | LISTEN" 查看队列相关的溢出次数,然后用 ss -Int 具体队列信息进行定位。

“

网络异常的场景我所遇不多,这里就不再一一列举,有兴趣的同学欢迎补充和自我学习。

三、常用命令速查

top 命令

-b      批处理
-c       显示完整的命令
-I       忽略失效过程
-s       保密模式
-S       累积模式
-d<时间>      设置间隔时间
-u<用户名>    指定用户名
-p<进程号>    指定进程
-n<次数>      循环显示的次数

vmstat 命令

-a:    显示活跃和非活跃内存
-f:    显示从系统启动至今的fork数量 
-m:    显示slabinfo
-n:    只在开始时显示一次各字段名称
-s:    显示内存相关统计信息及多种系统活动数量
-d:    显示磁盘相关统计信息
-p:    显示指定磁盘分区统计信息
-S:    使用指定单位显示。参数有 k 、K 、m 、M ,分别代表1000、1024、1000000、1048576字节(byte)。默认单位为K(1024 bytes)
-V:    显示vmstat版本信息

free 命令

-b     显示内存的单位为字节
-k     显示内存的单位为 KB
-m     显示内存的单位为 M
-o     忽略缓冲区调节列
-t     总和信息
-s<时间>     每隔指定时间执行一次命令,单位为s
-h     以可读形式显示容量,需要free -V显示版本大于3.3
-V     版本信息 

df 命令

-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    输出版本信息并退出 

iostat 命令

-c      只显示CPU行
-d      显示设备(磁盘)使用状态
-k      以千字节为单位显示磁盘输出
-t      在输出中包括时间戳
-x      在输出中包括扩展的磁盘指标

ifstat 命令

-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   指定一个驱动来收集状态信息

ps 命令

-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栏位

jps 命令

-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

jstack 命令

-F      当进程挂起,执行jstack <pid> 命令没有任何输出后,将强制转储堆内的线程信息
-m      在混合模式下,打印 java 和 native c/c++ 框架的所有栈信息
-l      长列表。打印关于锁的附加信息,例如属于 java.util.concurrent 的 ownable synchronizers 列表
-h      打印帮助信息

jstat 命令

–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编译的方法

jmap 命令

-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. 

jinfo 命令

no option        打印命令行参数和系统属性
-flags        打印命令行参数
-sysprops        打印系统属性
-h          帮助

推荐阅读

  1. 史上最全的 Java 应用程序线上问题排查全手册:从知识体系到实战指南(一):知识体系
  2. 程序员基本功|如何识别和解决 Java 代码中的坏味道
  3. 代码之美:如何为团队制定完美的代码风格
  4. 后端程序员基本功|接口设计大揭秘:打造完美 API 的黄金法则以及最佳实践(上)
  5. 后端程序员基本功|接口设计大揭秘:打造完美 API 的黄金法则以及最佳实践(中)
  6. 后端程序员基本功|接口设计大揭秘:打造完美 API 的黄金法则以及最佳实践(下)


原文作者:灸哥漫谈;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2023-11-26

  • 博文量
    368
  • 访问量
    173360