一次线上问题排查所引起的思考

前言

以前或多或少分享过一些内存模型对象建立之类的内容,其实大部分人看完都是懵懵懂懂,也不知道这些的实际意义。java

直到有一天你会碰到线上奇奇怪怪的问题,如:git

  • 线程执行一个任务迟迟没有返回,应用假死。
  • 接口响应缓慢,甚至请求超时。
  • CPU 高负载运行。

这类问题并不像一个空指针、数组越界这样明显好查,这时就须要刚才提到的内存模型、对象建立、线程等相关知识结合在一块儿来排查问题了。github

正好此次借助以前的一次生产问题来聊聊如何排查和解决问题。shell

生产现象

首先看看问题的背景吧:数据库

我这实际上是一个定时任务,在固定的时间会开启 N 个线程并发的从 Redis 中获取数据进行运算。数组

业务逻辑很是简单,但应用通常涉及到多线程以后再简单的事情都要当心对待。网络

果不其然此次就出问题了。多线程

现象:本来只须要执行几分钟的任务执行了几个小时都没退出。翻遍了全部的日志都没找到异常。并发

因而便开始定位问题之路。运维

定位问题

既然没办法直接从日志中发现异常,那就只能看看应用到底在干吗了。

最多见的工具就是 JDK 自带的那一套。

此次我使用了 jstack 来查看线程的执行状况,它的做用其实就是 dump 当前的线程堆栈。

固然在 dump 以前是须要知道我应用的 pid 的,可使用 jps -v 这样的方式列出全部的 Java 进程。

固然若是知道关键字的话直接使用 ps aux|grep java 也是能够的。

拿到 pid=1523 了以后就能够利用 jstack 1523 > 1523.log 这样的方式将 dump 文件输出到日志文件中。

若是应用简单不复杂,线程这些也比较少其实能够直接打开查看。

但复杂的应用导出来的日志文件也比较大仍是建议用专业的分析工具。

我这里的日志比较少直接打开就能够了。

由于我清楚知道应用中开启的线程名称,因此直接根据线程名就能够在日志中找到相关的堆栈:

因此一般建议你们线程名字给的有意义,在排查问题时颇有必要。

其实其余几个线程都和这里的堆栈相似,很明显的看出都是在作 Redis 链接。

因而我登陆 Redis 查看了当前的链接数,发现已经很是高了。

这样 Redis 的响应天然也就变慢了。

接着利用 jps -v 列出了当前因此在跑的 Java 进程,果不其然有好几个应用都在查询 Redis,并且都是并发链接,问题天然就找到了。

解决办法

因此问题的主要缘由是:大量的应用并发查询 Redis,致使 Redis 的性能下降。

既然找到了问题,那如何解决呢?

  • 减小同时查询 Redis 的应用,分开时段下降 Redis 的压力。
  • 将 Redis 复制几个集群,各个应用分开查询。可是这样会涉及到数据的同步等运维操做,或者由程序了进行同步也会增长复杂度。

目前咱们选择的是第一个方案,效果很明显。

本地模拟

上文介绍的是线程相关问题,如今来分析下内存的问题。

以这个类为例:

https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/oom/heap/HeapOOM.java

public class HeapOOM {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>(10) ;
        while (true){
            list.add("1") ;
        }
    }
}

启动参数以下:

-Xms20m
-Xmx20m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/Users/xx/Documents

为了更快的突出内存问题将堆的最大内存固定在 20M,同时在 JVM 出现 OOM 的时候自动 dump 内存到 /Users/xx/Documents(不配路径则会生成在当前目录)。

执行以后果不其然出现了异常:

同时对应的内存 dump 文件也生成了。

内存分析

这时就须要相应的工具进行分析了,最经常使用的天然就是 MAT 了。

我试了一个在线工具也不错(文件大了就不适合了):

http://heaphero.io/index.jsp

上传刚才生成的内存文件以后:

由于是内存溢出,因此主要观察下大对象:

也有相应提示,这个颇有可能就是内存溢出的对象,点进去以后:

看到这个堆栈其实就很明显了:

在向 ArrayList 中不停的写入数据时,会致使频繁的扩容也就是数组复制这些过程,最终达到 20M 的上限致使内存溢出了。

更多建议

上文说过,一旦使用了多线程,那就要格外当心。

如下是一些平常建议:

  • 尽可能不要在线程中作大量耗时的网络操做,如查询数据库(能够的话在一开始就将数据从从 DB 中查出准备好)。
  • 尽量的减小多线程竞争锁。能够将数据分段,各个线程分别读取。
  • 多利用 CAS+自旋 的方式更新数据,减小锁的使用。
  • 应用中加上 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp 参数,在内存溢出时至少能够拿到内存日志。
  • 线程池监控。如线程池大小、队列大小、最大线程数等数据,可提早作好预估。
  • JVM 监控,能够看到堆内存的涨幅趋势,GC 曲线等数据,也能够提早作好准备。

总结

线上问题定位须要综合技能,因此是须要一些基础技能。如线程、内存模型、Linux 等。

固然这些问题没有实操过都是纸上谈兵;若是第一次碰到线上问题,不要慌张,反而应该庆幸解决以后你又会习得一项技能。

号外

最近在总结一些 Java 相关的知识点,感兴趣的朋友能够一块儿维护。

地址: https://github.com/crossoverJie/Java-Interview

相关文章
相关标签/搜索