线上问题排查内存泄漏排查(模拟真实环境)

Posted 码农BookSea

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线上问题排查内存泄漏排查(模拟真实环境)相关的知识,希望对你有一定的参考价值。

文章目录



内存泄漏

什么是内存泄漏?

内存泄露 Memory Leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

Memory Leak会最终会导致Out Of Memory!

模拟内存泄漏

写一段 ThreadLocal 模拟内存泄漏的代码。

/**
 * <p>
 *  模拟ThreadLocal内存泄露导致OOM
 *  JVM启动参数 -Xms20M -Xmx20M -Xmn10M
 * </p>
 */
    @GetMapping("/memoryLeak")
    public void memoryLeak() 
        // 是否调用remove方法
        boolean doRemove = false;
        // 加锁,让多个线程串行执行,避免多个线程同时占用内存导致的内存溢出问题
        final Object lockObj = new Object();
        // 开启20个线程
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        // 为了不重复使用线程,用Map标记一下已经已使用过的线程,
        Map<Long, Integer> threadIdMap = new ConcurrentHashMap<>();
        // 循环向线程变量中设置数据 1024 * 1024 = 1M
        for (int i = 0; i < 20; i++) 
            executorService.execute(() -> 
                synchronized (lockObj) 
                    Integer num = threadIdMap.putIfAbsent(Thread.currentThread().getId(), 1);
                    if (num == null) 
                        ThreadLocal<Byte[]> threadLocal = new ThreadLocal<>();
                        threadLocal.set(new Byte[1024 * 1024]);
                        // 手工回收
                        System.gc();
                        try 
                            // 调用GC后不一定会马上回收
                            Thread.sleep(500);
                         catch (InterruptedException e) 
                            e.printStackTrace();
                        
                    

                    System.out.println(Thread.currentThread().getName());
                
            );
        

    

打包部署到服务器。并使用
nohup java -jar -Xms20M -Xmx20M -Xmn10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof & 运行。

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof 参数
是为了发生OOM的时候会自动导出Dump文件

部署之后请求一下接口: http://localhost:8080/memoryLeak
服务器上出现报错信息

使用 jstat -gcutil 线程ID 1000 查看,FGC比较频繁,多半是有问题。

接下来我们开始排查

排查过程

打开 heapdump.hprof 文件

文件还是有点大的
我们可以使用JDK自带的 Jvisualvm 打开文件

或者执行 jmap -histo:live 进程ID 查看内存中的存活对象

Byte[] 占用了空间的 46.7%,基本可以断定为是 Byte[] 没有被回收导致的内存泄漏
然后查看代码中使用到 Byte[] 地方的代码即可定位。

IDEA中可以使用 Ctrl + Shift +R 实现全局搜索

Java线上故障排查

      Java线上故障主要会包括 CPU、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。同时例如 jstack、jmap 等工具也是不囿于一个方面的问题的,基本上出问题就是 df、free、top 三连,然后依次 jstack、jmap 伺候,具体问题具体分析。

      内存泄漏、内存溢出、CPU飙升三者之间的关系:内存泄露可能会导致内存溢出。 内存溢出会抛出异常,内存泄露不会抛出异常,大多数时候程序看起来是正常运行的。 内存泄露的程序,JVM频繁进行FullGC尝试释放内存空间,进而会导致CPU飙升,内存泄露过多,造成可回收内存不足,程序申请内存失败,结果就是内存溢出。

一、CPU问题

CPU 异常往往还是比较好定位的。原因包括业务逻辑问题(死循环)、频繁 gc 以及上下文切换过多。而最常见的往往是业务逻辑(或者框架逻辑)导致的,可以使用 jstack 来分析对应的堆栈情况,具体步骤是:

  1. 查看当前占用cpu最高的进程pid:top
  2. 获取当前进程中所有线程占CPU的情况:top -Hp pid
  3. 将占用最高的pid转换为16进制得到nid:printf %x\\n pid
  4. 在 jstack 中找到相应的堆栈信息:jstack pid |grep nid -C5 –color
  5. 保存线程栈现场到指定文件里分析:jstack pid > jstack.log

