上一篇讲了 Flink 内存模型的整体架构(TaskManager 总内存 → JVM 进程内存 → 堆内/堆外 → 各内存区域),这篇聚焦 Flink 内存分配的源码深度剖析。TaskManager 启动时如何计算各内存区域大小?MemoryManager 如何管理堆外内存?NetworkBufferPool 和 LocalBufferPool 如何分配网络缓冲?Task 启动时如何分配内存?运行时如何动态申请和释放?Task 完成后如何回收?这篇从源码级别跟踪 Flink 内存分配的完整链路。
一、内存分配整体架构
下面这张图是内存分配整体架构,包括启动时内存计算、内存管理器组件和运行时内存分配三大部分。

Flink 内存分配分为三个阶段:
启动时内存计算:TaskManager 启动时,TaskManagerOptions 和 FlinkMemoryUtils 根据配置计算各内存区域大小。首先从 process.size(总进程内存)中减去 JVM Metaspace 和 JVM Overhead,得到 Flink 可用内存(flink.size)。然后计算 Network Memory(按 network.fraction 比例)、Managed Memory(按 managed.fraction 比例)、Framework Heap(固定大小),剩余部分为 Task Heap。最后设置 JVM 参数 -Xmx/-Xms(堆内存)和 MaxDirectMemorySize(堆外内存)。
内存管理器组件:TaskManager 创建多个内存管理器组件。MemoryManager 负责管理堆外内存(Managed Memory)的分配和释放,创建时预分配所有内存页。NetworkEnvironment 负责管理网络内存(Network Memory)和网络连接,创建全局 NetworkBufferPool 预分配所有网络缓冲。TaskExecutorMemoryManager 负责管理 Task 级别的内存。NetworkBufferPool 是全局网络缓冲池,LocalBufferPool 是每个 ResultPartition/InputGate 的本地缓冲池,MemoryPool 是堆外内存页池。
运行时内存分配:Task 运行过程中,内存动态申请和释放。Task 启动时分配 Task Heap(JVM 直接管理)和网络缓冲(从 LocalBufferPool 申请)。算子运行时通过 MemoryManager.allocatePages() 申请堆外内存(批处理排序/哈希表使用)。网络数据传输时通过 LocalBufferPool.requestBuffer() 申请网络缓冲。RocksDB 状态后端从 Managed Memory 分配内存。Task 完成后回收所有分配的内存和缓冲。
二、内存分配完整流程
下面这张图是内存分配完整流程,包括6步分配流程、方法调用链、核心类和常见问题。

