生产环境故障排查

0. 生产环境 vs. 开发环境

通常,本地开发环境无法访问生产环境。如果在生产环境中遇到问题,则无法使用 IDE 远程调试。更糟糕的是,在生产环境中调试是不可接受的,因为它会暂停所有线程,导致服务暂停。
开发人员可以尝试在测试环境或者预发环境中复现生产环境中的问题。但是,某些问题无法在不同的环境中轻松复现,甚至在重新启动后就消失了。总结来说,现场问题排查比本地问题排查困难一些,具体为:

  1. 生产环境中,无法通过IDE远程调试,因为会暂停服务。
  2. 生产环境中遇到的问题,无法稳定复现,如服务器宕机,内存溢出,死锁,线程忙等问题。
  3. 问题排查工具
    遇到线上问题时,一般定位问题都是基于程序的运行数据来进行,排查的内容包括运行日志,异常堆栈信息,GC情况,线程快照,堆快照等,所以需要借助一些工具,我将排查的工具分为JDK工具和第三方辅助工具,以应对不同场景的需求。
    常见的指令:ps, top, jstack, jmap, jstat, grep, awk tail

1.1 JDK提供的工具

JDK提供了很多指令,常见的指令如表1所示,后文介绍这些工具的使用指令。在生产环境中,一些指令会影响生产环境的性能,因此不建议使用,如jmap, jhat, jinfo.

表1. JDK提供的工具列表
工具说明
jps进程监控
jinfo配置信息查看
jstat信息统计监控
jmap堆内存统计
jhat堆内存快照分析
jstack堆栈跟踪

下面逐一介绍。

1.1.1 jsp-进程监控工具

查看机器上运行的java进程,相当于ps -aux | grep java
jps [options] [jvm 进程id]

指令参数说明

jps -q:查看机器所有运行的Java进程,但只显示进程号(lvmid)。
jps -m:只显示传递给main方法的参数。
jps -l:只显示运行程序主类的包名,或者运行程序jar包的完整路径。
jps -v:单独显示JVM启动时,显式指定的参数。
jps -V:显示主类名或者jar包名。

示列

jsp -V

1.1.2 jinfo-配置信息查看工具

实时查看JVM的运行参数,可以动态地修改参数。
jinfo [options1] [options2]

指令参数说明

  1. :如果第一个参数不写,默认输出JVM的全部参数和系统属性。
  2. -flag :输出与指定名称对应的所有参数,以及参数值。
  3. -flag [+|-]:开启或者关闭与指定名称对应的参数。
  4. -flag =:修改指定名称对应参数的值。
  5. -flags:输出JVM全部的参数。
  6. -sysprops:输出JVM全部的系统属性。

其中[option2]可选项如下:

  1. :对应的JVM进程ID(必需参数),指定一个jinfo要操作的Java进程。
  2. executable :输出打印堆栈跟踪的核心文件。
  3. [进程ID]@:远程操作的地址。

示列

jinfo -flags 9895

1.1.3 jstat-信息统计工具

jstat可以对Java程序的资源以及性能进行实时的命令行的监控,监控范围包含:堆空间的各数据区、垃圾回收状况以及类的加载与卸载状态。
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

参数说明

-t:在输出结果中加上Timestamp列,显示系统运行的时间。
-h:可以在周期性数据输出的时候,指定间隔多少行数据后输出一次表头。
vmid:Virtual Machine ID虚拟ID,也就是指定一个要监控的Java进程ID。
interval:每次执行的间隔时间,默认单位为ms。
count:用于指定输出多少条数据,默认情况下会一直输出。