对整个 jstack 文件进行分析,通常我们会比较关注 WAITING 和 TIMED_WAITING 的部分,BLOCKED 就不用说了。我们可以使用命令cat jstack.log | grep "java.lang.Thread.State" | sort -nr | uniq -c来对 jstack 的状态有一个整体的把握,如果 WAITING 之类的特别多,那么多半是有问题。

gc问题:

查看堆的各个部分的详细的使用情况:

  1. jstat -gc pid [1000 10]

查看gc情况,每1秒打印一次总共打印10次(可选),可以查看各个带的使用总大小和使用大小对于jvm的优化就是要去优化它的FullGC次数,FullGC越少越好,最好控制在FullGC几个小时甚至几天一次,具体看业务的情况。

jstat参数说明:

S0C:第一个幸存区的大小(From Survivor区),以下几个容量的单位都是KB 
S1C:第二个幸存区的大小 (To Survivor区)
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小 (Eden区)
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小(元空间)
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间,单位s
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间,单位s
GCT:垃圾回收消耗总时间,单位s

根据jstat查看出来的gc情况,我们可能需要以下几个主要指标:

各内存区域大小是否合理;
观察Eden区的对象增长,如每秒有多少对象创建;
每次YoungGC后有多少对象存活下来、有多少对象进入了老年代;
YoungGC的耗时;
FullGC触发频率及耗时;

二、内存问题

内存问题排查起来相对比 CPU 麻烦一些,场景也比较多。主要包括 OOM、GC 问题和堆外内存。

2.1 内存溢出OOM

大致可以分为以下几种:

  • Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

这个意思是没有足够的内存空间给线程分配 Java 栈,基本上还是线程池代码写的有问题,比如说忘记 shutdown,所以说应该首先从代码层面来寻找问题,使用 jstack 或者 jmap。如果一切都正常,JVM 方面可以通过指定Xss来减少单个 thread stack 的大小。另外也可以在系统层面,可以通过修改/etc/security/limits.confnofile 和 nproc 来增大 os 对线程的限制

  • Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

这个意思是堆的内存占用已经达到-Xmx 设置的最大值,应该是最常见的 OOM 错误了。解决思路仍然是先应该在代码中找,怀疑存在内存泄漏,通过 jstack 和 jmap 去定位问题。如果说一切都正常,才需要通过调整Xmx的值来扩大内存。

  • Caused by: java.lang.OutOfMemoryError: Meta space

这个意思是元数据区的内存占用已经达到XX:MaxMetaspaceSize设置的最大值,排查思路和上面的一致,参数方面可以通过XX:MaxPermSize来进行调整。

2.2 栈内存溢出Stack Overflow

  • Exception in thread "main" java.lang.StackOverflowError

表示线程栈需要的内存大于 Xss 值,同样也是先进行排查,参数方面通过Xss来调整,但调整的太大可能又会引起 OOM。

2.3 内存溢出的检查方法

  1. 在启动参数中指定-XX:+HeapDumpOnOutOfMemoryError来保存 OOM 时的 dump 文件
  2. 导出 dump 文件:jmap -dump:format=b,file=filename pid
  3. 通过 mat(Eclipse Memory Analysis Tools)导入 dump 文件进行分析,内存泄漏问题一般我们直接选 Leak Suspects 即可,mat 给出了内存泄漏的建议;另外也可以选择 Top Consumers 来查看最大对象报告;和线程相关的问题可以选择 thread overview 进行分析;除此之外就是选择 Histogram 类概览来自己慢慢分析。
  4. 线程的话太多而且不被及时 gc 也会引发 oom,大部分就是之前说的unable to create new native thread。除了 jstack 细细分析 dump 文件外,我们一般先会看下总体线程,通过pstreee -p pid |wc -l

三、网络问题

