Java服务相关问题排查

675 阅读6分钟

前言

作为程序员我们平时除了具备完成需求的能力,还需要解决问题的能力,这里说的问题不是指业务逻辑的bug,而是指服务出现了故障导致服务需要降级甚至熔断,就算重启可能能解决问题,我们也需要定位出来,不容许服务存在故障隐患。

服务出现问题主要分为四类:CPU、内存、磁盘、网络。

对于问题我们能做的也分为三个阶段:事前、事中、事后。

事前
既然我们知道会出现问题,我们可以在上线前做好监控&预警&警报、根据目前服务进行抗压估算进行限流等。

监控&预警&警报可以使用普罗米修斯

事中
事中就是通过预警感知异常,如果达到设置的阈值可以对服务进行降级避免继续恶化,开始进行定位。

事后
已经发送了宕机了只能进行熔断,进行定位问题。

下面我们具体讲讲四类问题该怎么定位和解决。

CPU问题

CPU问题指的是CPU占用率过高,CPU过高会导致死机甚至重启。CPU过高产生的原因(不考虑配置过低,我们可以防止和解决的)包括如下:

  • 编写的程序不合理。如:线程池的设置不合理(比如允许的核心数或者非核心线程数设置过大,cpu来回切换);出现死锁;频繁创建大对象导致频繁gc;程序处理慢需要优化算法等等。
  • 没有做好限流,请求进来的接口处理不过来。
  • 病毒、木马等

CPU占用情况可以使用,top命令进行查看,看到是java进程(我们这里主要是分析java)的话我们就进一步分析。因为CPU占用过高的原因有放进来的请求过多cpu忙碌(不仅任务多还需要CPU切换)、异步处理使用的线程过多(不仅任务多还需要CPU切换)、出现死锁(线程被占用)、频繁gc、某些任务长时间占用。

请求过多

这个在事前就应该根据服务器的压测配置好限流,一般情况不会因为这个导致的,但是如果想确定是否是因为这个可以使用普罗米修斯拿到tps等数据。

死锁

死锁的话可以使用《jvm之监控工具-命令行工具》中的jstack进行分析,文章中有写到,这里就不赘述。还可以通过可视化visualVM来查看。

image.png

可以看到和jstack一样的,不过这个工具提供可视化页面,可以看到线程持续了多长时间。

我们点一下线程dump

image.png

频繁GC

示例: vm参数: 这里因为堆内存我分配的比较少,系统中也存在一些对象,为了更加直观我指定了-Xmn5m新生代区域内存大小,目的是更加直观。

-Xms10m
-Xmx10m
-Xmn5m
-XX:-DoEscapeAnalysis
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/xxx

测试代码:

package com.study.jvm.memory;

import lombok.SneakyThrows;

import java.util.ArrayList;
import java.util.List;

public class GcOfenTest {
    static class Object {
        private Long l1;
        private Long l2;
        private Long l3;
        private Long l4;
        private Long l5;
        private Long l6;
        private Long l7;
        private Long l8;
    }

    @SneakyThrows
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            List<Object> objectList=new ArrayList<>(5000);
            Thread.sleep(500);
        }
    }
}

可以看到我的最多只分配了10M内存给堆用,频繁的创建可以被回收的大对象,对象的大小为64*5000个字节=312.5k,对于只有4M的Eden算是大的了。

到程序结束为止进行了30多次ygc。我们来看看dump文件(注意笔者这里使用的是jmap -dump:format=b,file=heap.bin <pid>进行生成dump的,因为VisualVM的dump使用的是默认的jmap -dump:live,format=b,file=heap.bin <pid>产生的是live对象的快照,在生成的时候会进行fullgc):

image.png

image.png

发现点进来看不出啥,因为笔者指导自己创建的数组cap为5000所以知道这个就是List占用的空间。

那么我们还能咋看呢?可以看看线程dump,结合代码也能看的出来。但是不够直接,读者有更好的方式欢迎留言。

image.png

cpu切换

通过top已经拿到pid,可以使用pidstat -w进行查看每个上下文切换情况。

pidstat -w -p 2831

image.png

  • PID:进程id
  • Cswch/s:每秒主动任务上下文切换数量
  • Nvcswch/s:每秒被动任务上下文切换数量
  • Command:命令名

除了查看Nvcswch/s列大概感知下每秒切换的数量,如果比较大,我们基本就确定了任务过多了。如果设置了限流就可以需要分析下是不是接口的响应时间变长了,导致任务的堆积,如果没有就需要看看是不是异步的线程池设置的不好,允许并发的线程数过多。

某些任务长时间占用

就是线程一 首先显示线程列表:

ps -mp <pid> -o THREAD,tid,time

