1 Flink背压原理
任务A写, 任务B读, 2者都是先申请 local buffer pool, 满了之后再向network buffer pool申请。
消费下游: local buffer pool和network buffer pool满了之后,发送消息给上游(ResultSubparittion) ,上有不在发送消息,下游的 input channel将不会接受到新的数据
发送上游:ResultSubparittion也会将local buffer pool和network buffer pool填满,消息写将不再产生消息。同样的他的上游将阻塞。直到source,消费数据变慢。
具体逻辑可以参看http://www.54tianzhisheng.cn/2019/08/26/flink-back-pressure/
2 Network buffer pool
从背压原理,我们可以看到2个重要的缓存localbuffer和network buffer。
其中network buffer是任务管理器下所有子任务共享。 而local buffer为单独的子任务所有。实际上local buffer就是network buffer,只是逻辑上大小分配受到相关限制。
参数1:taskmanager.memory.segment-size(默认为32K)
Network Buffer Pool主要用于数据的网络传输。在 TaskManager 启动的时候就会分配。默认数量是 2048 个
Flink 在数据传输时,会把数据序列化成二进制然后写到 Buffer 中,当 Buffer 满了,需要 Flush(默认为32KiB,通过设置)。
默认值基本够用,无需调优。
参数2:env.setBufferTimeout(timeoutMillis)
可能1分钟都没有 32 KB的数据,就会导致1分钟内的数据都积攒在 Buffer 中不会发送到下游 Task 去处理,从而导致数据出现延迟,这并不是我们想看到的。所以 Flink 有一个 Buffer timeout 的策略,意思是当数据量比较少,Buffer 一直没有变满时,后台的 Output flusher 线程会强制地将 Buffer 中的数据 Flush 到下游。Flink 中默认 timeout 时间是 100ms,即:Buffer 中的数据要么变满时 Flush,要么最多等 100ms 也会 Flush 来保证数据不会出现很大的延迟。当然这个可以通过 env.setBufferTimeout(timeoutMillis)
来控制超时时间。
- timeoutMillis > 0 表示最长等待 timeoutMillis 时间,就会flush
- timeoutMillis = 0 表示每条数据都会触发 flush,直接将数据发送到下游,相当于没有Buffer了(避免设置为0,可能导致性能下降)
- timeoutMillis = -1 表示只有等到 buffer满了或 CheckPoint的时候,才会flush。相当于取消了 timeout 策略
一些特殊的消息如果通过 RecordWriter 发送,也会触发立即 Flush 缓存的数据。其中最重要的消息包括 Checkpoint barrier 以及 end-of-partition 事件
Output flusher 不提供任何保证——它只向 Netty 发送通知,而 Netty 线程会按照能力与意愿进行处理。这也意味着如果存在反压,则 Output flusher 是无效的。言外之意,如果反压很严重,下游 Buffer 都满了,当然不能强制一直往下游发数据。
注: Network pool初始化内存段,使用堆外内存初始化:
参看1.9代码NetworkbBufferPool.java 119行, 默认32K*2048=64M availableMemorySegments.add(MemorySegmentFactory.allocateUnpooledOffHeapMemory(segmentSize, null));
3 堆内存管理
Remaining (Free) Heap:这部分的内存是留给用户代码以及 TaskManager 的数据结构使用的。
Memory Manager Pool:
由 MemoryManager 管理的,由众多MemorySegment组成的超大集合。Flink 中的算法(如 sort/shuffle/join)会向这个内存池申请 MemorySegment,将序列化后的数据存于其中,使用完后释放回内存池。默认情况下,池子占了堆内存的 70% 的大小。
注:Memory Manager Pool 主要在Batch模式下使用。在Steaming模式下,该池子不会预分配内存,也不会向该池子请求内存块。也就是说该部分的内存都是可以给用户代码使用的。社区打算在 Streaming 模式下也能将该池子利用起来。
-
减少GC压力。显而易见,因为所有常驻型数据都以二进制的形式存在 Flink 的MemoryManager中,这些MemorySegment一直呆在老年代而不会被GC回收。其他的数据对象基本上是由用户代码生成的短生命周期对象,这部分对象可以被 Minor GC 快速回收。只要用户不去创建大量类似缓存的常驻型对象,那么老年代的大小是不会变的,Major GC也就永远不会发生。从而有效地降低了垃圾回收的压力。另外,这里的内存块还可以是堆外内存,这可以使得 JVM 内存更小,从而加速垃圾回收。
-
避免了OOM。所有的运行时数据结构和算法只能通过内存池申请内存,保证了其使用的内存大小是固定的,不会因为运行时数据结构和算法而发生OOM。在内存吃紧的情况下,算法(sort/join等)会高效地将一大批内存块写到磁盘,之后再读回来。因此,OutOfMemoryErrors可以有效地被避免。
-
节省内存空间。Java 对象在存储上有很多额外的消耗。如果只存储实际数据的二进制内容,就可以避免这部分消耗。
-
高效的二进制操作 & 缓存友好的计算。二进制数据以定义好的格式存储,可以高效地比较与操作。另外,该二进制形式可以把相关的值,以及hash值,键值和指针等相邻地放进内存中。这使得数据结构可以对高速缓存更友好,可以从 L1/L2/L3 缓存获得性能的提升
常用参数配置:https://ci.apache.org/projects/flink/flink-docs-release-1.9/ops/config.html
Key | Default | Description |
---|---|---|
taskmanager.memory.fraction
| 0.7 |
任务管理器内存用于内部数据缓存占用整个堆(堆内或堆外,依赖taskmanager.memory.off-heap参数)的比例,taskmanager.memory.size未配置时有效。 |
taskmanager.memory.off-heap | false | 内存分配方式(JVM heap or off-heap), used for managed memory of the TaskManager. 当需要非常大量内存时,这个参数为true可以提及高内存操作性能。 |
taskmanager.memory.preallocate | false | TaskManager 启动时managed memory预分配内存. 如果设置为false, 会导致JVM 仅当MaxDirectMemorySize 达到限定值才会触发 full GC. 对于流式处理,推荐设置为false,因the core state backends currently do not use the managed memory. 疑问: 流式窗体是否算批处理呢? |
taskmanager.memory.segment-size | "32kb" | Size of memory buffers used by the network stack and the memory manager. |
taskmanager.memory.size | "0" | task manager 堆内或堆外size(依赖参数taskmanager.memory.off-heap) for sorting, hash tables, and caching of intermediate results. 未指定,则以参数为准 taskmanager.memory.fraction. |
4 Memory Manager初始化过程源码:
private static MemoryManager createMemoryManager(
TaskManagerServicesConfiguration taskManagerServicesConfiguration) throws Exception {
// computing the amount of memory to use depends on how much memory is available
// it strictly needs to happen AFTER the network stack has been initialized
// check if a value has been configured
// 参数taskmanager.memory.size对应的值
long configuredMemory = taskManagerServicesConfiguration.getConfiguredMemory();
MemoryType memType = taskManagerServicesConfiguration.getMemoryType();
final long memorySize;
boolean preAllocateMemory = taskManagerServicesConfiguration.isPreAllocateMemory();
// 参数taskmanager.memory.size已经配置
if (configuredMemory > 0) {
//参数 taskmanager.memory.preallocate
if (preAllocateMemory) {
LOG.info("Using {} MB for managed memory." , configuredMemory);
} else {
LOG.info("Limiting managed memory to {} MB, memory will be allocated lazily." , configuredMemory);
}
memorySize = configuredMemory << 20; // megabytes to bytes
} else {// 没有配置taskmanager内存的情况
// similar to #calculateNetworkBufferMemory(TaskManagerServicesConfiguration tmConfig)
// 参数taskmanager.memory.fraction
float memoryFraction = taskManagerServicesConfiguration.getMemoryFraction();
// 在堆内
if (memType == MemoryType.HEAP) {
// taskManagerServicesConfiguration在类 TaskManagerRunner 355行实例化、初始化
// FreeHeapMemoryWithDefrag来自于360行 EnvironmentInformation.getSizeOfFreeHeapMemoryWithDefrag()
// EnvironmentInformation的Line150: getSizeOfFreeHeapMemoryWithDefrag() = getMaxJvmHeapMemory()[设置值或物理内存1/4] - r.totalMemory()[当前虚拟机使用总内存] + r.freeMemory()[当前虚拟机空余内存]
long freeHeapMemoryWithDefrag = taskManagerServicesConfiguration.getFreeHeapMemoryWithDefrag();
// network buffers allocated off-heap -> use memoryFraction of the available heap:
long relativeMemSize = (long) (freeHeapMemoryWithDefrag * memoryFraction);
if (preAllocateMemory) {
LOG.info("Using {} of the currently free heap space for managed heap memory ({} MB)." ,
memoryFraction , relativeMemSize >> 20);
} else {
LOG.info("Limiting managed memory to {} of the currently free heap space ({} MB), " +
"memory will be allocated lazily." , memoryFraction , relativeMemSize >> 20);
}
memorySize = relativeMemSize;
}
// 堆外
else if (memType == MemoryType.OFF_HEAP) {
long maxJvmHeapMemory = taskManagerServicesConfiguration.getMaxJvmHeapMemory();//[设置值或物理内存1/4]
// The maximum heap memory has been adjusted according to the fraction (see
// calculateHeapSizeMB(long totalJavaMemorySizeMB, Configuration config)), i.e.
// maxJvmHeap = jvmTotalNoNet - jvmTotalNoNet * memoryFraction = jvmTotalNoNet * (1 - memoryFraction)// 排除Network内存之后的内存*(1-F)=最大JVM堆内存
// directMemorySize = jvmTotalNoNet * memoryFraction
long directMemorySize = (long) (maxJvmHeapMemory / (1.0 - memoryFraction) * memoryFraction);
if (preAllocateMemory) {
LOG.info("Using {} of the maximum memory size for managed off-heap memory ({} MB)." ,
memoryFraction, directMemorySize >> 20);
} else {
LOG.info("Limiting managed memory to {} of the maximum memory size ({} MB)," +
" memory will be allocated lazily.", memoryFraction, directMemorySize >> 20);
}
memorySize = directMemorySize;
} else {
throw new RuntimeException("No supported memory type detected.");
}
}
// now start the memory manager
final MemoryManager memoryManager;
try {
//初始化 memory manager,根据堆内或对外,初始化内存池,按段存储段
memoryManager = new MemoryManager(
memorySize,
taskManagerServicesConfiguration.getNumberOfSlots(),
taskManagerServicesConfiguration.getPageSize(),
memType,
preAllocateMemory);
} catch (OutOfMemoryError e) {
if (memType == MemoryType.HEAP) {
throw new Exception("OutOfMemory error (" + e.getMessage() +
") while allocating the TaskManager heap memory (" + memorySize + " bytes).", e);
} else if (memType == MemoryType.OFF_HEAP) {
throw new Exception("OutOfMemory error (" + e.getMessage() +
") while allocating the TaskManager off-heap memory (" + memorySize +
" bytes).Try increasing the maximum direct memory (-XX:MaxDirectMemorySize)", e);
} else {
throw e;
}
}
return memoryManager;
}
==》堆内
HybridHeapMemoryPool(int numInitialSegments, int segmentSize) { this.availableMemory = new ArrayDeque<>(numInitialSegments); this.segmentSize = segmentSize; for (int i = 0; i < numInitialSegments; i++) { this.availableMemory.add(new byte[segmentSize]); } }
==》堆外
static final class HybridOffHeapMemoryPool extends MemoryPool { /** The collection of available memory segments. */ private final ArrayDeque<ByteBuffer> availableMemory; private final int segmentSize; HybridOffHeapMemoryPool(int numInitialSegments, int segmentSize) { this.availableMemory = new ArrayDeque<>(numInitialSegments); this.segmentSize = segmentSize; for (int i = 0; i < numInitialSegments; i++) { this.availableMemory.add(ByteBuffer.allocateDirect(segmentSize)); } }