-class:输出类加载ClassLoad相关的信息。
-compiler:显示与JIT即时编译相关的信息。
-gc:显示与GC相关的信息。
-gccapacity:显示每个分代空间的容量以及使用情况。
-gcmetacapacity:输出元数据空间相关的信息。
-gcnew:显示新生代空间相关的信息。
-gcnewcapacity:显示新生代空间的容量大小以及使用情况。
-gcold:输出年老代空间的信息。
-gcoldcapacity:输出年老代空间的容量大小以及使用情况。
-gcutil:显示垃圾回收信息。
-gccause:和-gcutil功能相同,但是会额外输出最后一次或本次GC的原因。
-printcompilation:输出JIT即时编译的方法信息。

示列

jstat -gc -t -h30 9895 1s 300

-gc:监控GC的状态
-t:显示系统运行的时间
-h30:间隔30行数据,输出一次表头
9895:Java进程ID
1s:时间间隔
300:本次输出的数据行数

‒ Timestamp:系统运行的时间
‒ S0C:第一个Survivor区的总容量大小
‒ S1C:第二个Survivor区的总容量大小
‒ S0U:第二个Survivor区的已使用大小
‒ S1U:第二个Survivor区的已使用大小
‒ EC:Eden区的总容量大小
‒ EU:Eden区的已使用大小
‒ OC:Old区的总容量大小
‒ OU:Old区的已使用大小
‒ MC:Metaspace区的总容量大小
‒ MU:Metaspace区的已使用大小
‒ CCS:CCompressedClassSpace空间的总大小
‒ CCSU:CompressedClassSpace空间的已用大小
‒ YGC:从程序启动到采样时,期间发生的新生代GC次数
‒ YGCT:从程序启动到采样时,期间新生代GC总耗时FGC从程序启动到采样时,期间发生的整堆GC(FullGC)次数
‒ FGCT:从程序启动到采样时,期间整堆GC(FullGC)总耗时GCT从程序启动到采样时,程序发生GC的总耗时

1.1.4 jmap-堆内存分析工具

jamp用于查看堆空间的使用情况,通常会配合jhat工具一起使用,它可以用于生成Java堆的Dump文件、finalize队列、元数据空间的详细信息,Java堆中对象统计信息,如每个分区的使用率、当前装配的GC收集器等。
jmap [options1] [options2]

[options1]

  1. [no option]:查看进程的内存映像信息,与Solaris pmap类似。
  2. -heap:显示Java堆空间的详细信息。
  3. -histo[:live]:显示Java堆中对象的统计信息。
  4. -clstats:显示类加载相关的信息。
  5. -finalizerinfo:显示F-Queue队列中等待Finalizer线程执行finalizer方法的对象。
  6. -dump::生成堆转储快照。
  7. -F:当正常情况下-dump和-histo执行失效时,前面加-F可以强制执行。
  8. -help:显示帮助信息。
  9. -J:指定传递给运行jmap的JVM参数。

其中[option2]可选项如下:

  1. :对应的JVM进程ID(必需参数),指定一个jinfo要操作的Java进程。
  2. executable :输出打印堆栈跟踪的核心文件。
  3. [进程ID]@:远程操作的地址。

示列

jmap -dump:live,format=b,file=HeapDump.dat 60620
live:导出堆中存活对象快照;
format:指定输出格式;
file:指定输出的文件名称和格式*.phrof

1.1.5 jhat-堆内存快照分析工具

jhat工具一般配合jmap工具使用,主要用于分析jmap工具导出的Dump文件,其中也内嵌了一个微型的HTTP/HTML服务器,所以当jhat工具分析完Dump文件后,可以支持在浏览器中查看分析结果, 但是浏览器界面的不是很友好。 但是在线上环境中一般不会直接使用jhat工具对Dump文件进行解析,因为jhat解析Dump文件,尤其是大体积的Dump时,是一个非常耗时且占用硬件资源的过程。所以为了防止占用服务器过多的资源,通常都会将Dump文件拿到其他机器中分析,一般的分析工具会选择MAT,IBM Heap Analyzer, VisualVM, Jprofile等。
jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>

使用jhap分析Dump文件的流程
这里我运行了Math-Game.jar文件,通过jsp查看一下jar包运行的JVM-ID号,然后使用jmap指令Dump文件,最后通过jhat做分析。通过访问localhost:9999查看分析结果。

