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

背景

  • 笔者的大数据监控系统中有一项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了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352