Flink Runtime 内存结构概览

420 阅读7分钟

通过此篇文章,我们可以了解到哪些?

  • Flink TM/JM的内存结构(1.11版本)

  • Flink 是如何申请和管理内存的?

TaskManager内存模型

整体架构简介

Client:将用户代码构建成JobGraph,提交到JobManager

JobManager:调度task,协调checkpoint

  • 内存使用与task的数量相关

TaskManager:执行task,缓存并交换数据流。

  • 内存使用与作业逻辑相关,例如map、agg

如上图示,内存结构由8部分组成。

进程内存 VS Flink内存

  • 进程内存(Process Memory)

    • TaskManager进程的所有内存包括在内。

    • 在容器化场景下(yarn/kuternetes)关注,申请每一个container是有资源限定的,如果内存超用,container会被杀掉,所以申请container时,TaskManager进程运行的所有内存要等于container的内存。

  • Flink 内存(Flink Memory)

    • 除去JVM的开销,剩下的Flink application的内存。

    • 在standalone部署模式下,对内存的总用量没有一个严格的限制,实际上多个作业共享集群内存,不会由于单个进程内存超用造成被kill,JVM开销也较小,所以主要关注Flink使用内存。

Task 内存 VS Framework 内存

区别:

  • 是否计入slot资源

  • Task 内存, 执行用户代码时使用

  • Framework 内存,TaskManager代码本身使用。

为什么将Task 和 Framework 内存分开?

这里在设计时是为后续版本做准备。当前版本每个slot是等价的,之后打算做更细粒度的资源管理,分配每个slot不同大小的内存资源,这样框架本身是需要预留一些资源的。

FLIP-56: Dynamic Slot Allocation [released]

  1. Task 堆外内存

    • 如果在 task 的代码中调用了 Native 的方法,需要用到 off-heap 内存,这些内存会分配到 Task 堆外内存中。

Framework 堆外内存

  • 如果Flink加载的lib包中有调用了Native方法,需要用到这部分堆外内存。

Network Memory

在 Task 与 Task 之间进行数据交换时(shuffle),需要将数据缓存下来,缓存能够使用的内存大小就是这个 Network Memory。

  • Direct内存

  • 同一TaskManager的slot之间没有隔离,总量对即可

  • 根据作业的拓扑来计算需要多少网络内存

计算逻辑:

network memory = buffersize(32KB) * (inputbuffers + outputbuffers)

inputbuffers = remoteChannels * bufferPerChannel + gates * bufferPerGates

outputbuffers = subpartitions + 1(1个并行序列化,其他接收数据)

remoteChannels: 是不在当前tm的上游subtask的数量

gate:当前tm的上游task的数量

subpartitions:不在当前tm的下游的subtask的数量

Managed Memory

  • 在rocksbd作为状态存储时使用,无状态作业/使用heapStatebackend,可设置此部分内存为0(Flink 流式作业)

  • Native 内存,不受JVM的限制

  • 可以控制Managed Memory是否受Flink管理(逻辑上的限制)

    •   RocksDB的内存管理是在自己的C++ library里,我们可以通过设置state.backend.rocksdb.memory.managed来保证RocksDB申请的内存不会超过managed memory,主要是为了防止RocksDB内存超用造成container被yarn/kuternetes杀掉,在standalone情况下可以考虑关掉。

JVM Metaspace & OverHead

JVM ****Metaspace

  • 存储JVM加载的类的元数据

  • 加载的类越多,需要内存空间越大,一般20-50M,默认最大值128M

  • 作业加载了大量第三方库时,需要调大 Metaspace的最大值

JVM Overhead

  • 其他 JVM 开销的 Native 内存,例如栈空间、代码缓存、垃圾回收空间等

本身都是指JVM开销,为什么拆开?

Metaspace 是可以比较明确的限制内存使用,并且 GC

堆内存 & 堆外内存

实际上,tm进程就是一个JVM进程,我们从JVM内存的角度来看。

JVM 堆内/外内存定义? 不同角度去看

  • 经过 JVM 虚拟化

    • heap
  • JVM管理(申请、GC、限制用量)

    • heap、direct、metaspace、stack

堆内存

  • 经过 JVM 虚拟化受JVM管理(申请、GC、限制用量)

堆外内存

  • 不受 JVM 管理的机器内存,可以方便开辟大内存空间

  • IO 效率更高, 网络IO能够节省堆内存到堆外内存,例如 Netty 使用堆外内存池来实现零拷贝技术提高数据数据拷贝效率。

  • 难以监控分析,出现问题很难排查

native & Direct

Flink 内存 VS JVM 内存

用户设置的参数最终都会被设置成 JVM 参数,对应关系如下:

JVM ArgumentsValue for TaskManager
-Xmx and -Xms 堆内存,设置相同值Framework + Task Heap Memory
-XX:MaxDirectMemorySizeFramework + Task Off-heap + Network Memory
-XX:MaxMetaspaceSizeJVM Metaspace

找一个Taskmanager 验证下内存设置和实际的 JVM 内存

ps -ef |grep taskmanager 

/opt/tiger/jdk/jdk1.8/bin/java -Xmx12817793024 -Xms12817793024 -XX:MaxDirectMemorySize=2281701376 -XX:MaxMetaspaceSize=268435456
-Djava.io.tmpdir=./tmp -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=100M -Xloggc:/data08/yarn/userlogs/application_1656321317123_31478/container_e818_1656321317123_31478_01_000003/gc.log -XX:ErrorFile=/data08/yarn/userlogs/application_1656321317123_31478/container_e818_1656321317123_31478_01_000003/hs_err_pid%p.log -XX:ParallelGCThreads=5
-Dlog4j2.AsyncQueueFullPolicy=Discard
-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
-Dlog.file=/data08/yarn/userlogs/application_1656321317123_31478/container_e818_1656321317123_31478_01_000003/taskmanager.log -Dlog.level=INFO -Dlog4j.configuration=file:./log4j.properties -Dlog4j.configurationFile=file:./log4j.properties -Dlog4j2.isThreadContextMapInheritable=true -Dlog.databus.channel=flink_error_log -Dlog.databus.level=FATAL -Dlog.databus.permitsPerSecond=1000 -Dlog.streamlog.level=OFF 
org.apache.flink.yarn.YarnTaskExecutorRunner
-D taskmanager.memory.framework.off-heap .size=134217728b  # 128M  framework -D taskmanager.memory.framework.heap.size=134217728b  # 128M  framework-off
-D taskmanager.memory.network.max=2147483648b  # 2048M  network
-D taskmanager.memory.network.min=2147483648b  # 2048M
-D taskmanager.memory.managed.size=5033164800b  # 4800M  managed
-D taskmanager.memory.task.heap.size=12683575296b  # 12096M  heap
-D taskmanager.memory.task.off-heap .size=0b  # 0M heap-off
-D taskmanager.memory.jvm-metaspace .size=268435456b  # 256M  JVM Metaspace
-D taskmanager.memory.jvm-overhead .max=1073741824b  # 1024M JVM Overhead
-D taskmanager.memory.jvm-overhead .min=1073741824b  # 1024M
-D taskmanager.cpu.cores=5.0
--configDir . -Dweb.port=0 -Djobmanager.rpc.port=38847 -Drest.address=n145-135-130.byted.org
-Dsocket.address=10.145.135.130 -Dblob.server.port=47707 -Djobmanager.rpc.address=n145-135-130.byted.org
-Dweb.tmpdir=/data05/yarn/nmdata/usercache/wangzhuozhuo/appcache/application_1656321317123_31478/container_e818_1656321317123_31478_01_000001/./tmp/flink-web-92c77db3-aa38-4a60-a04f-b141d45d71c9
jinfo 12034

-XX:CICompilerCount=18
-XX:ErrorFile=null
-XX:GCLogFileSize=104857600
-XX :InitialHeapSize= 12817793024  # 12224M
-XX :MaxHeapSize= 12817793024    # 12224M = 12096M(heap) + 128M(framework) 
-XX :MaxDirectMemorySize= 2281701376  # 2176M = 2048M(network) + 128M(heap-off) + 0M(framework-off) 
-XX :MaxMetaspaceSize= 268435456  # 256M
-XX:MaxNewSize=4272422912
-XX:MinHeapDeltaBytes=524288
-XX:NewSize=4272422912
-XX:NumberOfGCLogFiles=5
-XX:OldSize=8545370112
-XX:ParallelGCThreads=5
-XX:+PrintGC
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseFastUnorderedTimeStamps
-XX:+UseGCLogFileRotation
-XX:+UseParallelGC 

内存管理

MemorySegment

Flink 内部并非直接将对象存储在堆上,而是将对象序列化到一个个预先分配的 MemorySegment 中。MemorySegment 是一段固定长度的内存(默认32KB),也是 Flink 中最小的内存分配单元。MemorySegment 提供了高效的读写方法,它的底层可以是堆上的 byte[], 也可以是堆外(off-heap)ByteBuffer。

  • 保证内存安全:由于分配的 MemorySegment 的数量是固定的,因而可以准确地追踪 MemorySegment 的使用情况。

  • 减少了 GC 的压力:因为分配的 MemorySegment 是长生命周期的对象,数据都以二进制形式存放,且 MemorySegment 可以回收重用,所以 MemorySegment 会一直保留在老年代不会被 GC;而由用户代码生成的对象基本都是短生命周期的,Minor GC 可以快速回收这部分对象,尽可能减少 Major GC 的频率。此外,MemorySegment 还可以配置为使用堆外内存,进而避免 GC。