从上至下分别为:
● 按照包路径查看不同类的具体对象实例。
● 查看堆中的所有Roots节点的集合。
● 查看所有类的对象实例数量(包括了JVM自身的类)。
● 查看所有类的对象实例数量(除去了JVM自身的类)。
● 查看Java堆中实例对象的统计直方图(和jmap的对象统计信息差不多)。
● 查看JVM的finalizer相关信息。
● 通过jhat工具提供的OQL(Object Query Language)获取指定对象的实例信息。关于QOL查询语言的文档可以通过访问本地的oqlhelp获取到。

1.1.6 jstack-堆栈跟踪工具

jstack工具主要用于捕捉JVM当前时刻的线程快照,线程快照是JVM中每条线程正在执行的方法堆栈集合。在线上情况时,生成线程快照文件可以用于定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源无响应等原因导致的线程停顿。
当线程出现停顿时,可以通过jstack工具生成线程快照,从快照信息中能查看到Java程序内部每条线程的调用堆栈情况,从调用堆栈信息中就可以看出:发生停顿的线程目前在干什么,在等待什么资源等。

  1. jstack [-l] : 连接一个正在运行的线程
  2. jstack -F [-m] [-l] : 连接一个挂起的线程
  3. jstack [-m] [-l] : 连接内核文件(core file)
  4. jstack [-m] [-l] [server_id@]: 连接到远程主机的虚拟机

参数说明

-F 强制dump线程信息,通常在jstack <pid>没有响应时使用,比如线程挂起了。
-m to print both java and native frames (mixed mode):如果线程调用到本地方法栈中的本地方法,也显示C/C++的堆栈信息。
-l long listing. Prints additional information about locks:除显示堆栈信息外,输出关于锁相关的附加信息(用于排查死锁问题)。
-h or -help to print this help message
jstack工具dump的线程日志需要查看的信息
‒ Deadlock: 线程出现死锁
‒ Waitting on condition: 线程等待资源
‒ Waitting on monitor entry: 线程等待获取监视器锁
‒ Suspended: 线程暂停
‒ TIME_WAITING:线程挂起
‒ Blocked:线程阻塞
‒ Parked:线程停止

1.2 第三方工具

1.2.1 Arthas

Arthas 旨在解决这些问题。开发人员可以在线解决生产问题。无需 JVM 重启,无需代码更改。 Arthas 作为观察者不会暂停正在运行的线程。
使用 Arthas-boot.jar 包启动,通过运行 jar 包启动,输入stop 停止运行。启动后可以通过地址 http://localhost:8563/ 访问。
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
通过dashboard指令查看运行的实时数据。这里运行了一个MathDemo.java的程序,代码详情

1.2.2 Jprofilter

分析dump文件

1.2.3 Perfino

1.2.4 YourKit

1.2.5 Perf4j

1.2.6 MAT

1.2.7 Jconsole

java图形化界面查看工具,只适合本地排查问题,不适合线上生产环境中使用。

2. 生产问题排查

生产过程存在的问题,通常表现为系统运行一段时间后突然宕机或者服务响应突然变得奇慢无比,JVM在线上环境通常会出现以下几个问题:

  1. JVM内存溢出(OOM)。
  2. JVM内存泄漏。
  3. 业务线程死锁。
  4. 应用程序异常宕机。
  5. 线程阻塞/响应速度变慢。
  6. CPU利用率飙升或100%。

2.1 内存溢出

