External Shuffle Service 引起的NodeManager OOM问题分析

1 现象描述及初步分析

近期公司yarn集群中存在NodeManager因OOM 而挂掉的情况, 且发生OOM前存在大量的Spark Shuffle Services相关信息, 通过分析最近Crash的NodeManager进程的dump信息发现存在大量的Finalizer,占用了大部分分的内存资源,其中dump分析结果如下所示:


Dominator tree

Finalizer对象数量分析

附:NodeManager日志信息
java.lang.OutOfMemoryError: GC overhead limit exceeded

通过dump分析可知,OOM原因为:FileInputStream引发的Finalizer对象堆集所致。

该对象为Shuffle Server在处理Shuffle Client数据请求时所创建。 其中包含如下代码部分(Spark1.6):

 /**
     * Sort-based shuffle data uses an index called "shuffle_ShuffleId_MapId_0.index" into a data file
     * called "shuffle_ShuffleId_MapId_0.data". This logic is from IndexShuffleBlockResolver,
     * and the block id format is from ShuffleDataBlockId and ShuffleIndexBlockId.
     */
    private ManagedBuffer getSortBasedShuffleBlockData(
            ExecutorShuffleInfo executor, int shuffleId, int mapId, int reduceId) {
        File indexFile = getFile(executor.localDirs, executor.subDirsPerLocalDir,
                "shuffle_" + shuffleId + "_" + mapId + "_0.index");

        DataInputStream in = null;
        try {
            in = new DataInputStream(new FileInputStream(indexFile));
            in.skipBytes(reduceId * 8);
            long offset = in.readLong();
            long nextOffset = in.readLong();
            return new FileSegmentManagedBuffer(
                    conf,
                    getFile(executor.localDirs, executor.subDirsPerLocalDir,
                            "shuffle_" + shuffleId + "_" + mapId + "_0.data"),
                    offset,
                    nextOffset - offset);
        } catch (IOException e) {
            throw new RuntimeException("Failed to open file: " + indexFile, e);
        } finally {
            if (in != null) {
                JavaUtils.closeQuietly(in);
            }
        }
    }


其中 创建FileInputStream的作用是为了拿到索引文件中的数据偏移量及文件长度,用于读取数据。(备注:读取shuffle数据时也会创建FileInputStream对象,上述代码供献1/2的对象数量。)并且,该实现中每次数据请求都会创建一个FileInputStream对象用于读取索引文件。

2 存在大量FileInputStream相关Fanalizer的原因

其FileInputStream自定义了finalize()方法,因此JVM会为每一个FileInputStream对象创建一个Finalizer引用对象,用于确保FileInputStream最终处理关闭状态。

    /**
     * Cleans up the connection to the file, and ensures that the
     * <code>close</code> method of this file output stream is
     * called when there are no more references to this stream.
     *
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FileInputStream#close()
     */
    protected void finalize() throws IOException {
        if (fd != null) {
            if (fd == FileDescriptor.out || fd == FileDescriptor.err) {
                flush();
            } else {

                /*
                 * Finalizer should not release the FileDescriptor if another
                 * stream is still using it. If the user directly invokes
                 * close() then the FileDescriptor is also released.
                 */
                runningFinalize.set(Boolean.TRUE);
                try {
                    close();
                } finally {
                    runningFinalize.set(Boolean.FALSE);
                }
            }
        }
    }

所有的Finalizer组成一个双向链表,其共同维护一个ReferenceQueue,进入队列中的对象可以被gc回收。

同时,JVM中存在一个守护线程:FinalizerThread 其优先级为 8,用于从双向链表中清除进入ReferenceQueue中的Finalizer,以便在下次GC时回收这部分Finalizer。

资源充足的情况下,FinalizerThread线程可以被调度执行,从而ReferenceQueue中的Finalizer会很快被清理掉,从而在GC时释放占用内存。

而在External Shuffle Services 场景中 Shuffle Server作为NodeManager进程中的daemon线程执行,并且其创建了大量提供数据服务 的shuffle-server服务线程(数量默认为NodeManager管理的cores * 2, 因此配制最低的机型拥有48个线程), 该线程优先级为5.

经过上述分析,我们可知NodeManager中有

  • 一个消费Finalizer的FinalizerThread线程,优先级为8
  • 48 个用于生产Finalizer的shuffle-server线程,优先级为5
  • 其它大量线程(如Thread-7872425匿名线程等),此处不一一给出
    "Thread-7872425" #8190511 prio=5 os_prio=0 tid=0x00007f1aa8d51000 nid=0xc671 runnable [0x00007f1a83435000]
    java.lang.Thread.State: RUNNABLE
        at java.io.FileInputStream.readBytes(Native Method)
        at java.io.FileInputStream.read(FileInputStream.java:255)
    