暂时无法在飞书文档外展示此内容

public final class HybridMemorySegment extends MemorySegment {

    // 堆内存
    HybridMemorySegment(byte [] buffer, Object owner) {
       super(buffer, owner);
       this.offHeapBuffer = null;
       this.allowWrap = true;
       this.cleaner = null;
    }    
    // 堆外内存
    HybridMemorySegment(
       @Nonnull ByteBuffer buffer, 
       @Nullable Object owner,
       boolean allowWrap,
       @Nullable Runnable cleaner) {
       super(getByteBufferAddress(buffer), buffer.capacity(), owner);
       this.offHeapBuffer = buffer;
       this.allowWrap = allowWrap;
       this.cleaner = cleaner;
    }
}

// 创建MemorySegment
public final class MemorySegmentFactory {

    
}

MemorySegment的管理

MemorySegmentFactory 来申请内存,创建MemorySegment

public final class     MemorySegmentFactory {

        public static MemorySegment allocateUnpooledOffHeapMemory(int size, Object owner) {
           ByteBuffer memory = allocateDirectMemory(size);
           return new HybridMemorySegment(memory, owner);
        }
        
        @VisibleForTesting
        public static MemorySegment allocateOffHeapUnsafeMemory(int size) {
           return allocateOffHeapUnsafeMemory(size, null, NO_OP);
        }
        
        // 申请堆外内存
        private static ByteBuffer allocateDirectMemory(int size) {
           //noinspection ErrorNotRethrown
           try {
              return ByteBuffer. allocateDirect (size); 
           } catch (OutOfMemoryError outOfMemoryError) {
              // TODO: this error handling can be removed in future,
        // once we find a common way to handle OOM errors in netty threads.
              // Here we enrich it to propagate better OOM message to the receiver
              // if it happens in a netty thread.
              Throwable enrichedOutOfMemoryError = TaskManagerExceptionUtils.tryEnrichTaskManagerError(outOfMemoryError);
              if (ExceptionUtils.isDirectOutOfMemoryError(outOfMemoryError)) {
                 LOG.error("Cannot allocate direct memory segment", enrichedOutOfMemoryError);
              }
        
              ExceptionUtils.rethrow(enrichedOutOfMemoryError);
              return null;
           }
        }
}

用户代码使用的堆内存、networkbuffer、Managed Memory这三部分是主要的内存。

NetworkBufferPool

暂时无法在飞书文档外展示此内容

public class NetworkBufferPool implements BufferPoolFactory, MemorySegmentProvider, AvailabilityProvider {
        public MemorySegment requestMemorySegment() {
           MemorySegment memorySegment = null;
           synchronized (availableMemorySegments) {
              memorySegment = internalRequestMemorySegment();
              if (memorySegment != null) {
                 return memorySegment;
              }
           }
        
           if (lazyAllocate) {
              memorySegment = allocateMemorySegmentLazy(); 
           }
        
           return memorySegment;
        }
        
        private MemorySegment allocateMemorySegmentLazy() {
           if (numberOfAllocatedMemorySegments.get() < totalNumberOfMemorySegments) {
              if (numberOfAllocatedMemorySegments.incrementAndGet() <= totalNumberOfMemorySegments) {
                 try {
                    MemorySegment segment = MemorySegmentFactory. allocateUnpooledOffHeapMemory (memorySegmentSize, null ); 
  LOG.debug("Allocated a segment success with memorySegmentSize: {}.", memorySegmentSize);
                    return segment;
                 } catch (OutOfMemoryError err) {
                    numberOfAllocatedMemorySegments.decrementAndGet();
        
                    long sizeInLong = (long) memorySegmentSize;
                    long configedMb = sizeInLong * totalNumberOfMemorySegments >> 20;
                    long allocatedMb = sizeInLong * numberOfAllocatedMemorySegments.get() >> 20;
                    long missingMb = configedMb - allocatedMb;
                    throw new OutOfMemoryError("Could not allocate enough memory segments for NetworkBufferPool " +
                       "(configed (Mb): " + configedMb +
                       ", allocated (Mb): " + allocatedMb +
                       ", missing (Mb): " + missingMb + "). Cause: " + err.getMessage());
                 }
              } else {
                 numberOfAllocatedMemorySegments.decrementAndGet();
              }
           }
        
           return null;
        }

}

当结果分区(ResultParition)开始写出数据的时候,需要向LocalBufferPool申请Buffer资源。

MemoryManager

MemoryManager 是管理 Managed Memory 的类