涉及到网络层面的问题一般都比较复杂,场景多,从 tcp 层、应用层以及工具的使用等方面进行阐述。

3.1 超时

超时错误大部分处在应用层面,所以这块着重理解概念。超时大体可以分为连接超时和读写超时,某些使用连接池的客户端框架还会存在获取连接超时和空闲连接清理超时。

  • 读写超时。readTimeout/writeTimeout,有些框架叫做 so_timeout 或者 socketTimeout,均指的是数据读写超时。注意这边的超时大部分是指逻辑上的超时。soa 的超时指的也是读超时。读写超时一般都只针对客户端设置。
  • 连接超时。connectionTimeout,客户端通常指与服务端建立连接的最大时间。服务端这边 connectionTimeout 就有些五花八门了,Jetty 中表示空闲连接清理时间,Tomcat 则表示连接维持的最大时间。
  • 其他。包括连接获取超时 connectionAcquireTimeout 和空闲连接清理超时 idleConnectionTimeout。多用于使用连接池或队列的客户端或服务端框架。

我们在设置各种超时时间中,需要确认的是尽量保持客户端的超时小于服务端的超时,以保证连接正常结束。

3.2 TCP队列溢出

  • netstat 命令,执行 netstat -s | egrep "listen|LISTEN"

overflowed 表示全连接队列溢出的次数,sockets dropped 表示半连接队列溢出的次数。

  • ss 命令,执行 ss -lnt

Send-Q 表示第三列的 listen 端口上的全连接队列最大为值,第一列 Recv-Q 为全连接队列当前使用了多少。

3.3 RST 异常

RST 包表示连接重置,用于关闭一些无用的连接,通常表示异常关闭,区别于四次挥手。

在实际开发中,我们往往会看到connection reset / connection reset by peer错误,这种情况就是 RST 包导致的。

3.4 端口不存在

如果像不存在的端口发出建立连接 SYN 请求,那么服务端发现自己并没有这个端口则会直接返回一个 RST 报文,用于中断连接。

3.5 主动代替 FIN 终止连接

一般来说,正常的连接关闭都是需要通过 FIN 报文实现,然而我们也可以用 RST 报文来代替 FIN,表示直接终止连接。实际开发中,可设置 SO_LINGER 数值来控制,这种往往是故意的,来跳过 TIMED_WAIT,提供交互效率,不闲就慎用。

3.6 TIME_WAIT 和 CLOSE_WAIT

ss -ant | awk ++S[$1] END for(a in S) print a, S[a]

time_wait 的存在一是为了丢失的数据包被后面连接复用,二是为了在 2MSL 的时间范围内正常关闭连接。它的存在其实会大大减少 RST 包的出现。

过多的 time_wait 在短连接频繁的场景比较容易出现。这种情况可以在服务端做一些内核参数调优:

#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1

当然我们不要忘记在 NAT 环境下因为时间戳错乱导致数据包被拒绝的坑了,另外的办法就是改小tcp_max_tw_buckets,超过这个数的 time_wait 都会被干掉,不过这也会导致报time wait bucket table overflow的错。

close_wait 往往都是因为应用程序写的有问题,没有在 ACK 后再次发起 FIN 报文。close_wait 出现的概率甚至比 time_wait 要更高,后果也更严重。往往是由于某个地方阻塞住了,没有正常关闭连接,从而渐渐地消耗完所有的线程。想要定位这类问题,最好是通过 jstack 来分析线程堆栈来排查问题。

以上是关于线上问题排查内存泄漏排查(模拟真实环境)的主要内容,如果未能解决你的问题,请参考以下文章

记一次线上内存溢出问题排查过程

Java线上故障排查

java内存泄漏问题排查

排查 Node.js 服务内存泄漏,没想到竟是它?

Java内存异常触发FGC无效导致CPU持续100%原因(线上JVM排查之三)

Java内存异常触发FGC无效导致CPU持续100%原因(线上JVM排查之三)