线程28802占用cpu时间快两个小时了

image.png

其次将需要的线程ID转换为16进制格式(因为jstack打印出来的是十六进制):

printf "%x\n" <tid>

image.png

最后打印线程的堆栈信息:

jstack <pid> |grep <tid> -A 30

image.png

内存

内存只要分为两类:内存泄漏和内存溢出。

内存泄漏

内存泄漏指的是对象因为程序编码问题长期被GC Root引用导致无法释放。

堆泄漏

示例:

vm参数:
-Xms10m
-Xmx10m
-XX:-DoEscapeAnalysis
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/xxx

代码:

package com.study.jvm.memory;

import lombok.SneakyThrows;

import java.util.ArrayList;
import java.util.List;

public class OOMTest {
    static class OOMObject {
        private Long l1;
        private Long l2;
        private Long l3;
        private Long l4;
        private Long l5;
        private Long l6;
        private Long l7;
        private Long l8;
    }

    @SneakyThrows
    public static void main(String[] args) {
        List<OOMObject> oomObjectList = new ArrayList<>();

        for (int i = 0; i < 163840; ++i) {
            oomObjectList.add(new OOMObject());
            if (i % 1000 == 0) {
                System.out.println(i);
                Thread.sleep(300);
            }
        }
    }
}

输出:

...
119000
120000
121000
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/naigaipaopao/java_pid11879.hprof ...
Heap dump file created [15989379 bytes in 0.112 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.study.jvm.memory.OOMTest.main(OOMTest.java:25)

可以看到java.lang.OutOfMemoryError: Java heap space了堆空间不足,下面我们来看看visualVM:

image.png 发现已经eden和老年代都已经满了所以OOM了。

在来看看生成的dump文件:

image.png

image.png

image.png

总结

其实这个例子很难去说是泄漏还是溢出,需要具体看场景,重点是java.lang.OutOfMemoryError: Java heap space

内存溢出

内存溢出是指内存空间不够分配,下面我们举几个常见的场景。

metaspace溢出

元空间:存放类的元信息、静态变量、生成的字节码(JIT、Cglib等生成)等等。(在《JVM之内存结构》中有提到。)

类的卸载

我们想模拟把metaspace撑爆,需要先了解什么时候才会把类从内存中卸载:java默认的加载器BootStrapClassLoader、ExtClassLoader、AppClassLoader,java虚拟机本身会始终引用这些类加载器,这些类加载器则会始终引用它们所加载的类的Class对象,所以默认的类加载器加载的对象不会被卸载。

示例

我们可以使用之前《静态代理、动态代理、cglib代理》中的cglib例子来把元空间撑满,我们需要改下main使其一直动态生成字节码,并且限制元空间的大小,具体见下:

vm参数:

-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/xxx

示例:

public class Student implements Person{
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public void giveMoney() {
//        System.out.println(name + "交了50元班费");
    }
}
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class StudentCglibProxy implements MethodInterceptor {
    private Class targetClass;

    public StudentCglibProxy(Class targetClass) {
        this.targetClass = targetClass;
    }

    //为目标对象生成代理对象
    public Object getProxyInstance(String name) {
        //工具类
        Enhancer en = new Enhancer();
        //设置父类
        en.setSuperclass(targetClass);
        //不使用缓存中已有的,重新生成
        en.setUseCache(false);
        //设置回调函数
        en.setCallback(this);
        //创建子类对象代理
        return en.create(new Class[]{String.class}, new Object[]{name});
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//        System.out.println("代理执行" + method.getName() + "方法");

        // 执行目标对象的方法
        Object returnValue = methodProxy.invokeSuper(obj, args);

        return returnValue;
    }

}
import org.springframework.cglib.core.DebuggingClassWriter;

public class CglibProxyTest {
    public static void main(String[] args) {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./");
        while (true) {
            Student proxyInstance = (Student) new StudentCglibProxy(Student.class).getProxyInstance("张三");
        }
    }

}

输出:

CGLIB debugging enabled, writing to './'
java.lang.OutOfMemoryError: Metaspace
Dumping heap to /Users/naigaipaopao/java_pid10631.hprof ...
Heap dump file created [19588747 bytes in 0.102 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:530)
	at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
	at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:582)
	at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)
	at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
	at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:569)
	at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:403)
	at com.study.proxy.StudentCglibProxy.getProxyInstance(StudentCglibProxy.java:27)
	at com.study.proxy.CglibProxyTest.main(CglibProxyTest.java:31)

可以看到发生了OOM,因为元空间。

visualVM:

image.png

可以看到已经撑爆了元空间。

image.png 一眼就可以看到是mian线程发生了OOM

