
1. 从一次线上OOM说起为什么Flink 1.11的内存模型值得单独拎出来讲Flink 1.11这个版本在内存管理上是个分水岭。如果你是从1.9、1.10一路用上来的会发现之前那套taskmanager.heap.size加taskmanager.memory.size的配置逻辑突然不灵了社区把内存模型整个翻新了一遍引入了TaskExecutor总内存、Framework堆内存、Task堆外内存、Managed Memory、Network Memory这一整套概念。很多人第一次看到taskmanager.memory.process.size这个参数的时候是懵的——以前一个taskmanager.heap.size走天下现在冒出来七八个参数还得算比例、算预留稍不留神就是启动报错或者跑着跑着OOM。我写这篇东西的起因是前段时间帮一个团队排查一个作业频繁Full GC的问题。他们的作业逻辑不复杂就是Kafka进、做点窗口聚合、再写回Kafka但TaskManager隔三差五就重启日志里全是java.lang.OutOfMemoryError: Java heap space。他们一开始的反应是堆不够加堆把taskmanager.heap.size从2G调到8G结果问题没解决反而因为容器内存超限被YARN直接kill了。这就是典型的没搞懂1.11内存模型——堆内存不是你想加就能加它受制于总进程内存的盘子而且堆外还有一堆内存在跟你抢。所以这篇内容我打算把Flink 1.11的内存模型从头到尾拆一遍重点讲清楚三件事这套模型到底分了哪几块内存、每块内存由哪个参数控制、以及在实际调参时怎么根据作业特征去分配。适合正在用Flink 1.11到1.13这个区间版本、被内存问题折腾过的开发和运维同学。如果你用的是1.14以后模型又有些微调但大框架是一致的看这篇也能帮你理解底层逻辑。2. Flink 1.11内存模型的五层切分从进程内存到Task可用堆2.1 总进程内存是最大的那个圈Flink 1.11把TaskManager的内存从外到内切成了好几层最外面一层叫Total Process Memory对应参数taskmanager.memory.process.size。这个参数代表的是整个TaskManager进程占用的物理内存总量包括JVM本身的开销、元空间、线程栈、代码缓存这些非堆但真实占内存的部分。你可以把它理解成你向资源调度器YARN、K8s申请的那个容器大小。这一层为什么重要因为它才是容器实际被限制的上限。很多人的误区是只盯着堆内存调结果堆没爆容器先被YARN因为超内存干掉了。YARN判断一个容器是否超限看的是物理内存RSS而不是你JVM堆用了多少。JVM堆只是进程内存的一部分堆外内存、元空间、JVM自身开销加起来可能占到几百MB甚至上G。所以process.size必须留足余量不能等于堆大小。在1.11里如果你不显式设置process.sizeFlink会根据其他参数反推一个默认值但这个默认值往往偏保守。我的习惯是永远显式设置这个参数让内存账目清清楚楚。2.2 去掉JVM开销剩下的是Flink总内存从process.size里扣掉JVM Metaspace、JVM Overhead这两块后面细说剩下的就是Total Flink Memory对应taskmanager.memory.flink.size。这块内存是Flink框架自己能支配的部分它又被进一步切成三块Framework内存、Task内存、Network内存。这里有个关键点process.size和flink.size你只能设一个。如果你两个都设Flink会报配置冲突。一般推荐设process.size因为它是从容器视角出发的更贴近实际部署flink.size更适合那种你自己管理JVM、不需要考虑容器限制的场景。我见过有人两个都配了启动直接抛ConfigurationException排查半天才发现是这俩参数打架。2.3 Framework内存框架自己吃饭的碗Framework Memory是Flink框架自身运行需要的内存包括JobManager通信、Checkpoint协调、状态后端元数据管理这些。它又分堆内和堆外两部分由taskmanager.memory.framework.heap.size和taskmanager.memory.framework.off-heap.size控制。默认情况下Framework堆内存是128MB堆外也是128MB。这块内存一般不需要动。除非你的作业有极其复杂的算子链或者大量的并发Checkpoint协调否则默认值够用。我调过的作业里只有那种单TaskManager跑几百个slot的极端场景才需要往上加Framework内存。普通作业动这块纯属给自己找麻烦。2.4 Task内存你的业务代码真正跑的地方Task Memory是用户代码执行的地方也就是你的Map、FlatMap、Window这些算子实际消耗的内存。它同样分堆内和堆外taskmanager.memory.task.heap.size和taskmanager.memory.task.off-heap.size。这里有个特别容易踩的坑Task堆内存的默认值不是固定的而是剩余法算出来的。Flink会从Total Flink Memory里扣掉Framework内存、Network内存、Managed Memory之后把剩下的全给Task堆内存。这意味着如果你不设Managed Memory和Network MemoryTask堆会吃掉大部分内存但如果你把Managed Memory设得很大Task堆就会被压缩可能导致业务代码OOM。我一般建议显式设置Task堆内存不要让Flink去猜。设多少取决于你的作业状态大小和算子复杂度。纯ETL、无状态的作业1G到2G足够带大窗口聚合的可能要4G以上。2.5 Managed Memory与Network Memory最容易被忽视的两块Managed Memory对应taskmanager.memory.managed.size或taskmanager.memory.managed.fraction这块内存主要给RocksDB状态后端和批处理算子用。如果你用RocksDB做状态后端Managed Memory就是RocksDB的block cache和write buffer的来源。默认fraction是0.4也就是Total Flink Memory的40%。Network Memory对应taskmanager.memory.network.min/max/fraction用于网络传输的缓冲区也就是shuffle和数据交换时的内存池。默认fraction是0.1min是64MBmax是1GB。这两块内存的特点是它们从堆外分配不占JVM堆。所以你在算堆内存的时候要意识到堆外还有这两块在吃内存。很多人堆设得很大结果堆外Managed Memory加上Network Memory把进程内存撑爆容器被kill还以为是堆的问题。下面这张表把各层内存和对应参数理一遍内存层级参数默认值/规则是否堆内总进程内存taskmanager.memory.process.size无默认需显式设含堆内外Flink总内存taskmanager.memory.flink.size由process反推含堆内外Framework堆taskmanager.memory.framework.heap.size128MB堆内Framework堆外taskmanager.memory.framework.off-heap.size128MB堆外Task堆taskmanager.memory.task.heap.size剩余法堆内Task堆外taskmanager.memory.task.off-heap.size0堆外Managed Memorytaskmanager.memory.managed.fraction0.4堆外Network Memorytaskmanager.memory.network.fraction0.1堆外JVM Metaspacetaskmanager.memory.jvm-metaspace.size256MB堆外JVM Overheadtaskmanager.memory.jvm-overhead.fraction0.1堆外3. 堆内堆外到底怎么分JVM Metaspace和Overhead的预留逻辑3.1 JVM Metaspace不是无限大taskmanager.memory.jvm-metaspace.size默认256MB。Metaspace存的是类的元数据Flink本身加载的类加上你作业jar包里带的类一般不会超过256MB。但如果你用了大量动态生成类的东西比如某些序列化框架、Groovy脚本Metaspace可能会涨。我遇到过一次Metaspace OOM是因为作业里用了反射动态生成大量代理类256MB不够调到512MB才稳住。这个参数一般不用动但如果你的作业启动时报Metaspace相关的OOM第一个要看的就是它。3.2 JVM Overhead是留给JVM自己的杂项开支taskmanager.memory.jvm-overhead.fraction默认0.1min 192MBmax 1GB。这块内存是留给线程栈、代码缓存、GC自身开销、Direct Memory等JVM内部使用的。它按Total Process Memory的比例算但有上下限。为什么要有这块因为JVM除了堆和Metaspace还有一堆看不见的内存消耗。线程栈默认1MB一个线程几百个线程就是几百MBJIT编译后的代码放在CodeCache里GC的卡表、记忆集也要占内存。这些加起来不是小数目如果不预留容器很容易因为RSS超限被kill。我的经验是小内存TaskManagerprocess.size小于4G用默认的192MB下限就行大内存TaskManager8G以上建议把fraction调到0.15因为大内存场景下线程数、GC开销都会更大。3.3 堆外内存的隐形消耗清单很多人算内存只算堆这是最大的误区。Flink 1.11里堆外内存的消耗项包括Managed MemoryRocksDB用默认占Flink总内存40%Network Memory网络缓冲区默认占10%Framework堆外128MBTask堆外默认0但如果你用了堆外状态或某些算子会占JVM Metaspace256MBJVM Overhead至少192MBDirect MemoryNetty、NIO用的部分和Network Memory重叠线程栈每个线程1MB左右把这些加起来一个process.size为4G的TaskManager堆内存可能只能分到2G左右。如果你把堆设成3.5G堆外那些加起来就超过4G了容器必被kill。提示排查内存问题时先用jcmd pid VM.native_memory summary看JVM的Native Memory Tracking或者用pmap看进程内存映射确认到底是堆爆了还是堆外爆了。别一上来就加堆。4. 参数调整的实战推演三种典型作业的内存分配方案4.1 无状态ETL作业堆内存为主Managed Memory可以压假设你有一个纯ETL作业Kafka进Kafka出中间只有Map和Filter没有窗口、没有状态。这种作业的特征是堆内存需求中等Managed Memory几乎用不上。我的配置思路是taskmanager.memory.process.size: 2048m taskmanager.memory.managed.fraction: 0.1 taskmanager.memory.network.fraction: 0.1 taskmanager.memory.task.heap.size: 1024m taskmanager.memory.jvm-overhead.fraction: 0.1算一下账process 2048mJVM Overhead按0.1算是204m但min是192m所以取204mMetaspace 256m剩下Flink总内存约1588m。Managed Memory按0.1算是158mNetwork按0.1算是158mFramework堆128m加堆外128m剩下Task堆约1016m和显式设的1024m基本吻合。这种配置下堆内存占了进程内存的一半堆外主要是Metaspace和OverheadManaged Memory压到最低适合无状态作业。4.2 大窗口聚合作业Managed Memory要留足堆也不能小带大窗口比如1小时、1天的聚合作业状态可能很大。如果用RocksDB做状态后端Managed Memory就是RocksDB的命根子。这时候配置要反过来taskmanager.memory.process.size: 8192m taskmanager.memory.managed.fraction: 0.4 taskmanager.memory.network.fraction: 0.1 taskmanager.memory.task.heap.size: 2048m taskmanager.memory.jvm-overhead.fraction: 0.15process 8192mOverhead按0.15算是1228mMetaspace 256mFlink总内存约6708m。Managed Memory按0.4算是2683mNetwork按0.1算是670mFramework 256m剩下Task堆约3099m。显式设2048m的话多出来的会被算进其他地方或者报错所以这里要么不显式设Task堆要么设得和剩余法算出来的一致。这种配置下Managed Memory占了近2.7G给RocksDB足够的block cache减少磁盘IO堆内存2G左右够业务代码用Network 670m应对shuffle。4.3 高吞吐shuffle作业Network Memory是瓶颈有些作业逻辑简单但数据量极大shuffle频繁比如大规模join或者repartition。这种作业的瓶颈往往在Network Memory上。默认0.1的fraction可能不够会导致网络缓冲区频繁申请释放吞吐上不去。taskmanager.memory.process.size: 4096m taskmanager.memory.network.fraction: 0.2 taskmanager.memory.network.min: 256m taskmanager.memory.network.max: 1g taskmanager.memory.managed.fraction: 0.2 taskmanager.memory.task.heap.size: 1536mNetwork提到0.2min提到256m给网络传输更多缓冲。Managed降到0.2因为这种作业状态通常不大。堆1.5G左右够用。注意Network Memory的min和max要设得合理。min太小会导致启动时缓冲区不够max太大可能挤占其他内存。一般min设64m到256mmax设1g到2g。5. 踩坑实录那些年我们配错的内存参数5.1 坑一只设heap.size不设process.size这是最常见的坑。1.11之前大家习惯设taskmanager.heap.size升级到1.11后这个参数虽然还能用但语义变了——它现在只影响Task堆内存不影响总进程内存。如果你只设它Flink会用默认的process.size通常是1024m或者根据其他参数反推结果你设了4G堆process才1G启动直接报内存不足。正确做法升级到1.11后把taskmanager.heap.size替换成taskmanager.memory.process.size然后根据需要设Task堆。5.2 坑二Managed Memory和Task堆抢内存有个作业用RocksDB状态后端状态很大但没设Managed Memory用的是默认0.4。同时Task堆设了4Gprocess设了6G。算一下process 6GOverhead 0.1是614mMetaspace 256mFlink总内存约5.1G。Managed 0.4是2GNetwork 0.1是510mFramework 256m剩下Task堆约2.3G。但显式设了4G这就冲突了Flink会报配置错误。正确做法要么不显式设Task堆让Flink用剩余法算要么把process.size加大让剩余法算出来的Task堆能满足你的需求。5.3 坑三Network Memory设太小导致反压一个高吞吐作业Network Memory用默认0.1process 4G算下来Network约350m。跑起来发现反压严重但CPU和堆都很闲。查了半天发现是网络缓冲区不够数据在算子间传输时频繁等待缓冲区。正确做法高吞吐shuffle场景把Network fraction提到0.15到0.2或者显式设min和max。同时用Flink UI的Network Buffer指标监控使用率如果长期接近100%就该加了。5.4 坑四JVM Overhead预留不足被容器kill这个坑最隐蔽。process设了4G堆设了3GManaged 0.4Network 0.1算下来堆外已经超过4G了但JVM Overhead只留了默认的192m。跑一段时间后容器被YARN kill日志里没有OOM只有Container killed by YARN for exceeding memory limits。正确做法大内存TaskManager把jvm-overhead.fraction调到0.15或者显式设jvm-overhead.size。同时用pmap或NMT确认进程实际RSS确保process.size大于RSS。6. 监控与验证调完之后怎么确认配对了6.1 启动日志里的内存账本Flink启动时会在日志里打印内存配置的详细账本类似这样Total Process Memory: 4096MB Total Flink Memory: 3276MB Framework Heap: 128MB Framework Off-Heap: 128MB Task Heap: 1536MB Task Off-Heap: 0MB Managed Memory: 1310MB Network Memory: 327MB JVM Metaspace: 256MB JVM Overhead: 409MB每次调完参数第一件事就是看这段日志确认各项加起来等于process.size没有冲突或溢出。如果Flink报Sum of configured ... exceeds ...说明你的显式配置和剩余法算出来的对不上需要调整。6.2 运行时监控的关键指标调完参数后通过Flink UI或Metrics系统关注这几个指标Heap Used / Heap Committed堆使用率长期超过80%要考虑加堆或优化代码Managed Memory UsedRocksDB实际用的Managed Memory如果接近总量说明需要加ManagedNetwork Buffer Usage网络缓冲区使用率长期高位说明Network不够GC Time / GC CountFull GC频繁说明堆不够或者对象生命周期有问题Container Memory RSS进程实际物理内存对比process.size看余量6.3 一个简单的压测验证方法调完参数后别直接上生产。先用一个数据量相当的测试作业跑一遍观察上述指标。我一般会跑一个持续30分钟以上的压测因为有些内存问题比如Managed Memory泄漏、Network缓冲区碎片需要时间才能暴露。压测期间用jstat -gc pid 1000持续观察GC情况用jcmd pid VM.native_memory summary看堆外内存变化。如果压测期间RSS稳定、GC正常、无反压基本可以认为配置合理。如果RSS持续上涨可能是堆外泄漏如果Full GC频繁堆不够如果反压但CPU闲Network不够。7. 几个容易被忽略的细节和我的个人习惯7.1 Slot数量影响内存分配TaskManager的slot数量taskmanager.numberOfTaskSlots会影响每个slot能分到的内存。如果你一个TaskManager跑4个slot那Task堆内存是被4个slot共享的每个slot实际能用的是Task堆的四分之一。所以slot越多Task堆要设得越大否则单个slot的算子容易OOM。我的习惯是slot数量不超过CPU核数Task堆按每个slot至少512MB来估算。比如4个slotTask堆至少2G。7.2 状态后端选择决定Managed Memory策略用HashMapStateBackend1.11里叫MemoryStateBackend的替代的话状态存在堆里Managed Memory用不上可以压到最低。用EmbeddedRocksDBStateBackend的话Managed Memory就是RocksDB的内存池必须留足。所以先确定状态后端再定Managed Memory顺序不能反。7.3 批作业和流作业的内存模型差异Flink 1.11的批作业DataSet API和流作业DataStream API共用同一套内存模型但批作业对Managed Memory的依赖更强因为批处理的sort、join、hash表都用Managed Memory。批作业建议Managed fraction设0.5以上流作业看状态大小0.2到0.4之间。7.4 我个人的配置模板经过多个项目的折腾我总结了一个流作业的通用模板大部分场景可以直接用taskmanager.memory.process.size: 4096m taskmanager.memory.jvm-metaspace.size: 256m taskmanager.memory.jvm-overhead.fraction: 0.1 taskmanager.memory.managed.fraction: 0.3 taskmanager.memory.network.fraction: 0.1 taskmanager.memory.network.min: 64m taskmanager.memory.network.max: 1g taskmanager.numberOfTaskSlots: 2这个模板下process 4GOverhead约410mMetaspace 256mFlink总内存约3.4G。Managed约1GNetwork约340mFramework 256mTask堆约1.8G。两个slot各分900m左右适合中等状态、中等吞吐的作业。如果状态大把Managed提到0.4如果shuffle多把Network提到0.15如果堆不够加大process.size。调参这件事没有银弹核心是搞清楚每块内存的用途根据作业特征分配然后用监控验证。Flink 1.11的内存模型虽然参数多但逻辑是清晰的——它把以前模糊的堆内存拆成了有明确用途的几块逼着你去想清楚每块内存到底给谁用。想明白了调参就不难了。