内存溢出 OOM 是线上排查中比较常遇到的,它不仅仅在Java内存空间中,也会在堆栈空间中发生OOM的问题。
通常情况下,线上生产环境 OOM 的原因大概有两类:

  1. JVM内存分配太小,不能满足程序的正常运行。
  2. 代码内部存在问题,导致GC回收速度跟不上分配速度,频繁GC。
  3. 内存泄漏。
    如果是第一个问题,则调高JVM参数就可以解决,如果是由于代码编写不严谨,导致Java内存中产生了大量的垃圾对象,导致新对象没有空闲内存分配,产生溢出。在排查OOM问题时,核心思路是:哪里OOM了?为什么OOM了?怎么避免出现的OOM?同时,在排查过程中,应当建立在数据的分析之上,也就是指Dump数据。获取堆Dump文件方式有两种:1. 启动时设置 -XX:HeapDumpPath,事先指定OOM出现时,自动导出Dump文件。
  4. 重启并在程序运行一段时间后,通过工具导出,如jmap或第三方工具。
    OOM模拟案例
public class OOMCase
{
    public static class OOMObject{}
    public static void main(String[] args)
    {
        List<OOMObject> list = new ArrayList<>();
        while (true)
        {
            // oom 
            list.add(new OOMObject());
        }
    }
}
// -Xms64M
// -Xmx64M
// -XX:+HeapDumpOnOutOfMemoryError
// -XX:HeapDumpPath=/Users/leesure/Documents/aloudata/leetcode_java/Heap_OOM.hprof

通过IDEA的插件Profiler可以分析看到造成OOM的对象是OOMObeject

OOM问题排查思路

  1. 首先获取Dump文件,最好是上线部署时配置了,这样可以保留第一现场,但如若未配置对应参数,可以调小堆空间,然后重启程序的时候重新配置参数,争取做到“现场”重现。
  2. 如果无法通过配置参数获得程序OOM自然导出的Dump文件,那则可以等待程序在线上运行一段时间,并协调测试人员对各接口进行压测,而后主动式的通过jmap等工具导出堆的Dump文件(这种方式没有程序自动导出的Dump文件效果好)。
  3. 将Dump文件传输到本地,然后通过相关的Dump分析工具分析,如JDK自带的jvisualvm,或第三方的MAT工具等。
  4. 根据分析结果尝试定位问题,先定位问题发生的区域,如:确定是堆外内存还是堆内空间溢出,如果是堆内,是哪个数据区发生了溢出。确定了溢出的区域之后,再分析导致溢出的原因。
  5. 根据定位到的区域以及原因,做出对应的解决措施,如:优化代码、优化SQL等。

2.2 内存泄漏

内存泄漏和内存溢出两个概念,概念相似,但本质上是两个完全不同的问题。内存溢出是指内存不够用了,内存泄漏往往是指内存中有了程序运行时候不需要的数据。比如一个2L水桶,内存溢出的概念就像是水桶装的水太多了,水桶装不下了,但是内存泄漏是水桶里有1L的泥沙(有垃圾),虽然是2L的水桶容量,但是现在只能装1L的水。不过在发生OOM出时,有可能是因为内存泄漏诱发的,但内存泄漏绝对不可能因为OOM引发。
线上的Java程序中,出现内存泄漏主要分为两种情况:

  1. 堆内泄漏:对象没有及时清理。由于代码不合理导致内存出现泄漏,如垃圾对象与静态对象保持着引用、未正确的关闭外部连接等。
  2. 堆外泄漏:如申请Buffer流后未释放内存、直接内存中的数据未手动清理等。
    生产环境中并不能一眼就看出内存泄露的问题,内存泄露往往首先出现的问题就是内存溢出。在排查内存溢出的过程中,需要排查内存泄漏的问题。
    内存泄漏模拟案例
public class MemoryLeak
{
    // 长期的生命周期对象,静态类型的root节点
    static List<Object> ROOT = new ArrayList<>();
    public static void main(String[] args)
    {
        int i = 0;
        for(;;)
        {
            // 不断创建新对象,使用后不手动将其从容器中移除
            Object obj = new Object();
            ROOT.add(obj);
            obj = i;
            i ++;
        }
    }
}
-Xms64M
-Xmx64M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/leesure/Documents/aloudata/leetcode_java/Heap_MemoryLeak.hprof