在Java中线程优先级的范围是:1-10,且数字越大优先级越高,线程优先级高仅仅表示线程获取的 CPU时间片的几率高。然而,由于shuffle-server线程数量较多,当Shuffle Client端请求频繁(大量reduce任务Fetch数据)时,shuffle-server线程被调度的机率会比Finalizer线程大,这会导致shuffle-server线程生产Finalizer的速率远大于FinalizerTread线程清理的速率,从而导致Finalizer堆集。

3 实验复现方案

其于如前所述的原因分析:Client端请求频繁时,会导致shuffle-server线程生产Finalizer的速率远大于FinalizerTread线程清理的速率,会导致Finalizer堆集。因此,可增加部分节点的shuffle-server线程,使用问题更易复现。

  • 调整实验集群中一个节点的NodeManager管理的Cores的数量
    操作方法:
    yarn.nodemanager.resource.cpu-vcore = 80 <更大的值>
    或 
    spark.shuffle.io.serverThreads=160
    

备注: 该问题会出现在Shuffle fetch密集的场景(即分布式任务并发度高的场景)。

3.1 模拟实验

复现方案中提出以调参的方式增加服务线程数量,从而增加shuffle-server线程被调度的机率。但复现的前提是要造出大量的密集的Fetch请求,然而,目前测试集群规模无法与生产环境相提并论,不易造出上述场景;而且,用户应用负载及数据的获取不易。因此,进行以下实验模拟真实环境的执行情况。

3.1.1 实验

  • 实验一
    50个线程创建FileInputStream

  • 实验二
    100个线程创建FileInputStream

    上述实验,采用Java 8,采用JVM默认配制,并在相同的节点中进行。

3.1.2 结果

  • 实验一
    Full GC时,ParOldGen(老年代)可以正常回收。
  • 实验二
    Full GC时,ParOldGen(老年代)几乎不能回收,从而引发如下异常:
Exception in thread "shuffle-server-5" java.lang.OutOfMemoryError: GC overhead limit exceeded

异常发生时,Java Heap信息:

Heap
 PSYoungGen      total 465920K, used 226578K [0x0000000795580000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 232960K, 97% used [0x0000000795580000,0x00000007a32c4bb0,0x00000007a3900000)
  from space 232960K, 0% used [0x00000007b1c80000,0x00000007b1c80000,0x00000007c0000000)
  to   space 232960K, 0% used [0x00000007a3900000,0x00000007a3900000,0x00000007b1c80000)
 ParOldGen       total 1398272K, used 1397883K [0x0000000740000000, 0x0000000795580000, 0x0000000795580000)
  object space 1398272K, 99% used [0x0000000740000000,0x000000079551ee20,0x0000000795580000)
 Metaspace       used 2718K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 291K, capacity 386K, committed 512K, reserved 1048576K

3.1.3 实验结果分析

上述实验唯一区别为线程数量不同,即FileInputStream的生产线程与Finalizer线程竞争执行资源的激烈程度不同。

实验一 由于线程数量较少,竞争激烈程度低,FinalizerThread线程可以被调度执行,从而可以从Finalizer链表中清除无引用的对象,进而在GC时回收掉Finalizer.

实验二 线程数量较大,竞争激烈程度高,FinalizerThread线程被调度的机会少,从而Finalizer链表(双向链表)中的对象无法被回收,只能在Heap的 from 区 及 to 区进行拷贝,多个回合后进入old区(老年代)。当FinalizerThread持续被阻塞时,就会发生Finalizer堆满old区的情况。由于Finalizer对象在一个双向链表中相互引用,Full GC 依然会无法回收,最终会引发:“java.lang.OutOfMemoryError: GC overhead limit exceeded”。

在真实的NodeManager中,除了存在shuffle-server线程外,还存在大量其它大量线程(有些线程也会产生FileInputStream)。在负载较高时,这些线程都会与FinalizerThread发生竞争,从而降低FinalizerThread执行的机率。

  • 附1:GC overhead limit exceeded发生的原因

This message means that for some reason the garbage collector is taking an excessive amount of time (by default 98% of all CPU time of the process) and recovers very little memory in each run (by default 2% of the heap)

  • 附2:实验过程中jstack快照
"shuffle-server-28" #37 prio=5 os_prio=31 tid=0x00007faea901f000 nid=0x8503 waiting for monitor entry [0x000070000e6e7000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at java.lang.ref.Finalizer.add(Finalizer.java:51)
    "- waiting to lock <0x000000074127c070> (a java.lang.Object)"
    at java.lang.ref.Finalizer.<init>(Finalizer.java:82)
    at java.lang.ref.Finalizer.register(Finalizer.java:87)
    at java.lang.Object.<init>(Object.java:37)
    at java.io.InputStream.<init>(InputStream.java:45)
    at java.io.FileInputStream.<init>(FileInputStream.java:123)
    at java.io.FileInputStream.<init>(FileInputStream.java:93)
    at TestThread.run(Test.java:32)
    
    
"shuffle-server-26" #35 prio=5 os_prio=31 tid=0x00007faea88c9800 nid=0x8103 waiting for monitor entry [0x000070000e4e1000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at java.lang.ref.Finalizer.add(Finalizer.java:51)
    "- waiting to lock <0x000000074127c070> (a java.lang.Object)"
    at java.lang.ref.Finalizer.<init>(Finalizer.java:82)
    at java.lang.ref.Finalizer.register(Finalizer.java:87)
    at java.lang.Object.<init>(Object.java:37)
    at java.io.InputStream.<init>(InputStream.java:45)
    at java.io.FileInputStream.<init>(FileInputStream.java:123)
    at java.io.FileInputStream.<init>(FileInputStream.java:93)
    at TestThread.run(Test.java:32)
    
    ......
    
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007faeaa00d800 nid=0x3103 waiting for monitor entry [0x000070000c37e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at java.lang.ref.Finalizer.remove(Finalizer.java:61)
    "- waiting to lock <0x000000074127c070> (a java.lang.Object)"
    at java.lang.ref.Finalizer.runFinalizer(Finalizer.java:93)
    - locked <0x00000007bacfba70> (a java.lang.ref.Finalizer)
    at java.lang.ref.Finalizer.access$100(Finalizer.java:34)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:210)

4 结论

通过上述分析及实验可知,NodeManager发生OOM的主要原因为:其内部线程生产Finalizer的速率大于FinalizerTread线程清理的速率,从而使Finalizer链表(双向链表)中无法回收的对象,只能在Heap的 from 区 及 to 区进行拷贝,多个回合后进入old区(老年代)。当FinalizerThread持续被阻塞时,就会发生Finalizer堆满old区的情况,由于Finalizer对象在一个双向链表中相互引用,Full GC依然会无法回收,最终会引发:“java.lang.OutOfMemoryError: GC overhead limit exceeded”。

5 拟解决方案

shuffle-server线程存在为获取得Shuffle IndexFile中reduce任务对应数据的偏移量及数据长度而创建FileInputStream的情况,且原有方案中每次获取都重新打开一次文件,即创建一个FileInputStream对象。 因此,可以引入缓存机制减少读取该文件的次数。

  1. 引入缓存机制减少读取该文件的次数。

    一个IndexFile中包含一个APP在该节点中的所有数据索引,因此引入缓存具有
    较大收益。

    Spark-15074中已引入缓存特性,且#SPARK-21501对缓存方案进行了完善,
    因此可merge官方feature 达到缓解问题的目的。
    缺点:缓存只能涵盖读取IndexFile时产生FileInputStream的场景,仅覆盖
    Shuffle Server中1/2的FileInputStream对象。

除此之外,还可以使用以下方式进行调整:

  1. 使用Files NIO替换FileInputStream
    因为该问题主要是FileInputStream中实现了finalize()方法所置。
    缺点:不能减少文件频繁读写的开销, 对Netty等的影响暂无法评估。

  2. 减少shuffle-server线程数量,降低FileInputStream产生速率,通过参数io.serverThreads调整。
    缺点:机型较多,一种配制可能不能满足三种机型, 且不合适的配制可能影响作业的执行效率,目前缺少数据支撑。

综上所述:为将风险降至最低,可以先尝试 1 或1、3结合的方案。最后尝试1、2结合方案(事实证明1、2结合可以有效解决问题)。

文献:http://www.oracle.com/technetwork/java/javamail/finalization-137655.html 中提到Finalizer产生的原因及一些处理办法。

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,562评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,169评论 11 349
  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,946评论 2 31
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,064评论 0 62
  • 想办法像满足情人一样,对待你的工作和生活,这种感觉就会很美妙。 生活上,总会遇到这样那样的问题,比如和父母和爱人的...
    小琴晴阅读 307评论 0 2