通过此篇文章,我们可以了解到哪些?
-
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]
-
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 Arguments | Value for TaskManager |
|---|---|
| -Xmx and -Xms 堆内存,设置相同值 | Framework + Task Heap Memory |
| -XX:MaxDirectMemorySize | Framework + Task Off-heap + Network Memory |
| -XX:MaxMetaspaceSize | JVM 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 的类