image.png

image.png

就是因为cglib导致的OOM。

总结

就算是使用cglib代理或者jdk代理一般情况也不会出现java.lang.OutOfMemoryError: Metaspace,因为只是针对于某些被代理类进行,只要做好自动化回归测试覆盖所有功能观察下元空间的情况即可。

栈溢出

一般递归比较容易出现,下面我们试下:

import lombok.SneakyThrows;

public class StackOverflowErrorTest {
    @SneakyThrows
    private static void test() {
        long l = 1L;
        test();
    }

    public static void main(String[] args) {
        test();
    }
}

image.png

磁盘

查看磁盘空间使用情况

可以通过df -h查看磁盘使用情况统计(多大、使用了多少),通过du -h --max-depth=1查看当前目录的一级文件占用空间情况,分析出那个文件占用过多。

查看进程io使用情况

pidstat -d

image.png

  • PID:进程id
  • kB_rd/s:每秒从磁盘中读取的KB
  • kB_wr/s:每秒写入磁盘KB
  • kB_ccwr/s:任务取消的写入磁盘的KB。当前任务截断脏的pagecache的时候会发生。
  • COMMAND:task的命名

网络

netstat

netstat用于查看整个Linux系统的网络情况。

  • 某个端口的情况netstat -nap|grep <pid>

  • 网络统计信息netstat -s

# netstat -s
Ip:
  184695 total packets received
  0 forwarded
  0 incoming packets discarded
  184687 incoming packets delivered
  143917 requests sent out
  32 outgoing packets dropped
  30 dropped because of missing route
Icmp:
  676 ICMP messages received
  5 input ICMP message failed.
  ICMP input histogram:
    destination unreachable: 44
    echo requests: 287
    echo replies: 345
  304 ICMP messages sent
  0 ICMP messages failed
  ICMP output histogram:
    destination unreachable: 17
    echo replies: 287
Tcp:
  473 active connections openings
  28 passive connection openings
  4 failed connection attempts
  11 connection resets received
  1 connections established
  178253 segments received
  137936 segments send out
  29 segments retransmited
  0 bad segments received.
  336 resets sent
Udp:
  5714 packets received
  8 packets to unknown port received.
  0 packet receive errors
  5419 packets sent
TcpExt:
  1 resets received for embryonic SYN_RECV sockets
  ArpFilter: 0
  12 TCP sockets finished time wait in fast timer
  572 delayed acks sent
  3 delayed acks further delayed because of locked socket
  13766 packets directly queued to recvmsg prequeue.
  1101482 packets directly received from backlog
  19599861 packets directly received from prequeue
  46860 packets header predicted
  14541 packets header predicted and directly queued to user
  TCPPureAcks: 12259
  TCPHPAcks: 9119
  TCPRenoRecovery: 0
  TCPSackRecovery: 0
  TCPSACKReneging: 0
  TCPFACKReorder: 0
  TCPSACKReorder: 0
  TCPRenoReorder: 0
  TCPTSReorder: 0
  TCPFullUndo: 0
  TCPPartialUndo: 0
  TCPDSACKUndo: 0
  TCPLossUndo: 0
  TCPLoss: 0
  TCPLostRetransmit: 0
  TCPRenoFailures: 0
  TCPSackFailures: 0
  TCPLossFailures: 0
  TCPFastRetrans: 0
  TCPForwardRetrans: 0
  TCPSlowStartRetrans: 0
  TCPTimeouts: 29
  TCPRenoRecoveryFail: 0
  TCPSackRecoveryFail: 0
  TCPSchedulerFailed: 0
  TCPRcvCollapsed: 0
  TCPDSACKOldSent: 0
  TCPDSACKOfoSent: 0
  TCPDSACKRecv: 0
  TCPDSACKOfoRecv: 0
  TCPAbortOnSyn: 0
  TCPAbortOnData: 1
  TCPAbortOnClose: 0
  TCPAbortOnMemory: 0
  TCPAbortOnTimeout: 3
  TCPAbortOnLinger: 0
  TCPAbortFailed: 3
  TCPMemoryPressures: 0

ping

Linux ping 命令用于检测主机。

curl

Linux curl命令是一个利用URL规则在命令行下工作的文件传输工具

笔者一般用于测试接口,比如get接口https://www.coonote.com/linux/linux-cmd-curl.html,post接口curl -H "Content-Type:application/json" -X POST -d '{"user": "admin", "passwd":"12345678"}' http://127.0.0.1:8000/login

参考

pidstat 命令详解

JVM(十四)visualVM使用分析GC日志,OOM

内存溢出与内存泄漏

JVM 类的卸载

Java 虚拟机笔记 - 类的卸载