記springboot程序OOM排查和解決過程

背景

  • 筆者的大數(shù)據(jù)監(jiān)控系統(tǒng)中有一項(xiàng)hdfs路徑下異常格式文件檢測(cè)的功能。簡(jiǎn)單的說就是每天需要定期的采集hdfs下的路徑涩禀。
  • 在某天添加了hive staging路徑后亿汞,發(fā)現(xiàn)程序OOM了亮垫。當(dāng)時(shí)從代碼出發(fā)懷疑是(1)從DB查詢過大沒有分頁導(dǎo)致直接load到內(nèi)存導(dǎo)致OOM,(2)線程池中blockqueue是沒有設(shè)置大小的,可能任務(wù)都提交到blockqueue中導(dǎo)致內(nèi)存溢出躺涝。
  • 基于上述兩個(gè)可能點(diǎn)對(duì)代碼進(jìn)行了修改并發(fā)布厨钻,但是在第二天還是又OOM了!诞挨!
  • 于是只能對(duì)堆內(nèi)存進(jìn)行分析來找出真正的泄漏點(diǎn)莉撇。在這里僅分享最關(guān)鍵的一處泄漏點(diǎn)。
  • 注: 可能有朋友問為何不通過解析fsimage來獲取hdfs詳情惶傻,其實(shí)fsimage的解析每天都有在做棍郎。筆者每天起docker容器拉取fsimage并解析后導(dǎo)入到hive分區(qū)表中,再進(jìn)行相關(guān)的加工后導(dǎo)入到mysql中银室,此過程雖已標(biāo)準(zhǔn)化但是比較麻煩涂佃。我也參考了hadoop回放fsimage代碼,通過java對(duì)fsimage進(jìn)行解析蜈敢,但是所需的JVM堆內(nèi)存需要很大(如:fsimage 20G則至少需要40G JVM內(nèi)存)辜荠。這也是沒有用java解析的原因,如果有你有更好的辦法麻煩跟我聯(lián)系抓狭,謝謝伯病。

排查過程

  • 首先看下項(xiàng)目JVM參數(shù)。我使用的是G1回收期(確實(shí)給力)否过,然后有記錄相關(guān)的GC日志午笛,這個(gè)可以幫助我覺得到底要設(shè)置多大Xmx和Xms,然后在程序OOM的時(shí)候會(huì)自動(dòng)給我dump下堆內(nèi)存(雖然dump過程中對(duì)程序有影響苗桂,但是好像沒其他更好辦法了R┗恰!)
${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>執(zhí)行了一次強(qiáng)制的full GC癌佩,之后通過arthas的heapdump 命令dump下來,也可以用jmap的dump命令便锨。

  • 關(guān)于OOM分析工具围辙,visualvm和eclipse memory analyze我都有用,總體上覺得MemoryAnalyze會(huì)好用一些放案。

  • 如下圖所示是程序中加載類的清單姚建,可以看到hdfs的configuration占據(jù)了絕大多數(shù)


    image.png
  • 同時(shí)看到重點(diǎn)包加載也都是跟hadoop的filesystem相關(guān),因此可以判斷這個(gè)可能是主要的泄漏點(diǎn)卿叽。

    image.png

  • 其它的還可以看到很多kerberos鑒權(quán)類也沒有釋放


    image.png

代碼排查和修改

  • 在代碼的工具類中有個(gè)獲取filesystem的方法桥胞。代碼通過cluster對(duì)象來攜帶集群的詳情,然后構(gòu)建FileSystem的過程(很多線程都會(huì)調(diào)用此方法)考婴,每個(gè)進(jìn)程進(jìn)來都先初始化一個(gè)Configuration對(duì)象贩虾,然后進(jìn)行kerberos鑒權(quán)!相關(guān)代碼如下:
 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;
    }
  • 上面代碼有兩個(gè)致命缺點(diǎn)(1)Configuration其實(shí)是可以復(fù)用的而不需要每次重新new沥阱,(2)等于登陸一次kerberos鑒權(quán)即可而不需要每個(gè)都用UserGroupInformation去鑒權(quán)缎罢。
  • 基于上面的致命缺點(diǎn)進(jìn)行修改后的代碼如下:
    private Map<String,Configuration> confMap = new HashMap<>();

    private Configuration generateFileSystemConf(Cluster cluster) throws IOException {

        UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
        String userName = currentUser.getUserName();
        //防止其它操作更新掉當(dāng)前線程中的kerberos認(rèn)證用戶
        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;
    }
  • 修改后重新發(fā)布程序,運(yùn)行了幾天之后再也沒有OOM的現(xiàn)象考杉。從下圖可見每次minor gc都能正常的回收策精,old_gen也維持在一個(gè)較低的范圍內(nèi)。


    image.png

