美文网首页
记springboot程序OOM排查和解决过程

记springboot程序OOM排查和解决过程

作者: _Kantin | 来源:发表于2021-11-10 10:36 被阅读0次

背景

  • 笔者的大数据监控系统中有一项hdfs路径下异常格式文件检测的功能。简单的说就是每天需要定期的采集hdfs下的路径。
  • 在某天添加了hive staging路径后,发现程序OOM了。当时从代码出发怀疑是(1)从DB查询过大没有分页导致直接load到内存导致OOM,(2)线程池中blockqueue是没有设置大小的,可能任务都提交到blockqueue中导致内存溢出。
  • 基于上述两个可能点对代码进行了修改并发布,但是在第二天还是又OOM了!!
  • 于是只能对堆内存进行分析来找出真正的泄漏点。在这里仅分享最关键的一处泄漏点。
  • 注: 可能有朋友问为何不通过解析fsimage来获取hdfs详情,其实fsimage的解析每天都有在做。笔者每天起docker容器拉取fsimage并解析后导入到hive分区表中,再进行相关的加工后导入到mysql中,此过程虽已标准化但是比较麻烦。我也参考了hadoop回放fsimage代码,通过java对fsimage进行解析,但是所需的JVM堆内存需要很大(如:fsimage 20G则至少需要40G JVM内存)。这也是没有用java解析的原因,如果有你有更好的办法麻烦跟我联系,谢谢

排查过程

  • 首先看下项目JVM参数。我使用的是G1回收期(确实给力),然后有记录相关的GC日志,这个可以帮助我觉得到底要设置多大Xmx和Xms,然后在程序OOM的时候会自动给我dump下堆内存(虽然dump过程中对程序有影响,但是好像没其他更好办法了!!)
${JAVA_EXEC} -server -XX:+UseG1GC -Xmx8G -Xms8G -Xss256k -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=/var/log/xxx  -XX:MaxGCPauseMillis=300 -Xloggc:/var/log/xxx/xxx_gc.log  -XX:+PrintGCTimeStamps -XX:+PrintGCDetails   -Dservice.name=${EXEC_COMMEN} -Dfastjson.parser.safeMode=true   -cp "${FULL_PATH_SERVER_JAR}:${LIB_PATH}/*:${CONFIG_VERSION_PATH}/" ${MAIN_CLASS} >> ${LOG_FILE} 2>&1 &

  • 在dump程序之前,我先用jmap -histo:live <PID>执行了一次强制的full GC,之后通过arthas的heapdump 命令dump下来,也可以用jmap的dump命令。

  • 关于OOM分析工具,visualvm和eclipse memory analyze我都有用,总体上觉得MemoryAnalyze会好用一些。

  • 如下图所示是程序中加载类的清单,可以看到hdfs的configuration占据了绝大多数


    image.png
  • 同时看到重点包加载也都是跟hadoop的filesystem相关,因此可以判断这个可能是主要的泄漏点。

    image.png
  • 其它的还可以看到很多kerberos鉴权类也没有释放


    image.png

代码排查和修改

  • 在代码的工具类中有个获取filesystem的方法。代码通过cluster对象来携带集群的详情,然后构建FileSystem的过程(很多线程都会调用此方法),每个进程进来都先初始化一个Configuration对象,然后进行kerberos鉴权!相关代码如下:
 public FileSystem getFileSystemInstance(Cluster cluster){
        FileSystem fs = null;
        HadoopClusterParam param = JSONObject.parseObject(cluster.getParam(), HadoopClusterParam.class);
        Configuration conf = new Configuration();
        System.setProperty("java.security.krb5.conf", param.getKrb5Conf());
        conf.set("dfs.namenode.kerberos.principal", param.getHdfsKerberosPrincipal());
        conf.set("dfs.namenode.kerberos.principal.pattern", "*");
        conf.set("hadoop.security.authentication", "kerberos");
        conf.set("fs.trash.interval", "1");
        conf.set("fs.defaultFS", String.format("hdfs://%s", param.getHaName()));
        conf.set(String.format("dfs.client.failover.proxy.provider.%s", param.getHaName()), "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider");
        conf.set(String.format("dfs.ha.namenodes.%s", param.getHaName()), param.getHaNamenodes());
        String[] nns = param.getHaNamenodes().split(",");
        String[] nnHosts = param.getNamenodeAddress().split(",");
        conf.set(String.format("dfs.namenode.rpc-address.%s.%s", param.getHaName(), nns[0]), String.format("%s:8020", nnHosts[0]));
        conf.set(String.format("dfs.namenode.rpc-address.%s.%s", param.getHaName(), nns[1]), String.format("%s:8020", nnHosts[1]));
        conf.set("dfs.nameservices", param.getHaNames());
        try {
            UserGroupInformation.setConfiguration(conf);
            UserGroupInformation.loginUserFromKeytab(param.getHdfsKerberosPrincipal(), param.getHdfsKerberosKeytab());
            fs =  FileSystem.get(conf);
        } catch (Exception e) {
            LOGGER.error("Build FileSystem found execption,caused by:", e);
        }
        return fs;
    }
  • 上面代码有两个致命缺点(1)Configuration其实是可以复用的而不需要每次重新new,(2)等于登陆一次kerberos鉴权即可而不需要每个都用UserGroupInformation去鉴权。
  • 基于上面的致命缺点进行修改后的代码如下:
    private Map<String,Configuration> confMap = new HashMap<>();

    private Configuration generateFileSystemConf(Cluster cluster) throws IOException {

        UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
        String userName = currentUser.getUserName();
        //防止其它操作更新掉当前线程中的kerberos认证用户
        if(!HDFS_USER.equals(userName)){
            LOGGER.info("The login user has changed,current user:{},change to {}",userName,HDFS_USER);
            confMap.remove(cluster.getClusterName());
        }
        if(confMap.getOrDefault(cluster.getClusterName(),null)==null){
            Configuration conf = new Configuration();
            HadoopClusterParam param = JSONObject.parseObject(cluster.getParam(), HadoopClusterParam.class);
            System.setProperty("java.security.krb5.conf", param.getKrb5Conf());
            conf.set("dfs.namenode.kerberos.principal", param.getHdfsKerberosPrincipal());
            conf.set("dfs.namenode.kerberos.principal.pattern", "*");
            conf.set("hadoop.security.authentication", "kerberos");
            conf.set("fs.trash.interval", "1");
            conf.set("fs.defaultFS", String.format("hdfs://%s", param.getHaName()));
            conf.set(String.format("dfs.client.failover.proxy.provider.%s", param.getHaName()), "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider");
            conf.set(String.format("dfs.ha.namenodes.%s", param.getHaName()), param.getHaNamenodes());
            String[] nns = param.getHaNamenodes().split(",");
            String[] nnHosts = param.getNamenodeAddress().split(",");
            conf.set(String.format("dfs.namenode.rpc-address.%s.%s", param.getHaName(), nns[0]), String.format("%s:8020", nnHosts[0]));
            conf.set(String.format("dfs.namenode.rpc-address.%s.%s", param.getHaName(), nns[1]), String.format("%s:8020", nnHosts[1]));
            conf.set("dfs.nameservices", param.getHaNames());
            UserGroupInformation.setConfiguration(conf);
            UserGroupInformation.loginUserFromKeytab(param.getHdfsKerberosPrincipal(), param.getHdfsKerberosKeytab());
            confMap.put(cluster.getClusterName(),conf);
            return conf;
        }
        return confMap.get(cluster.getClusterName());
    }

    public FileSystem getFileSystemInstance(Cluster cluster){
        FileSystem fs = null;
        try {
            Configuration conf = this.generateFileSystemConf(cluster);
            fs =  FileSystem.get(conf);
        } catch (Exception e) {
            LOGGER.error("Build FileSystem found execption,caused by:", e);
        }
        return fs;
    }
  • 修改后重新发布程序,运行了几天之后再也没有OOM的现象。从下图可见每次minor gc都能正常的回收,old_gen也维持在一个较低的范围内。


    image.png