理论上来说,Java 创建的对象在使用完成后,当内存不足时,GC 线程会将其回收,但是上述案例是将创建出来的对象与静态成员 ROOT 建立了引用关系。静态成员在JVM中又被作为GCroots节点来对待,所以创建的 Object 对象无法被 GC 线程及时回收,导致内存泄漏。若没有手动去断开静态成员的引用,这个“废弃对象”所占的空间一直不会被回收。
因此在代码开发中,应该尽量避免自己创建的对象和静态对象建立连接。
内存泄漏排查思路
线上遇到因内存泄露而造成的OOM问题时,应当首先确认是堆内存泄漏,还是堆外内存泄漏,毕竟堆空间和元空间都有可能存在内存泄漏的隐患,搞清楚内存溢出的位置后再进行排查。常见的内存泄漏的例子:

  1. 外部临时连接对象使用后没有合理关闭,如DB、Socket、文件IO流等。
  2. 程序内部创建的对象与长生命周期的对象建立引用,对象使用完成后没有及时断开引用。
  3. 申请堆外的内存,在使用完成后没有手动释放清理内存,导致内存泄漏。如使用Buffer缓冲区后没有及时清理关闭。

2.3 死锁

死锁就是两个或两个以上线程,因为资源竞争而照成相互等待对方释放资源无法继续运行的情况。出现死锁的情况必须通过外力干涉解除线程相互等待的状态。
死锁模拟案例

public class ForkAndSpoonExample
{
    public static void main(String[] args)
    {
        Tool fork = new Tool("Fork");
        Tool spoon = new Tool("Spoon");
        // 造成死锁
        new EatThread("Alice", spoon, fork).start();
        new EatThread("Bob", fork, spoon).start();
    }
}
Alice takes up Tool{name='Spoon'} (left) 
Bob takes up Tool{name='Fork'} (left) 

上面的案例中,Alice线程和Bob线程因为等待对方释放Tool工具锁而等待,进入死锁状态。

死锁排查思路

常见的两种种方式来排查死锁问题:

  1. JVM提供的工具:通过 jps + jstack工具排查。
  2. 第三方工具:通过 jconsole 或 jvisualvm 工具排查,jconsole可视化工具,不太适合线上的Linux系统
    方式一:jps + jstack
[~]$ jps         
62226 Jps
60260 Launcher
58262 
43143 
62172 Launcher
62173 ForkAndSpoonExample
[~]$ jstack -l 6217

显示的堆栈跟踪信息如下:

方式二: jconsole

jconsole启动后是一个可视化界面,因此在生产环境中,这种方式不适合线上死锁问题排查。

2.4 线上宕机

程序宕机的原因可能是由于多方面引起的,如:机房环境因素、服务器本身硬件问题、系统内其他上下游节点引发的雪崩、Java应用自身导致(频繁GC、OOM、流量等)、服务器中被植入木马或矿机脚本等情况,都有可能导致程序出现异常宕机问题。
处理这类问题,由于原因的不确定性,往往需要开发,运维和网络安全的人协同解决,而且要保证程序异常宕机时,能立即重启并且及时通知开发人员及时排错,Keepalived工具可以定期执行脚本、出现故障时给指定邮箱发送信件通知开发人员等。

2.5 CPU利用率高

CPU利用率高模拟案例

public class CpuOverload
{
    public static void main(String[] args)
    {
        // 启动十条不活跃的线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(60 * 10 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "InActivate_Thread_" + i).start();
        }
        // 启动一个不断循环的线程
        new Thread(() -> {
            int i = 0;
            while (true) {
                i++;
            }
        }, "Activate_Thread").start();
    }
}
  1. 启动java程序后,通过top指令查看系统后台进程运行状态,可以看到java进程的CPU利用率到达率92.0%。

  2. 通过使用top -Hp [pid]进一步排查Java程序中,CPU占用率最高的线程。