后記

  • 為什么Configuration和UserGroupInformation無法回收的問題崇棠,我猜測(cè)可能跟FileSystem的設(shè)計(jì)有關(guān)咽袜。
  • Hadoop把對(duì)于文件系統(tǒng)的調(diào)用封裝成了一個(gè)FileSystem類,同時(shí)FileSystem對(duì)于文件系統(tǒng)類的實(shí)例做了緩存枕稀,如果是來自同一個(gè)文件系統(tǒng)询刹,它會(huì)返回同一個(gè)實(shí)例。代碼如下:
  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);
  }
  • 為什么要設(shè)置cache萎坷?我猜是因?yàn)槊總€(gè)FileSystem的實(shí)例都會(huì)建立一個(gè)到HDFS Namenode的連接凹联,在大量并發(fā)的讀時(shí)緩存確實(shí)能減低namenode的壓力。那么可能產(chǎn)生什么情況哆档?你從FileSystem里面拿到的文件系統(tǒng)的實(shí)例可能別人也拿到了蔽挠,而且可能正在用,所以這里也是不推薦你close掉fs的原因
  • 由于FileSystem是緩存在hadoop中的瓜浸,則對(duì)Configuration和UserGroupInformation的引用關(guān)系是存在的澳淑,也即GC ROOT是存在的,因此當(dāng)前不會(huì)被回收斟叼。慢慢堆積也就OOM了偶惠。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市朗涩,隨后出現(xiàn)的幾起案子忽孽,更是在濱河造成了極大的恐慌,老刑警劉巖谢床,帶你破解...
    沈念sama閱讀 222,946評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兄一,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡识腿,警方通過查閱死者的電腦和手機(jī)出革,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渡讼,“玉大人骂束,你說我怎么就攤上這事耳璧。” “怎么了展箱?”我有些...
    開封第一講書人閱讀 169,716評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵旨枯,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我混驰,道長(zhǎng)攀隔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,222評(píng)論 1 300
  • 正文 為了忘掉前任栖榨,我火速辦了婚禮昆汹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘婴栽。我一直安慰自己满粗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,223評(píng)論 6 398
  • 文/花漫 我一把揭開白布居夹。 她就那樣靜靜地躺著败潦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪准脂。 梳的紋絲不亂的頭發(fā)上劫扒,一...
    開封第一講書人閱讀 52,807評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音狸膏,去河邊找鬼沟饥。 笑死,一個(gè)胖子當(dāng)著我的面吹牛湾戳,可吹牛的內(nèi)容都是我干的贤旷。 我是一名探鬼主播,決...
    沈念sama閱讀 41,235評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼砾脑,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼幼驶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起韧衣,我...
    開封第一講書人閱讀 40,189評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤盅藻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后畅铭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氏淑,經(jīng)...
    沈念sama閱讀 46,712評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,775評(píng)論 3 343
  • 正文 我和宋清朗相戀三年硕噩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了假残。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,926評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炉擅,死狀恐怖辉懒,靈堂內(nèi)的尸體忽然破棺而出阳惹,到底是詐尸還是另有隱情,我是刑警寧澤眶俩,帶...
    沈念sama閱讀 36,580評(píng)論 5 351
  • 正文 年R本政府宣布穆端,位于F島的核電站,受9級(jí)特大地震影響仿便,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜攒巍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,259評(píng)論 3 336
  • 文/蒙蒙 一嗽仪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柒莉,春花似錦闻坚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跨蟹,卻和暖如春雳殊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窗轩。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評(píng)論 1 274
  • 我被黑心中介騙來泰國打工夯秃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人痢艺。 一個(gè)月前我還...
    沈念sama閱讀 49,368評(píng)論 3 379
  • 正文 我出身青樓仓洼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親堤舒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子色建,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,930評(píng)論 2 361