后记

  • 为什么Configuration和UserGroupInformation无法回收的问题,我猜测可能跟FileSystem的设计有关。
  • Hadoop把对于文件系统的调用封装成了一个FileSystem类,同时FileSystem对于文件系统类的实例做了缓存,如果是来自同一个文件系统,它会返回同一个实例。代码如下:
  public static FileSystem get(URI uri, Configuration conf) throws IOException {
    String scheme = uri.getScheme();
    String authority = uri.getAuthority();

    if (scheme == null && authority == null) {     // use default FS
      return get(conf);
    }

    if (scheme != null && authority == null) {     // no authority
      URI defaultUri = getDefaultUri(conf);
      if (scheme.equals(defaultUri.getScheme())    // if scheme matches default
          && defaultUri.getAuthority() != null) {  // & default has authority
        return get(defaultUri, conf);              // return default
      }
    }
    
    String disableCacheName = String.format("fs.%s.impl.disable.cache", scheme);
    if (conf.getBoolean(disableCacheName, false)) {
      return createFileSystem(uri, conf);
    }

    return CACHE.get(uri, conf);
  }
  • 为什么要设置cache?我猜是因为每个FileSystem的实例都会建立一个到HDFS Namenode的连接,在大量并发的读时缓存确实能减低namenode的压力。那么可能产生什么情况?你从FileSystem里面拿到的文件系统的实例可能别人也拿到了,而且可能正在用,所以这里也是不推荐你close掉fs的原因
  • 由于FileSystem是缓存在hadoop中的,则对Configuration和UserGroupInformation的引用关系是存在的,也即GC ROOT是存在的,因此当前不会被回收。慢慢堆积也就OOM了。

相关文章

  • 记springboot程序OOM排查和解决过程

    背景 笔者的大数据监控系统中有一项hdfs路径下异常格式文件检测的功能。简单的说就是每天需要定期的采集hdfs下的...

  • 2. Interview-JVM&GC

    JVM知识图谱 1 怎么解决OOM?/ 怎么排查OOM?/ JVM调优 参考:https://blog.csdn....

  • 再一次oom的记录

    上一次oom的记录在一次排查OOM的总结,这次oom的排查大概花了一天不到,和leader一起,我这边主要是根据后...

  • OOM排查

    http://ifeve.com/one-java-oom/

  • OOM排查

    一般CPU100%疯狂GC,都是死循环的锅,那怎么排查呢? 1):先进服务器,用top -c 命令找出当前进程的运...

  • zookeeper oom

    一:问题: zookeeper oom挂掉之后重启及选举失败 二:排查: 2.1 oom信息: 其中,FileTx...

  • OOM 和 FGC异常排查

    概述 OOM: JVM一般都是先尝试GC,GC以后仍然无法腾出空间给新对象的时候才会针对对应线程触发OOM 另外O...

  • OOM问题

    OOM是开发中会经常遇见的一类问题,其中很多原因是可以在写代码阶段就可以排查出来的,本文结合之前解决的OOM的问题...

  • jprofiler定位内存占用过多问题

    现象 解决办法 排查过程 结论

  • Android 线上 OOM 的排查过程

    作者:王晨彦转载地址:https://juejin.cn/post/7055644963706503204[htt...

网友评论

      本文标题:记springboot程序OOM排查和解决过程

      本文链接:https://www.haomeiwen.com/subject/rhybzltx.html