内存分配分为6步:
第1步:启动时内存计算。TaskManager 启动时,FlinkMemoryUtils.generateJvmParameters() 方法根据配置计算各内存区域大小。首先解析 process.size(总进程内存),减去 JVM Metaspace(默认 128MB)和 JVM Overhead(默认 10%,范围 192MB-1024MB),得到 Flink 可用内存。然后计算 Network Memory(Flink Memory × network.fraction,默认 10%,范围 64MB-1024MB),Managed Memory = (Flink Memory - Network) × managed.fraction(默认 40%),Framework Heap(默认 128MB),剩余为 Task Heap。最后生成 JVM 参数:-Xmx/-Xms 设置堆内存大小,-XX:MaxDirectMemorySize 设置堆外内存大小(Managed + Network)。
第2步:创建 MemoryManager。内存计算完成后,MemoryBuilder.build() 方法创建 MemoryManager 实例。MemoryManager 负责管理堆外内存(Managed Memory)的分配和释放。创建时根据 Managed Memory 大小和页大小(pageSize,默认 32KB)计算内存页数,创建 MemoryPool 内存池。MemoryPool.allocateAllPages() 方法预先分配所有内存页(使用 ByteBuffer.allocateDirect()),运行时通过 allocatePages() 和 releasePages() 方法动态分配和回收。MemoryManager 还支持内存预留(reserveMemory)和释放(releaseMemory),确保 Task 之间的内存隔离。
第3步:初始化 NetworkEnvironment。创建 MemoryManager 后,NetworkEnvironment.create() 方法初始化网络环境。NetworkEnvironment 负责管理网络内存(Network Memory)和网络连接。创建时根据 Network Memory 大小和缓冲大小(bufferSize,默认 32KB)计算网络缓冲数,创建全局 NetworkBufferPool。NetworkBufferPool 构造方法中预先分配所有网络缓冲(使用 ByteBuffer.allocateDirect()),运行时通过 requestBuffer() 和 recycle() 方法动态分配和回收。每个 ResultPartition 和 InputGate 会创建自己的 LocalBufferPool,从全局 NetworkBufferPool 申请独占缓冲(buffers-per-channel,默认 2)和浮动缓冲(floating-buffers-per-gate,默认 8)。
第4步:Task 启动分配内存。JobMaster 提交 Task 后,TaskExecutor.submitTask() 方法创建 Task 实例并启动。Task 启动时,TaskExecutorMemoryManager 为 Task 分配内存资源。首先为 Task 的 ResultPartition 和 InputGate 创建 LocalBufferPool,从全局 NetworkBufferPool 申请独占缓冲和浮动缓冲。然后为算子创建状态后端(RocksDBStateBackend 从 Managed Memory 分配内存,初始化 Block Cache、Write Buffer、Index Filter)。Task Heap 由 JVM 直接管理,不需要显式分配,JVM 在运行时自动为对象分配堆内存。
第5步:运行时动态申请/释放。Task 运行过程中,内存动态申请和释放。算子处理数据时,通过 MemoryManager.allocatePages() 申请堆外内存页(批处理排序/哈希表使用),使用完成后 releasePages() 释放。网络数据传输时,ResultPartition 通过 LocalBufferPool.requestBuffer() 申请网络缓冲,写入数据后通过 Netty 发送到下游,发送完成后 Buffer.recycle() 回收。InputGate 接收数据后读取 Buffer,处理完成后 recycle()。RocksDB 状态后端通过 Block Cache 和 Write Buffer 动态管理 Managed Memory,数据写入 MemTable,满后 flush 到磁盘,Compaction 时合并 SST 文件。反压时,LocalBufferPool 缓冲耗尽,requestBuffer() 阻塞等待,反压向上游传播。
第6步:Task 完成回收内存。Task 执行完成(成功或失败)后,Task.cleanup() 方法回收所有分配的内存资源。首先关闭 ResultPartition 和 InputGate,销毁 LocalBufferPool,将所有网络缓冲归还全局 NetworkBufferPool。然后关闭状态后端(RocksDB 关闭,释放 Block Cache、Write Buffer 等 Managed Memory),释放所有堆外内存页。最后 TaskExecutorMemoryManager 清理 Task 的所有内存资源。Task 完成后,其占用的内存和缓冲全部回收,可供其他 Task 使用。内存泄漏会导致可用内存持续减少,最终 OOM。
三、核心类源码剖析
3.1 MemoryManager 内存管理器
java
public class MemoryManager {
private final MemoryPool memoryPool;
private final long totalMemorySize;
private final int pageSize;
private final Map<Object, Long> reservedMemory;
public static MemoryManager create(
MemorySize memorySize,
int pageSize,
MemoryType memoryType) {
return new MemoryBuilder()
.setMemorySize(memorySize)
.setPageSize(pageSize)
.setMemoryType(memoryType)
.build();
}
private MemoryManager(MemoryPool memoryPool, int pageSize) {
this.memoryPool = memoryPool;
this.pageSize = pageSize;
this.totalMemorySize = (long) memoryPool.getNumberOfPages() * pageSize;
this.reservedMemory = new HashMap<>();
}
// 申请内存页
public List<MemorySegment> allocatePages(
Object owner,
int numberOfPages) {
List<MemorySegment> pages = new ArrayList<>(numberOfPages);
try {
for (int i = 0; i < numberOfPages; i++) {
MemorySegment segment = memoryPool.allocatePage();
pages.add(segment);
}
} catch (MemoryPoolException e) {
// 申请失败,释放已申请的页
releasePages(pages);
throw e;
}
return pages;
}
// 释放内存页
public void releasePages(List<MemorySegment> pages) {
for (MemorySegment page : pages) {
memoryPool.release(page);
}
}
// 预留内存
public void reserveMemory(Object owner, long size) {
long available = memoryPool.getAvailableMemorySize();
if (size > available) {
throw new MemoryReservationException(
"Cannot reserve " + size + " bytes, only " + available + " available");
}
reservedMemory.merge(owner, size, Long::sum);
memoryPool.reserve(size);
}
// 释放预留内存
public void releaseMemory(Object owner, long size) {
Long reserved = reservedMemory.remove(owner);
if (reserved != null) {
memoryPool.release(size);
}
}
public long getTotalMemorySize() { return totalMemorySize; }
public int getPageSize() { return pageSize; }
public long getAvailableMemorySize() {
return memoryPool.getAvailableMemorySize();
}
}
MemoryManager 是 Flink 的堆外内存管理器,负责 Managed Memory 的分配和释放。create() 静态方法通过 MemoryBuilder 构建 MemoryManager 实例。构造方法中创建 MemoryPool,预分配所有内存页。allocatePages() 方法从内存池申请指定数量的内存页,申请失败时释放已申请的页并抛出异常。releasePages() 方法释放内存页回内存池。reserveMemory() 和 releaseMemory() 方法用于内存预留和释放,确保 Task 之间的内存隔离,避免一个 Task 占用所有内存。
3.2 NetworkBufferPool 全局网络缓冲池
java
public class NetworkBufferPool {
private final int numberOfSegmentsToAllocate;
private final int segmentSize;
private final List<MemorySegment> availableMemorySegments;
private final Set<LocalBufferPool> allBufferPools;
private int totalNumberOfRequestedBuffers;
public NetworkBufferPool(int numberOfSegmentsToAllocate, int segmentSize) {
this.numberOfSegmentsToAllocate = numberOfSegmentsToAllocate;
this.segmentSize = segmentSize;
this.availableMemorySegments = new ArrayList<>(numberOfSegmentsToAllocate);
this.allBufferPools = new HashSet<>();
// 预分配所有网络缓冲
for (int i = 0; i < numberOfSegmentsToAllocate; i++) {
availableMemorySegments.add(
MemorySegmentFactory.allocateUnpooledSegment(segmentSize));
}
}
// 申请缓冲
public MemorySegment requestMemorySegment() throws InterruptedException {
synchronized (availableMemorySegments) {
// 缓冲不足时阻塞等待
while (availableMemorySegments.isEmpty()) {
availableMemorySegments.wait();
}
return availableMemorySegments.remove(
availableMemorySegments.size() - 1);
}
}
// 回收缓冲
public void recycle(MemorySegment segment) {
synchronized (availableMemorySegments) {
availableMemorySegments.add(segment);
availableMemorySegments.notifyAll();
}
}
// 创建 LocalBufferPool
public LocalBufferPool createBufferPool(
int numExclusiveBuffers,
int numFloatingBuffers) {
LocalBufferPool bufferPool = new LocalBufferPool(
this, numExclusiveBuffers, numFloatingBuffers);
synchronized (allBufferPools) {
allBufferPools.add(bufferPool);
}
return bufferPool;
}
// 销毁 LocalBufferPool
public void destroyBufferPool(LocalBufferPool bufferPool) {
synchronized (allBufferPools) {
allBufferPools.remove(bufferPool);
}
bufferPool.lazyDestroy();
}
public int getTotalNumberOfMemorySegments() {
return numberOfSegmentsToAllocate;
}
public int getNumberOfAvailableMemorySegments() {
synchronized (availableMemorySegments) {
return availableMemorySegments.size();
}
}
public int getSegmentSize() { return segmentSize; }
}
NetworkBufferPool 是全局网络缓冲池,TaskManager 级别唯一。构造方法中根据网络缓冲数和缓冲大小预分配所有网络缓冲(使用 ByteBuffer.allocateDirect())。requestMemorySegment() 方法申请缓冲,缓冲不足时阻塞等待(实现反压),缓冲可用时从可用列表中移除并返回。recycle() 方法回收缓冲,添加回可用列表并通知等待的线程。createBufferPool() 方法为每个 ResultPartition/InputGate 创建 LocalBufferPool,指定独占缓冲数和浮动缓冲数。destroyBufferPool() 方法销毁 LocalBufferPool,回收所有缓冲。
3.3 LocalBufferPool 本地缓冲池
java
public class LocalBufferPool implements BufferPool {
private final NetworkBufferPool networkBufferPool;
private final int numberOfExclusiveBuffers;
private final int numberOfFloatingBuffers;
private final List<MemorySegment> availableBuffers;
private int numberOfRequestedBuffers;
private boolean isDestroyed;
public LocalBufferPool(
NetworkBufferPool networkBufferPool,
int numberOfExclusiveBuffers,
int numberOfFloatingBuffers) {
this.networkBufferPool = networkBufferPool;
this.numberOfExclusiveBuffers = numberOfExclusiveBuffers;
this.numberOfFloatingBuffers = numberOfFloatingBuffers;
this.availableBuffers = new ArrayList<>();
this.numberOfRequestedBuffers = 0;
// 从全局池申请独占缓冲
for (int i = 0; i < numberOfExclusiveBuffers; i++) {
try {
availableBuffers.add(networkBufferPool.requestMemorySegment());
numberOfRequestedBuffers++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Failed to allocate exclusive buffers", e);
}
}
}
// 申请缓冲
@Override
public MemorySegment requestBuffer() throws InterruptedException {
synchronized (availableBuffers) {
// 先从本地可用缓冲申请
while (availableBuffers.isEmpty()) {
if (isDestroyed) {
throw new IllegalStateException("Buffer pool is destroyed");
}
// 尝试从全局池申请浮动缓冲
if (numberOfRequestedBuffers <
numberOfExclusiveBuffers + numberOfFloatingBuffers) {
try {
MemorySegment segment =
networkBufferPool.requestMemorySegment();
availableBuffers.add(segment);
numberOfRequestedBuffers++;
} catch (InterruptedException e) {
// 全局池缓冲不足,阻塞等待
availableBuffers.wait(1000);
}
} else {
// 已达最大缓冲数,阻塞等待
availableBuffers.wait();
}
}
return availableBuffers.remove(availableBuffers.size() - 1);
}
}
// 回收缓冲
@Override
public void recycle(MemorySegment segment) {
synchronized (availableBuffers) {
availableBuffers.add(segment);
availableBuffers.notifyAll();
}
}
// 销毁缓冲池
@Override
public void lazyDestroy() {
synchronized (availableBuffers) {
isDestroyed = true;
// 归还所有缓冲到全局池
for (MemorySegment segment : availableBuffers) {
networkBufferPool.recycle(segment);
}
availableBuffers.clear();
numberOfRequestedBuffers = 0;
availableBuffers.notifyAll();
}
}
@Override
public int getNumberOfAvailableBuffers() {
synchronized (availableBuffers) {
return availableBuffers.size();
}
}
@Override
public int getMaxNumberOfBuffers() {
return numberOfExclusiveBuffers + numberOfFloatingBuffers;
}
}
LocalBufferPool 是每个 ResultPartition/InputGate 的本地缓冲池。构造方法中从全局 NetworkBufferPool 申请独占缓冲(Exclusive Buffers,默认每通道 2 个),保证最低传输能力。requestBuffer() 方法申请缓冲,先从本地可用缓冲申请,不足时尝试从全局池申请浮动缓冲(Floating Buffers,默认每 Gate 8 个),浮动缓冲也不足时阻塞等待(实现反压)。recycle() 方法回收缓冲到本地池。lazyDestroy() 方法销毁缓冲池,将所有缓冲归还全局池。独占缓冲保证最低传输能力,浮动缓冲在通道间动态分配,提高缓冲利用率。
3.4 FlinkMemoryUtils 内存工具类
java
public class FlinkMemoryUtils {
// 生成 JVM 参数
public static String generateJvmParameters(FlinkMemory flinkMemory) {
StringBuilder jvmParams = new StringBuilder();
// 堆内存参数
MemorySize heapSize = flinkMemory.getHeapMemorySize();
jvmParams.append("-Xms").append(heapSize.getMebiBytes()).append("m ");
jvmParams.append("-Xmx").append(heapSize.getMebiBytes()).append("m ");
// 堆外内存参数(Managed + Network)
MemorySize offHeapSize = flinkMemory.getOffHeapMemorySize();
jvmParams.append("-XX:MaxDirectMemorySize=")
.append(offHeapSize.getMebiBytes()).append("m ");
// Metaspace 参数
MemorySize metaspaceSize = flinkMemory.getJvmMetaspaceSize();
jvmParams.append("-XX:MaxMetaspaceSize=")
.append(metaspaceSize.getMebiBytes()).append("m ");
return jvmParams.toString();
}
// 计算 Flink 内存
public static FlinkMemory generateFlinkMemory(
TaskManagerOptions options) {
// 1. 获取总进程内存
MemorySize processSize = options.getProcessMemorySize();
// 2. 计算 JVM Overhead
MemorySize jvmOverhead = calculateJvmOverhead(processSize, options);
// 3. 计算 JVM Metaspace
MemorySize jvmMetaspace = options.getJvmMetaspaceSize();
// 4. Flink 可用内存 = 总内存 - Metaspace - Overhead
MemorySize flinkSize = processSize.subtract(jvmMetaspace).subtract(jvmOverhead);
// 5. 计算 Network Memory
MemorySize networkMemory = calculateNetworkMemory(flinkSize, options);
// 6. 计算 Managed Memory
MemorySize managedMemory = calculateManagedMemory(
flinkSize.subtract(networkMemory), options);
// 7. Framework Heap
MemorySize frameworkHeap = options.getFrameworkHeapSize();
// 8. Task Heap = 剩余
MemorySize taskHeap = flinkSize
.subtract(networkMemory)
.subtract(managedMemory)
.subtract(frameworkHeap);
return new FlinkMemory(
taskHeap, frameworkHeap, networkMemory, managedMemory,
jvmMetaspace, jvmOverhead);
}
private static MemorySize calculateJvmOverhead(
MemorySize processSize, TaskManagerOptions options) {
double fraction = options.getJvmOverheadFraction();
MemorySize min = options.getJvmOverheadMin();
MemorySize max = options.getJvmOverheadMax();
MemorySize overhead = processSize.multiply(fraction);
return overhead.clip(min, max); // 限制在 [min, max] 范围
}
private static MemorySize calculateNetworkMemory(
MemorySize flinkSize, TaskManagerOptions options) {
double fraction = options.getNetworkFraction();
MemorySize min = options.getNetworkMin();
MemorySize max = options.getNetworkMax();
MemorySize network = flinkSize.multiply(fraction);
return network.clip(min, max);
}
}
FlinkMemoryUtils 是内存计算工具类,负责根据配置计算各内存区域大小并生成 JVM 参数。generateJvmParameters() 方法根据 FlinkMemory 生成 JVM 启动参数:-Xms/-Xmx 设置堆内存,-XX:MaxDirectMemorySize 设置堆外内存(Managed + Network),-XX:MaxMetaspaceSize 设置元空间。generateFlinkMemory() 方法是核心计算逻辑:从总进程内存中减去 Metaspace 和 Overhead 得到 Flink 可用内存,然后按比例计算 Network Memory 和 Managed Memory,减去 Framework Heap,剩余为 Task Heap。calculateJvmOverhead() 和 calculateNetworkMemory() 方法按比例计算并限制在 min, max 范围内。
四、网络内存与 Managed 内存分配
下面这张图是网络内存与 Managed 内存分配,包括网络缓冲池三层架构、Managed 内存两大用途和缓冲申请释放流程。

