Flink 作业跑着跑着突然 OOM,是很多大数据工程师的噩梦。Heap OOM、Metaspace OOM、Direct buffer memory OOM、GC Overhead OOM,各种 OOM 类型让人眼花缭乱。Flink 的内存模型到底是怎么设计的?TaskManager 的总内存是如何分配到各个区域的?堆内和堆外有什么区别?Managed Memory 和 Network Memory 是干什么的?RocksDB 状态存在哪里?这篇从源码级别深入剖析 Flink 内存模型的完整分配体系。
一、Flink 内存模型整体架构
下面这张图是 Flink 内存模型整体架构,采用嵌套包含关系展示 TaskManager 总内存的完整分配体系。
Flink TaskManager 的内存模型是一个分层嵌套结构,从外到内分为三层:
最外层:TaskManager 总内存(process.size)。这是 TaskManager 进程的总内存,包括 JVM 进程内存和 JVM 开销。通过taskmanager.memory.process.size配置,默认 1728MB。这是容器化部署(YARN/K8s)时最重要的配置,决定了容器申请的总内存。
中间层:JVM 进程内存(flink.size)。从总内存中减去 JVM Metaspace 和 JVM Overhead,得到 Flink 可用的内存。通过taskmanager.memory.flink.size配置。JVM 进程内存分为堆内内存和堆外内存两大部分。
最内层:各内存区域。
- 堆内内存(Heap):由 JVM GC 管理,包括 Framework Heap(框架堆,默认 128MB)和 Task Heap(任务堆,剩余堆内存)。
- 堆外内存(Off-Heap / Direct):由 Flink 直接管理,不受 GC 影响,包括 Managed Memory(管理内存,默认 40% 堆外)和 Network Memory(网络内存,默认 10% 总内存)。
- JVM 开销:包括 JVM Metaspace(元空间,默认 128MB)、JVM Overhead(JVM 开销,默认 10% 总内存)、Direct Memory(直接内存)。
理解这个嵌套结构是排查内存问题的基础。Heap OOM 说明 Task Heap 不足,Direct buffer memory OOM 说明堆外内存不足,Metaspace OOM 说明类元数据空间不足。
二、内存区域详解
下面这张图是内存区域详解,包括6大内存区域的用途、默认配置和内存计算逻辑。
2.1 Task Heap(任务堆内存)
Task Heap 是用户任务使用的 JVM 堆内内存,是 Flink 作业最核心的内存区域。
- 类型:JVM 堆内,由 GC 管理
- 默认大小:剩余堆内存(Flink Memory - Framework Heap - Network - Managed)
- 配置参数:
taskmanager.memory.task.heap.size - 用途:用户算子执行、对象创建、HashMapStateBackend 状态存储、用户代码中的对象和集合
Task Heap 是流处理小状态作业的主要内存区域。使用 HashMapStateBackend 时,所有状态都存储在 Task Heap 中,状态大小受堆内存限制。Task Heap 由 JVM GC 管理,频繁创建对象会导致 GC 压力增大,Full GC 会导致作业停顿。
典型问题:Heap OOM(java.lang.OutOfMemoryError: Java heap space)。原因通常是状态过大、用户缓存无界、数据倾斜导致单 key 状态过大。解决方法:增大 Task Heap、使用 RocksDBStateBackend、配置状态 TTL、优化用户代码缓存。
2.2 Framework Heap(框架堆内存)
Framework Heap 是 Flink 框架本身使用的堆内存。
- 类型:JVM 堆内,由 GC 管理
- 默认大小:128MB
- 配置参数:
taskmanager.memory.framework.heap.size - 用途:Flink 框架代码、Akka 消息系统、调度器、Slot 管理、JobManager/TaskManager 框架对象
Framework Heap 一般不需要调整,默认 128MB 足够大多数场景。大集群(数千 TM)或复杂调度场景可适当增加到 256MB。Framework Heap 不足会导致框架层面的 OOM,但这种情况比较少见。
2.3 Managed Memory(管理内存)
Managed Memory 是 Flink 管理的堆外内存,是大状态作业的关键区域。
- 类型:JVM 堆外(Direct Memory),由 Flink 直接管理
- 默认比例:40% 堆外内存(Flink Memory - Network)
- 配置参数:
taskmanager.memory.managed.fraction或taskmanager.memory.managed.size - 用途:RocksDB 状态后端、批处理排序(Sort)、哈希表(Hash Table)、内存中缓存
Managed Memory 是 Flink 1.10 之后引入的统一堆外内存管理机制。使用 RocksDBStateBackend 时,所有状态都存储在 Managed Memory 中,不受 JVM GC 影响,性能稳定,支持大状态(TB 级)。批处理作业的排序和哈希表也使用 Managed Memory。
关键配置:state.backend.rocksdb.memory.managed=true(默认开启),RocksDB 的内存由 Flink 的 Managed Memory 统一管理,避免 RocksDB 内存超限导致 OOM。RocksDB 内部分为 Block Cache(数据块缓存,默认 50% Managed)、Write Buffer(写缓冲,默认 64MB × 2)、Index Filter(索引和布隆过滤器,默认 10% Managed)、其他开销(迭代器、快照、Compaction)。
2.4 Network Memory(网络内存)
Network Memory 是 Flink 网络传输使用的堆外内存。
- 类型:JVM 堆外(Direct Memory),由 Flink 直接管理
- 默认比例:10% 总内存(Flink Memory),范围 [min, max]
- 配置参数:
taskmanager.memory.network.fraction、taskmanager.memory.network.min、taskmanager.memory.network.max - 用途:数据传输缓冲(ResultPartition/InputGate)、Shuffle、网络通信、反压控制
Network Memory 是数据在 TaskManager 之间传输的缓冲。上游算子通过 RecordWriter 将数据写入 ResultPartition,ResultPartition 缓冲数据后通过 Netty 网络发送到下游,下游 InputGate 接收数据后交给 RecordReader 反序列化。Network Memory 不足会导致反压和性能下降,严重时会导致数据传输超时。
关键配置:
taskmanager.network.memory.buffers-per-channel:每个通道独占缓冲数(Exclusive Buffers),默认 2,保证最低传输能力taskmanager.network.memory.floating-buffers-per-gate:每个 InputGate 浮动缓冲数(Floating Buffers),默认 8,在通道间动态分配taskmanager.network.memory.buffer-size:网络缓冲大小,默认 32KB
高并行度、大流量作业需要调大 Network Memory,建议设置network.fraction=0.15-0.2。
2.5 JVM Metaspace(元空间)
JVM Metaspace 是 JVM 的类元数据空间。
- 类型:JVM 堆外(本地内存),由 JVM 直接管理
- 默认大小:128MB
- 配置参数:
taskmanager.memory.jvm-metaspace.size - 用途:JVM 类元数据、方法区、常量池、静态变量、JIT 编译后的代码
Metaspace 是 JDK 8 之后替代 PermGen(永久代)的区域,使用本地内存,不受堆大小限制。Flink 作业中类加载较多(动态代码生成、大量 UDF、Cep 模式编译)时需要调大 Metaspace。
典型问题:java.lang.OutOfMemoryError: Metaspace。原因通常是类加载泄漏(动态类加载未卸载)、UDF 过多、代码生成量大。解决方法:增大 Metaspace、检查类加载泄漏、减少动态类生成。
2.6 JVM Overhead(JVM 开销)
JVM Overhead 是 JVM 的其他开销内存。
- 类型:JVM 堆外(本地内存),由 JVM 直接管理
- 默认比例:10% 总内存,范围 [min, max]
- 配置参数:
taskmanager.memory.jvm-overhead.fraction、taskmanager.memory.jvm-overhead.min、taskmanager.memory.jvm-overhead.max - 用途:线程栈(Thread Stack)、JNI 调用、代码缓存(Code Cache)、直接内存(Direct Memory)、GC 开销、其他本地内存
JVM Overhead 是一个兜底区域,覆盖 JVM 的各种额外内存需求。线程数多(每个线程栈默认 1MB)、JNI 调用频繁、直接内存使用多时需要调大。
典型问题:java.lang.OutOfMemoryError: Direct buffer memory。原因通常是直接内存使用超限(Netty、RocksDB 未纳入 managed)、JNI 调用内存泄漏。解决方法:增大 JVM Overhead、开启 RocksDB managed 内存、检查 JNI 调用。
三、内存计算逻辑
Flink 的内存计算遵循固定的优先级和公式,理解计算逻辑有助于合理配置内存。
以taskmanager.memory.process.size=4096MB为例:
第1步:计算 JVM 开销 JVM Overhead = 4096 × 0.1 = 409.6MB(范围 [192MB, 1024MB]) JVM Metaspace = 128MB(固定大小) 第2步:计算 Flink 可用内存 Flink Memory = 4096 - 409.6 - 128 = 3558.4MB 第3步:计算 Network Memory Network Memory = 3558.4 × 0.1 = 355.84MB(范围 [64MB, 1024MB]) 第4步:计算 Managed Memory Managed Memory = (3558.4 - 355.84) × 0.4 = 1281.024MB 第5步:计算 Task Heap Framework Heap = 128MB(固定大小) Task Heap = 3558.4 - 128 - 355.84 - 1281.024 = 1793.536MB最终各区域大小:
- Task Heap:约 1794MB(50.5% Flink Memory)
- Managed Memory:约 1281MB(36% Flink Memory)
- Network Memory:约 356MB(10% Flink Memory)
- Framework Heap:128MB(3.6% Flink Memory)
- JVM Metaspace:128MB(3.1% 总内存)
- JVM Overhead:约 410MB(10% 总内存)
可以看到,默认配置下 Task Heap 占比最大(约 50%),适合流处理小状态作业。大状态作业需要调大 Managed Memory 比例,批处理作业需要同时调大 Managed 和 Network。
四、不同场景内存配置对比
不同场景下各内存区域的需求差异很大,合理配置是性能优化的关键。
| 场景 | Task Heap | Managed | Network | 说明 |
|---|---|---|---|---|
| 流处理小状态 | 大 | 小 | 中 | HashMapStateBackend,状态在堆内,算子执行需要大量堆内存 |
| 流处理大状态 | 中 | 大 | 中 | RocksDBStateBackend,状态在堆外,Managed 是关键,堆内存只用于算子执行 |
| 批处理 | 中 | 大 | 大 | 排序/哈希表使用 Managed,Shuffle 使用 Network,堆内存需求中等 |
| 高并发网络 | 中 | 中 | 大 | 高并行度、大流量,网络缓冲需求大,反压风险高 |
| 复杂 CEP/规则 | 大 | 中 | 中 | CEP 模式匹配、复杂规则引擎,堆内存需求大,状态中等 |
配置建议:
- 流处理小状态:
managed.fraction=0.2,network.fraction=0.1,剩余给 Task Heap - 流处理大状态:
managed.fraction=0.5,network.fraction=0.15,Task Heap 保持中等 - 批处理:
managed.fraction=0.5,network.fraction=0.2,Task Heap 保持中等 - 高并发网络:
managed.fraction=0.3,network.fraction=0.25,增加网络缓冲
五、内存调优与最佳实践
下面这张图是内存调优与最佳实践,包括 OOM 排查流程、内存泄漏原因、GC 调优、RocksDB 内存配置。
5.1 OOM 排查流程
遇到 OOM 时,按照以下5步排查:
- 确认 OOM 类型:查看日志中的 OOM 错误信息,确认是 Heap、Metaspace、Direct buffer memory 还是 GC Overhead。不同类型对应不同的内存区域。
- 查看日志堆栈:分析 OOM 时的堆栈信息,定位是哪个算子、哪段代码导致的内存问题。堆栈信息是排查的关键线索。
- 分析内存快照:使用
jmap -dump生成堆转储文件,用 MAT(Memory Analyzer Tool)或 JProfiler 分析大对象和内存泄漏点。 - 调整内存配置:根据分析结果,增大对应内存区域(Task Heap / Managed / Network / Metaspace / Overhead),优化状态后端和用户代码。
- 监控验证:调整后重新运行作业,通过 Flink Web UI 监控各内存区域的使用情况,验证修复效果,确保不再 OOM。
5.2 内存泄漏常见原因
内存泄漏是 Flink 作业最常见的内存问题,常见原因有6个:
- 状态未清理:Keyed State 未配置 TTL,状态无限增长。这是最常见的原因,必须为所有有状态算子配置 TTL。
- 连接未关闭:open() 中创建的外部连接(数据库、Redis、HTTP)在 close() 中未关闭,连接泄漏导致内存和资源耗尽。
- 缓存无界:用户代码中使用 HashMap/List 做缓存但没有上限,数据持续增长直到 OOM。必须使用 LRU 缓存或设置最大容量。
- 广播变量过大:Broadcast State 数据量过大,每个 TaskManager 全量存储,内存占用 = 广播数据大小 × TM 数量。
- 线程泄漏:用户代码中创建线程(Thread/ExecutorService)未关闭,线程数持续增长,每个线程栈占用约 1MB 内存。
- 类加载泄漏:动态类加载(如 Groovy/Janino 脚本编译)后类未卸载,Metaspace 持续增长,最终 Metaspace OOM。
5.3 GC 调优
Task Heap 由 JVM GC 管理,GC 调优对流处理作业的稳定性至关重要。
关键 GC 参数:
- GC 收集器:推荐 G1(JDK 8 默认)或 ZGC(JDK 11+,低延迟)。大堆(>8GB)推荐 G1,低延迟场景推荐 ZGC。
- MaxGCPauseMillis:最大 GC 停顿时间,流处理建议设置 100-200ms。设置过小会导致 GC 频率增加,设置过大会导致单次停顿时长。
- InitiatingHeapOccupancyPercent:触发并发 GC 的堆占用比例,默认 45%。流处理作业建议设置 35-40%,提前触发 GC 避免 Full GC。
- MaxDirectMemorySize:最大直接内存,必须与 Flink 堆外配置(Managed + Network)一致,否则可能导致直接内存 OOM。
GC 调优建议:
- 堆内存不要过大,建议 ≤ 8GB,过大导致 GC 停顿长,大状态用 RocksDB 堆外存储
- 开启 GC 日志:
-Xlog:gc*:file=gc.log:time,level,tags,便于分析 GC 行为 - 关注 Full GC 频率,Full GC 会导致作业长时间停顿,应通过调优避免
- 使用 G1 时设置
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
5.4 RocksDB 内存配置
使用 RocksDBStateBackend 时,RocksDB 的内存管理是关键。
RocksDB 内存分为4层:
- Block Cache:数据块缓存,读性能关键,默认占 Managed Memory 的 50%。缓存热点数据块,减少磁盘 IO。
- Write Buffer:写缓冲(MemTable),写性能关键,默认 64MB × 2(每个 Column Family 两个 MemTable)。数据先写入 MemTable,满后 flush 到磁盘。
- Index Filter:索引和布隆过滤器,查询加速,默认占 Managed Memory 的 10%。加速数据查找,减少磁盘 IO。
- 其他开销:迭代器、快照、Compaction 开销,使用剩余 Managed Memory。
关键配置:
state.backend.rocksdb.memory.managed=true:开启 Managed 内存管理,RocksDB 内存由 Flink 统一控制,避免 OOMstate.backend.rocksdb.memory.write-buffer-ratio=0.5:Write Buffer 占 Managed 的比例state.backend.rocksdb.memory.high-prio-pool-ratio=0.1:Index Filter 高优先级池比例state.backend.rocksdb.block.blocksize=32KB:数据块大小,默认 4KB,大状态建议 32KB
六、关键配置参数
# 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# 网络内存比例(数据传输缓冲、Shuffle)taskmanager.memory.network.fraction:0.1taskmanager.memory.network.min:64MBtaskmanager.memory.network.max:1024MB# JVM 元空间大小taskmanager.memory.jvm-metaspace.size:128MB# JVM 开销比例taskmanager.memory.jvm-overhead.fraction:0.1taskmanager.memory.jvm-overhead.min:192MBtaskmanager.memory.jvm-overhead.max:1024MB# 网络缓冲配置taskmanager.network.memory.buffers-per-channel:2taskmanager.network.memory.floating-buffers-per-gate:8taskmanager.network.memory.buffer-size:32KB# RocksDB 内存管理state.backend:rocksdbstate.backend.rocksdb.memory.managed:truestate.backend.rocksdb.memory.write-buffer-ratio:0.5state.backend.rocksdb.memory.high-prio-pool-ratio:0.1# 每个 TaskManager 的 Slot 数taskmanager.numberOfTaskSlots:1# 默认并行度parallelism.default:1七、常见问题与最佳实践
常见问题
Heap OOM(Java heap space)
- 原因:状态过大(HashMapStateBackend)、用户缓存无界、数据倾斜、对象创建频繁。
- 解决:增大 Task Heap、改用 RocksDBStateBackend、配置状态 TTL、优化用户代码缓存、解决数据倾斜。
Direct buffer memory OOM
- 原因:堆外内存不足(Managed + Network 配置过小)、RocksDB 未开启 managed 内存、Netty 直接内存超限。
- 解决:增大 Managed/Network Memory、开启 RocksDB managed 内存、增大 JVM Overhead、检查 MaxDirectMemorySize 配置。
Metaspace OOM
- 原因:类加载泄漏、UDF 过多、动态代码生成量大、CEP 模式编译。
- 解决:增大 JVM Metaspace、检查类加载泄漏、减少动态类生成、复用 UDF 实例。
GC Overhead OOM
- 原因:堆内存不足、对象创建频繁、Full GC 频繁、GC 时间过长。
- 解决:增大 Task Heap、优化对象创建(复用对象、避免频繁序列化)、调优 GC 参数、改用 RocksDB 减少堆内存压力。
Container killed by YARN/K8s
- 原因:容器实际内存使用超过申请的总内存(process.size),通常是 JVM Overhead 不足或堆外内存泄漏。
- 解决:增大 process.size、增大 JVM Overhead、检查堆外内存泄漏、开启 RocksDB managed 内存。
内存使用持续增长
- 原因:状态未清理(无 TTL)、用户缓存无界、连接泄漏、线程泄漏、类加载泄漏。
- 解决:配置状态 TTL、用户缓存设置上限、close() 中关闭连接和线程、检查类加载泄漏。
最佳实践
大状态用 RocksDB:状态 > 1GB 必须使用 RocksDBStateBackend,状态存储在堆外 Managed Memory,不受 JVM GC 影响,避免 Full GC 导致的作业停顿,支持 TB 级大状态。
配置状态 TTL:为所有有状态算子的 Keyed State 配置 TTL(Time-To-Live),自动清理过期状态,防止状态无限增长导致 OOM。TTL 支持 processing time 和 event time,可在 StateDescriptor 中配置。
合理设置堆大小:Task Heap 不要过大,建议 ≤ 8GB,过大导致 GC 停顿长,影响流处理延迟。大状态用 RocksDB 堆外存储,堆内存只用于算子执行和临时对象。
开启 Managed 内存:RocksDB 必须开启
state.backend.rocksdb.memory.managed=true,由 Flink 的 Managed Memory 统一管理 RocksDB 内存,避免 RocksDB 内存超限导致容器 OOM。监控内存使用:通过 Flink Web UI 监控各内存区域的使用情况(Heap / Managed / Network / Direct),及时发现内存泄漏和 OOM 风险。关注 Metrics:
Status.Flink.Memory.Managed.Used、Status.Flink.Memory.Network.Used。资源关闭在 close():所有外部资源(数据库连接、Redis 连接、HTTP 客户端、文件句柄、线程池)必须在 RichFunction 的 close() 方法中关闭,防止资源泄漏导致内存和连接耗尽。使用 try-with-resources 确保关闭。
避免用户缓存无界:用户代码中的缓存(HashMap/List/Guava Cache)必须设置最大容量和过期策略,使用 LRU 缓存或 Caffeine,防止数据持续增长导致 OOM。不要在静态变量中缓存大量数据。
八、总结
Flink 内存模型是一个分层嵌套结构,TaskManager 总内存(process.size)= JVM 进程内存(flink.size)+ JVM Metaspace + JVM Overhead。JVM 进程内存分为堆内内存(Framework Heap + Task Heap)和堆外内存(Managed Memory + Network Memory)。Task Heap用于用户算子执行和 HashMapStateBackend 状态,由 JVM GC 管理;Managed Memory用于 RocksDB 状态、批处理排序和哈希表,由 Flink 直接管理;Network Memory用于数据传输缓冲和 Shuffle;JVM Metaspace用于类元数据;JVM Overhead用于线程栈、JNI、代码缓存等其他开销。
内存计算遵循固定公式: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。理解计算逻辑有助于合理配置各内存区域。
内存调优的核心是合理分配各内存区域和防止内存泄漏。OOM 排查5步:确认 OOM 类型 → 查看日志堆栈 → 分析内存快照 → 调整内存配置 → 监控验证。内存泄漏常见原因包括状态未清理、连接未关闭、缓存无界、广播变量过大、线程泄漏、类加载泄漏。GC 调优推荐 G1 收集器,设置 MaxGCPauseMillis=200ms,大堆可考虑 ZGC。RocksDB 内存分为 Block Cache、Write Buffer、Index Filter、其他开销四层,开启 managed 内存由 Flink 统一管理。
大状态用 RocksDB、配置状态 TTL、合理设置堆大小、开启 Managed 内存、监控内存使用、资源关闭在 close()、避免用户缓存无界,这7条最佳实践可以显著提高 Flink 作业的内存稳定性,减少 OOM 和内存泄漏问题,为生产环境的稳定运行提供保障。