在这一步就能看到当前的Java线程中Activate_Thread线程占用CPU过高。通过jstack查看堆栈跟踪信息。

CPU利用率高排查思路

  1. top指令查看系统后台进程的资源占用情况,确定是否是Java应用造成的。

  2. 使用top -Hp [pid]进一步排查Java程序中,CPU占用率最高的线程,printf "%X <pid。

  3. 通过jstack工具导出Java应用的堆栈线程快照信息jstack | grep <sub_id>,连续打印。

  4. 通过<sub_id>,在线程栈信息中搜索,定位导致CPU飙升的具体代码。

  5. 确认引发CPU飙升的线程是虚拟机自带的VM线程,还是业务线程。如果是业务线程就是代码问题,根据栈信息修改为正确的代码后,将程序重新部署上线。如果是JVM线程,那可能是由于频繁GC、频繁编译等JVM的操作导致的,此时需要进一步排查。

  6. 日志排查方法
    生产情况下,排查多线程的线程很难排查,比较常见的方法就是用过日志排查,日志排查的时间线最好也要打印出来,通过对比

  7. 总结
    现场常见问题分类
    通常情况下来说,系统部署在线上出现故障,经过分析排查后,最终诱发问题的根本原因通常有以下几点:
    ● 应用程序本身导致的问题

    ○ 程序内部频繁触发GC,造成系统出现长时间停顿,导致客户端堆积大量请求。
    ○ JVM参数配置不合理,导致线上运行失控,如堆内存、各内存区域太小等。
    ○ Java程序代码存在缺陷,导致线上运行出现Bug,如死锁/内存泄漏、溢出等。
    ○ 程序内部资源使用不合理,导致出现问题,如线程/DB连接/网络连接/堆内存等。
    ● 上下游内部系统导致的问题

    ○ 上游服务出现并发情况,导致当前程序请求量急剧增加,从而引发问题拖垮系统。
    ○ 下游服务出现问题,导致当前程序堆积大量请求拖垮系统,如Redis宕机/DB阻塞等。
    ● 程序所部署的机器本身导致的问题

    ○ 服务器机房网络出现问题,导致网络出现阻塞、当前程序假死等故障。
    ○ 服务器中因其他程序原因、硬件问题、环境因素(如断电)等原因导致系统不可用。
    ○ 服务器因遭到入侵导致Java程序受到影响,如木马病毒/矿机、劫持脚本等。
    ● 第三方的RPC远程调用导致的问题

    ○ 作为被调用者提供给第三方调用,第三方流量突增,导致当前程序负载过重出现问题。
    ○ 作为调用者调用第三方,但因第三方出现问题,引发雪崩问题而造成当前程序崩溃。
    现场问题排查的整体思路
    现场排查问题,一定要熟悉系统的调用流程和系统,这是基本的,不要假设不可能,整理好排查问题的思路(二分方法)。生产环境出现问题,首先就是要定位问题出现的节点,针对节点进行逐一排查,出现问题的原因按照排查的优先级就是这几个方面:代码,CPU,内存,磁盘,网络。
    首先查看代码是不是出现漏洞,查看CPU调度,是否出现死锁,内存是否溢出,是否出现频繁GC,导致线上服务器响应缓慢,磁盘和网络是否不可用。
    在排查问题时,还需要注意不能影响线上程序的运行,但是通常JVM提供的大部分分析工具都会影响性能,所以最好在生产环境中能够隔离一个独立的机器,用于保留现场并调试问题。如果由于故障已经影响了线上的服务,而且短时间无法定位问题,那么终极方法就是:版本回滚,服务降级,然后重启应用,能保证线上的服务正常运行,再去定位问题。
    最后,任何生产环境故障排除的方法都不能影响线上服务运行。

参考

  1. https://arthas.aliyun.com/doc/
  2. https://github.com/alibaba/arthas/blob/master/math-game/src/main/java/demo/MathGame.java
  3. https://juejin.cn/post/7086038306177155103
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

企鹅宝儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值