网络缓冲池三层架构
Flink 网络内存采用三层缓冲池架构:
- L1:NetworkBufferPool(全局缓冲池):TaskManager 级别全局唯一,创建时预分配所有网络缓冲。管理总缓冲数 = Network Memory / bufferSize(默认 32KB)。所有 LocalBufferPool 从全局池申请缓冲,全局池负责缓冲的创建和销毁。缓冲耗尽时,requestMemorySegment() 阻塞等待,实现反压。
- L2:LocalBufferPool(本地缓冲池):每个 ResultPartition 和 InputGate 各有一个。创建时从全局池申请独占缓冲(Exclusive Buffers,默认每通道 2 个)和浮动缓冲(Floating Buffers,默认每 Gate 8 个)。独占缓冲保证最低传输能力,浮动缓冲在通道间动态分配。LocalBufferPool 负责缓冲的分配和回收,使用完后归还全局池。
- L3:Buffer(缓冲实例):单个网络缓冲,默认大小 32KB。包含数据区和引用计数。ResultPartition 通过 requestBuffer() 申请 Buffer,写入数据后通过 Netty 发送到下游,发送完成后 recycle() 回收。InputGate 接收数据后读取 Buffer,处理完成后 recycle()。Buffer 的引用计数确保多消费者场景下的安全回收。
Managed 内存两大用途
Managed Memory 有两大用途:
RocksDB 状态后端:
- Block Cache(数据块缓存):默认占 Managed Memory 的 50%,缓存热点数据块,减少磁盘 IO。读性能关键,大状态作业建议调大。
- Write Buffer(写缓冲):默认 64MB × 2(每个 Column Family 两个 MemTable),数据先写入 MemTable,满后 flush 到磁盘。写性能关键。
- Index Filter(索引过滤器):默认占 Managed Memory 的 10%,索引和布隆过滤器,加速数据查找,减少磁盘 IO。
- 其他开销:迭代器、快照、Compaction 开销,使用剩余 Managed Memory。Compaction 时内存需求增大,需预留足够空间。
批处理排序/哈希表:
- 排序(Sort):批处理排序算子使用 Managed Memory 存储排序数据。内存不足时溢写磁盘(spill),排序完成后合并。大排序作业需要大 Managed Memory。
- 哈希表(Hash Table):批处理 Join/聚合算子使用 Managed Memory 存储哈希表。内存不足时溢写磁盘,分桶处理。大 Join/聚合作业需要大 Managed Memory。
- 内存申请方式:通过 MemoryManager.allocatePages() 申请内存页,使用完成后 releasePages() 释放。支持预留和动态分配,确保 Task 间内存隔离。
- 溢写机制:Managed Memory 不足时,数据溢写到磁盘(IOManager 管理临时文件),处理完成后删除临时文件。溢写会降低性能,需要合理配置 Managed Memory。
网络缓冲申请释放流程
网络缓冲申请释放流程4步:
- 申请缓冲:ResultPartition.requestBuffer() → LocalBufferPool.requestBuffer() → 本地可用缓冲不足时从全局 NetworkBufferPool 申请
- 写入数据:序列化数据写入 Buffer,Buffer 引用计数 +1
- 网络发送:Netty 发送到下游 InputGate,发送完成后 Buffer 引用计数 -1,引用计数为 0 时 recycle()
- 回收缓冲:Buffer.recycle() → LocalBufferPool.recycle() → 归还本地可用缓冲或全局 NetworkBufferPool
反压机制:下游 InputGate 处理慢时,LocalBufferPool 缓冲耗尽,requestBuffer() 阻塞等待,ResultPartition 无法写入数据,反压向上游传播,直到 Source 算子暂停读取数据。反压是 Flink 流控的核心机制,确保系统稳定运行,不会因下游处理慢导致 OOM。
五、关键配置参数
yaml
# TaskManager 总进程内存
taskmanager.memory.process.size: 4096MB
# Flink 可用内存(不包含 JVM 开销)
taskmanager.memory.flink.size: 3558MB
# 任务堆内存(用户算子和 HashMapStateBackend 状态)
taskmanager.memory.task.heap.size: 1794MB
# 框架堆内存(Flink 框架使用)
taskmanager.memory.framework.heap.size: 128MB
# 管理内存比例(RocksDB 状态、批处理排序、哈希表)
taskmanager.memory.managed.fraction: 0.4
taskmanager.memory.managed.size: 1281MB
# 网络内存比例(数据传输缓冲、Shuffle)
taskmanager.memory.network.fraction: 0.1
taskmanager.memory.network.min: 64MB
taskmanager.memory.network.max: 1024MB
# JVM 元空间大小
taskmanager.memory.jvm-metaspace.size: 128MB
# JVM 开销比例
taskmanager.memory.jvm-overhead.fraction: 0.1
taskmanager.memory.jvm-overhead.min: 192MB
taskmanager.memory.jvm-overhead.max: 1024MB
# 网络缓冲配置
taskmanager.network.memory.buffers-per-channel: 2
taskmanager.network.memory.floating-buffers-per-gate: 8
taskmanager.network.memory.buffer-size: 32KB
# 内存页大小
taskmanager.memory.segment-size: 32KB
# RocksDB 内存管理
state.backend: rocksdb
state.backend.rocksdb.memory.managed: true
state.backend.rocksdb.memory.write-buffer-ratio: 0.5
state.backend.rocksdb.memory.high-prio-pool-ratio: 0.1
# 每个 TaskManager 的 Slot 数
taskmanager.numberOfTaskSlots: 1
六、常见问题与最佳实践
常见问题
-
网络缓冲不足
- 原因:并行度高、buffers-per-channel 配置小、Network Memory 不足、反压严重。
- 解决:增大 Network Memory(network.fraction=0.15-0.2)、调大 buffers-per-channel 和 floating-buffers-per-gate、优化下游算子处理速度。
-
Managed 内存不足
- 原因:RocksDB 状态大、批处理排序/哈希表内存需求大、managed.fraction 配置小、溢写磁盘频繁。
- 解决:增大 Managed Memory(managed.fraction=0.5-0.6)、优化状态大小(配置 TTL)、减少批处理数据量。
-
内存泄漏
- 原因:Task 完成后内存未回收、缓冲未归还全局池、状态后端未关闭、用户代码资源未释放。
- 解决:检查 close() 方法确保资源释放、监控内存使用趋势、升级 Flink 版本(修复已知泄漏 bug)。
-
直接内存 OOM
- 原因:MaxDirectMemorySize 配置过小、堆外内存使用超限、RocksDB 未纳入 managed 内存、Netty 直接内存泄漏。
- 解决:增大 JVM Overhead、开启 RocksDB managed 内存(state.backend.rocksdb.memory.managed=true)、检查直接内存使用。
-
启动时内存计算错误
- 原因:配置矛盾(同时设置 process.size 和 flink.size)、各区域大小之和超过总内存、min/max 范围配置错误。
- 解决:只设置 process.size 让 Flink 自动计算、检查各区域配置、确保 min ≤ 计算值 ≤ max。
-
Task 启动失败(缓冲分配失败)
- 原因:全局 NetworkBufferPool 缓冲不足、独占缓冲需求超过总缓冲数、Task 并行度过高。
- 解决:增大 Network Memory、减少 buffers-per-channel、降低并行度、增加 TaskManager 数量。
最佳实践
-
只设置 process.size,让 Flink 自动计算:生产环境推荐只设置 taskmanager.memory.process.size,让 Flink 根据比例自动计算各区域大小,避免配置矛盾。需要精细调优时再单独设置各区域大小。
-
高并行度作业调大网络内存:高并行度作业通道数多,网络缓冲需求大,建议 network.fraction=0.15-0.2,同时调大 floating-buffers-per-gate(如 16-32),减少反压。
-
大状态作业调大 Managed 内存:RocksDB 大状态作业需要大 Managed Memory,建议 managed.fraction=0.5-0.6,同时开启 state.backend.rocksdb.memory.managed=true,由 Flink 统一管理 RocksDB 内存,避免内存超限 OOM。
-
批处理作业调大 Managed 内存:批处理排序/Join/聚合使用 Managed Memory,建议 managed.fraction=0.5,减少溢写磁盘,提高性能。Network Memory 也可适当调大(network.fraction=0.15),加速 Shuffle。
-
监控内存池使用情况:通过 Flink Web UI 和 Metrics 监控 MemoryManager(堆外内存使用)、NetworkBufferPool(全局缓冲使用)、LocalBufferPool(本地缓冲使用)的指标,及时发现内存不足和泄漏。关注 Metrics:Status.Flink.Memory.Managed.Used、Status.Flink.Memory.Network.Used。
-
合理设置缓冲大小:大流量作业可调大 buffer-size 到 64KB,减少缓冲申请次数和网络开销;小流量作业保持 32KB,减少内存占用。缓冲大小需要与网络 MTU 和延迟匹配。
-
确保资源在 close() 中释放:用户代码中创建的外部资源(连接、文件、线程池、缓冲)必须在 RichFunction.close() 中释放,防止内存泄漏。使用 try-with-resources 确保资源关闭。Task 完成后 Flink 会回收框架管理的内存,但用户代码管理的资源需要手动释放。
七、总结
Flink 内存分配是一个6步流程:启动时内存计算 → 创建 MemoryManager → 初始化 NetworkEnvironment → Task 启动分配内存 → 运行时动态申请/释放 → Task 完成回收内存。核心类包括 MemoryManager(堆外内存管理器,allocatePages/releasePages)、NetworkBufferPool(全局网络缓冲池,预分配所有缓冲,requestMemorySegment/recycle)、LocalBufferPool(本地缓冲池,独占缓冲+浮动缓冲,requestBuffer/recycle)、FlinkMemoryUtils(内存计算工具类,generateFlinkMemory/generateJvmParameters)、TaskExecutorMemoryManager(TaskExecutor 内存管理器)、MemoryPool(内存页池)。
网络内存采用三层缓冲池架构:NetworkBufferPool(全局池)→ LocalBufferPool(本地池,每 ResultPartition/InputGate 一个)→ Buffer(缓冲实例,默认 32KB)。缓冲耗尽时 requestBuffer() 阻塞等待,实现反压。Managed Memory 有两大用途:RocksDB 状态后端(Block Cache + Write Buffer + Index Filter + 其他开销)和批处理排序/哈希表(内存不足时溢写磁盘)。RocksDB 必须开启 managed 内存,由 Flink 统一管理,避免内存超限 OOM。
内存计算遵循固定公式:Flink Memory = Process Size - Metaspace - Overhead;Network = Flink Memory × network.fraction;Managed = (Flink Memory - Network) × managed.fraction;Task Heap = Flink Memory - Framework Heap - Network - Managed。生产环境推荐只设置 process.size,让 Flink 自动计算各区域大小。
只设置 process.size 让 Flink 自动计算、高并行度作业调大网络内存、大状态作业调大 Managed 内存、批处理作业调大 Managed 内存、监控内存池使用、合理设置缓冲大小、确保资源在 close() 中释放,这7条最佳实践可以显著提高 Flink 作业的内存稳定性,减少 OOM 和内存泄漏问题,为生产环境的稳定运行提供保障。