资讯动态

内存计算框架资源分配优化:Spark与Flink实战调优指南

发布时间:2026/10/13 3:44:00 来源:尧图企业网站定制
做大数据加工这些年我见过的任务跑不动大部分不是代码写得烂而是内存计算框架的资源分配压根没给对。调内存参数这件事表面上看是改几个配置项实际上它牵扯到框架怎么划分堆内堆外、怎么平衡执行内存和存储内存、怎么避免和操作系统其他进程抢资源一环扣一环。这篇文章就围绕大数据领域的内存计算专门聊资源分配怎么落到实处让数据处理任务既跑得快又不容易把集群搞挂。适合自己搭过 Spark、Flink或者正在被各类 OOM、Full GC 折磨的同学参考。1. 内存计算为什么是数据处理的“胜负手”1.1 磁盘 I/O 的流血教训传统 MapReduce 时代处理一个离线任务最直观的感受就是慢而且慢得让人没脾气。一个几 TB 的关联查询中间结果要反复落到磁盘Map 端写完Reduce 端再读一次 join 恨不得让磁盘搬好几趟数据。磁盘顺序读写虽然还能撑一撑但随机读写、大量小文件反复打开关闭直接把吞吐拉到谷底。我见过一个日调度任务原本只要十几分钟就能跑完的逻辑改成磁盘落地的老方案后跑了快三个小时差距就摆在那不是算法变聪明了而是数据根本没有必要反复进出磁盘。内存计算的切入点很直接能留在内存中间结果、Shuffle 数据、状态数据就尽量减少落盘。但注意这不是说内存计算就完全不落盘而是把“能复用、高频复用、多轮迭代”的数据尽量放在内存里。对于迭代式计算、交互式查询、流处理状态维护这类场景的收益最为明显。数据不需要每次计算都从磁盘翻出来内存带宽和延迟的优势被真正用起来整个任务的耗时就能缩短一个量级。1.2 内存计算并不是“把数据塞进内存”很多新手有个误解以为内存计算就是想法子把所有数据都加载到内存里内存不够就加内存。真正做起来完全不是这回事。内存计算的本质是计算模型的改变算子在流水线中尽量不落盘同一份数据在内存中被多个计算阶段复用通过缓存、广播、列式存储等手段减少重复 I/O。数据能不能全放内存取决于业务需求和数据规模而不是拍脑袋决定。比如 Spark 的 RDD/DataFrame 缓存适合反复使用的中间结果但如果你把一份每天新增几个 G 的全量明细无脑 cache不仅占用大量存储内存还可能挤压执行内存反而拖垮后续 Shuffle。内存计算要解决的是“如何让有限的内存发挥最大计算效率”而不是“把无限的数据塞进内存”。这也是资源分配优化的核心矛盾所在。1.3 为什么资源分配这一环最容易被低估同样一套代码不同人配出来的运行表现可能差好几倍。问题往往不出在 SQL 或者 DataFrame 的写法上而是资源分配阶段就没把内存模型吃透。比如同一个 Executor 上执行内存和存储内存怎么切堆外开了多大JVM 老年代够不够用这些参数决定了任务在高峰期是顺滑跑完还是频繁 Full GC、疯狂溢写磁盘甚至直接 OOM 被 Container 干掉。我在给某套集群做优化的时候发现任务失败记录里有一大半是“Container killed by YARN for exceeding memory limits”但业务方第一反应是“加内存”。加完以后依然失败因为物理内存超限的根因往往是堆外内存、网络缓冲、内存开销这几项没算进去单纯调整堆内参数根本没解决整体预算超限的问题。资源分配最容易被低估的原因就在这里它不是一个配置项的事而是一整套内存模型下的预算测算。2. 给内存算账四个关键维度2.1 总内存预算别把物理内存掏空在分布式计算里每个节点上跑的不只是计算进程还有操作系统、监控 Agent、HDFS 的 DataNode、可能存在的 NodeManager 等常驻进程。如果给 Executor 或 TaskManager 规划的内存总和逼近物理内存上限系统就会开始疯狂使用 Swap性能瞬间断崖式下跌。我一般会在节点内存预算里预留 10%~20% 给系统和其他服务具体比例取决于节点角色和混合部署情况。比如一台 128G 物理内存的数据节点我通常会把计算框架可用的总内存控制在 100G~110G 左右剩下的留给 Page Cache、系统内核和周边进程。有些同学看到 128G 就恨不得配出 120G 给 Executor结果跑大任务时节点直接卡死SSH 都连不上去。内存计算追求的是“用满”而不是“用绝”留出余量才能让 JVM、堆外内存和操作系统的 Page Cache 彼此不打架。2.2 堆内与堆外必须想清楚的“外快钱”堆内内存是 JVM 管理的那部分对象创建、回收都走 GC。堆外内存则绕过 JVM 堆直接使用操作系统内存适用于大块缓冲区、DirectByteBuffer、以及某些框架的原生数据结构。为什么不能让所有东西都放堆内因为堆内几十 GB 的 JVM 很容易让 GC 变成灾难Full GC 一次卡顿好几秒吞吐和延迟都让人自闭。堆外内存可以减少 GC 压力但代价是需要自己管理生命周期用不好更容易出内存泄漏。在 Spark 里Executor 的物理内存大致等于spark.executor.memory加上spark.executor.memoryOverhead。后者就是堆外开销默认大概是堆内内存的 10%下限 384M。很多线上任务跑 Shuffle、访问 HDFS、使用某些原生库时堆外内存用量很容易超出默认值。我通常会在 Shuffle 数据量大或者启用 Kyro 之外的原生序列化时把 overhead 调高到堆内的 15%~25%而不是等 Container 被 YARN 杀掉了再补课。2.3 执行内存与存储内存的跷跷板以 Spark 的统一内存管理为例堆内有一个统一内存池由spark.memory.fraction划定默认是 0.6。也就是说Executor 堆内 24G 的话统一内存大约 14.4G。剩余 40% 留给 JVM 自身的结构、用户代码、以及其他不便纳入统一管理的内存。统一内存内部又分执行内存和存储内存spark.memory.storageFraction默认 0.5它表示存储内存从统一内存中分到的保底份额。执行内存用于 Shuffle、join、aggregation 等操作存储内存用于缓存 RDD/DataFrame、广播变量等。两者可以互相抢占但执行内存在被存储内存占用时可以强行驱逐存储数据反过来存储内存不能驱逐执行内存。所以如果任务里 Shuffle 很重我会把 storageFraction 调低一些比如 0.3给执行内存留更大的腾挪空间如果任务要大量缓存复用中间结果就调高到 0.6。这个比例没有银弹完全取决于你的任务画像。2.4 JVM 本身也要留口粮很多人在调参时只盯着spark.executor.memory却忽略了 JVM 自身需要的元空间、线程栈、字符串常量池这些内存。如果堆内内存设置过于贴近上限统一内存还没开始用老年代就先溢出了。实际上默认的 0.6 这个比例就是帮 JVM 留出了 40% 的余地但如果你在代码里创建了大量对象驻留在堆内或者用了大量自定义类加载器这 40% 也不一定够用。我曾经遇到过一个任务代码里频繁用new创建临时对象并且缓存了一些没有被回收的大集合结果 Executor 频繁 Full GC任务时好时坏。后来通过调整代码把这些大集合改成按需加载同时把spark.memory.fraction降到 0.5情况立刻好转。调 JVM 内存不是只看计算框架的参数也要结合代码行为的实际情况把用户代码、第三方库的驻留内存考虑进去。3. 实操Spark 内存资源分配的完整流程3.1 起步时的保守配置我给新任务配置内存时不会一上来就追求极限吞吐而是先给一套保守参数确认任务能稳定跑通。假设一个作业节点是 8 核 32G 物理内存起步配置可以这样给。spark.executor.cores4 spark.executor.memory12g spark.executor.memoryOverhead2g spark.memory.fraction0.6 spark.memory.storageFraction0.5 spark.sql.shuffle.partitions200这样单 Executor 物理内存约 14G一台节点可以放两个 ExecutorCPU 资源也能被相对均匀地分配。先跑一遍小规模数据观察 Spark UI 里的 Shuffle Spill、GC 时间、执行内存峰值再决定要不要加大内存、提高并行度。一上来就把spark.executor.memory配成 24G任务确实可能会快但一旦 Shuffle 量上来GC 停顿和溢写风险也会同步放大。3.2 用一道算术题拆解 Executor 内存划分为了让大家真正理解这几个参数的关系我举个例子假设我们把 Executor 内存设为 24Goverhead 设为 4Gspark.memory.fraction默认 0.6spark.memory.storageFraction默认 0.5。先算统一内存24G × 0.6 14.4G。这个 14.4G 由执行内存和存储内存共享。存储内存保底为 14.4G × 0.5 7.2G执行内存理论上最多可以占满整个统一内存但在存储内存没有数据需要缓存时执行内存可以尽情使用。反过来如果存储内存占了 7G执行内存就只能用剩下的大约 7.4G但在压力大的时候执行内存可以驱逐存储内存的数据。再来看 overhead 的 4G。这部分不会出现在 JVM 堆内而是用于 JVM 的 Direct Memory、线程栈、以及 Spark 内部的一些原生内存开销。为什么我要给比默认值更高的 4G因为任务里有大量 Shuffle 和 HDFS 读写这些操作涉及 DirectBuffer默认的 2.4G 在实际负载下很容易触顶索性多留一些余量。整体上单个 Executor 的实际占用是 24G 4G 28G这也是容器调度时需要向 YARN 申请的物理内存预算。3.3 并行度与内存的配合内存分配不是孤立的事情它和并行度密切相关。同样一个 Executor4 个核和 8 个核处理同样的数据量内存压力和 GC 表现完全不同。核数多意味着并发执行的 Task 多每个 Task 需要的执行内存、聚合缓冲区都会放大。如果你给 Executor 配了 8 个核却只有 16G 堆内每个 Task 分到的内存大约 2G遇到大 Shuffle 很容易溢写或者 OOM。我的建议是先确定 Executor 的核数再根据单 Task 的内存需求反推 Executor 内存。正常情况下单个 Shuffle 任务至少要有 1G~2G 的可用内存复杂 join、聚合操作则要更多。一个 8 核 Executor堆内内存最好别低于 20G同时要把 overhead 也算进去。如果因为节点资源限制给不了那么大堆宁可减少 Executor 的核数让每个 Task 吃得更饱也不要为了跑满 CPU 让每个 Task 饿肚子。3.4 用 Spark UI 验证分配效果参数配完不是就结束了还要通过 Spark UI 的 Executor 页签观察内存使用情况。重点看两个指标一个是 Storage Memory 的当前使用量和 RDD/DataFrame 缓存大小另一个是 Shuffle Spill 的磁盘溢写量包括 Spill (memory) 和 Spill (disk)。如果 Spill (disk) 很大说明执行内存不够用结果往磁盘吐了如果 Shuffle 阶段频繁 GC且 GC 时间占总运行时间的比例超过 5%就要考虑调整内存比例或优化代码逻辑。我还习惯看 Executor 的峰值内存Peak JVM Memory和 Direct Memory 相关监控。正常情况下每个 Executor 的物理内存占用不会超过spark.executor.memory spark.executor.memoryOverhead。一旦接近上限任务很容易被 Container 杀掉。发现这类征兆我会优先检查是不是 Shuffle 数据量异常、广播变量过大、或者某些代码把大量数据 collect 回 Driver而不是盲目加内存。4. 扩展到 Flink 的资源分配要点4.1 TaskManager 内存划分Flink 的内存模型和 Spark 不同TaskManager 的进程内存包含 JVM 堆、托管内存Managed Memory、网络缓冲、框架内存、以及 JVM 自身开销。taskmanager.memory.process.size定义了整个进程的总内存Flink 会根据这个值自动推导各区域的分配。实际调优时我先看任务的状态大小和计算复杂度再决定堆内和托管内存的比例。和 Spark 的一个明显差异是Flink 的托管内存主要用于排序、哈希表、状态后端等默认taskmanager.memory.managed.fraction是 0.4。如果任务使用了 RocksDB 状态后端状态数据其实不在 JVM 堆内而在托管内存中这时需要保证托管内存足够大不然状态读写会频繁落到磁盘性能立刻下滑。反之如果任务主要是流式处理、状态不大托管内存配 30% 就够用了。4.2 状态内存与网络缓冲流处理任务的核心是状态状态内存给多少直接决定大状态作业能否稳定运行。RocksDB 状态后端会使用堆外内存我需要重点关注taskmanager.memory.managed.size或taskmanager.memory.managed.fraction并预留足够空间给 RocksDB 的 Block Cache、Write Buffer 等。如果状态增长快、并发高托管内存不足会导致本地 RocksDB 频繁合并严重时还会抛OutOfMemoryError。网络缓冲内存则用于 Flink 节点间的数据交换默认值为taskmanager.memory.network.size的 64MB。在数据 Shuffle 量大的 Window 作业或者大量数据在节点间交叉传输时64MB 往往不够用。调大网络缓冲可以减少反压的发生频率但也要注意它不是越大越好因为网络缓冲也是从进程总内存里切出来的会给其他区域带来压力。4.3 与反压的关系之前帮人排查一个实时数仓任务现象是 Source 端消费 Kafka 速率忽高忽低Sink 端写 HBase 经常超时。看指标发现 TaskManager 的网络缓冲经常打满背压一直往上顶。进一步分析是作业里有大窗口 join中间数据量暴涨而网络缓冲和算子内部缓冲池设置偏小。后来调整了网络缓冲内存同时把窗口算子的并行度提高反压才降下来。反压是流处理系统的信号灯它本质上是某个算子处理速度跟不上让上游不再超速发送。但很多时候根因正是资源分配没跟上。内存分配合理算子就能多缓冲一些数据平滑流量抖动内存分配不足数据只能堆积、超时、失败。所以排查反压问题一定不要只盯着业务逻辑还要把 TaskManager 内存曲线拉出来看特别是网络缓冲和托管内存的使用情况。5. 常见问题与排查技巧实录5.1 Executor OOM别急着加内存Spark 任务最常见的死法是 Executor 进程被 YARN 杀掉日志里通常写着 physical memory usage 超限。很多人的第一反应是把spark.executor.memory往上提但我遇到的大部分案例根因都是 overhead 不足、Shuffle 数据异常、或者某处代码把不该 collect 的全量数据拉了回来。真正的做法是分几步排查先在 Spark UI 看每个 Executor 的峰值内存是否符合预算再分析 GC 时间和溢写情况最后确认是不是任务的数据量超过了预期。有一个典型例子某任务每天全量刷新一张大表我查日志发现每个 Executor 都以Container killed结束但代码逻辑本身不复杂。打开 UI 一看输入数据量比平时涨了五倍Shuffle 溢写量大得离谱。这时候单纯加 Executor 内存只是缓解真正该做的是优化关联逻辑、增加分区数、或提前过滤掉无用字段。加内存不是不行但要分清到底是“预算不够”还是“效率太低”。5.2 Driver OOM通常和 collect 有关Driver 端的问题也经常出现尤其是代码里写了collect或take把所有结果拉回 Driver 再处理。小数据没问题数据量一大Driver 堆就被打爆了。我在一个报表任务里踩过这个坑DataFrame 聚合结果只有几十万行但每个 Executor 返回的中间对象体积很大collect到 Driver 后 JVM 直接 Full GC最后整诊断出来才发现是某字段里面存了超大字符串。应对方案很简单尽量不要一次性把全量结果 collect 回 Driver改成用saveAsTable或者写入外部存储让后续逻辑在分布式环境下处理。如果确实需要拉回一些小结果集也要估算它的大小并把spark.driver.memory调到一个安全水位。Driver 内存不是越大越好但至少不能让它成为任务的瓶颈点。5.3 频繁 Full GC 与 GC 时间过长GC 问题很难一眼定位但可以从监控曲线里看出端倪。Spark UI 的 Executor 页签能看到每个 Executor 的 GC Time如果 GC 总时长占运行时长比例过高说明堆内存使用压力大或者代码里创建了大量生命周期长的对象。先用jstat或者 YARN 的监控面板看老年代占用如果老年代持续上升大概率是缓存、静态集合、或者第三方库在堆里留下了大量对象。处理思路有三条一是调整内存比例给用户代码留更多余地二是优化代码减少对象驻留三是把大对象挪到堆外或者用序列化方式存储。有一回我把一个任务里的map算子中反复创建的 Java 对象改成复用同一个实例GC 时间直接下降了 40%效果立竿见影。所以说面对 GC 问题参数调整只是兜底代码层面的瘦身才是真正的止疼药。5.4 数据倾斜其实是资源分配失衡的放大器数据倾斜不是一个纯粹的参数问题但它会让资源分配问题变得更刺眼。两个表 join 的时候某个 key 占了绝大部分数据少数几个 Task 要处理的数据量是其他 Task 的几十倍。这些 Task 所在 Executor 的内存会被快速打满触发溢写甚至 OOM而其他 Executor 却闲得发慌。处理倾斜要从数据本身下手加盐打散、广播小表、倾斜 key 单独处理这些手段可以在代码层面缓解。但我发现很多人做完这些操作之后忘了回头检查内存参数是否还需要重新调整。因为加盐和单独处理会改变 Shuffle 分布原本给执行内存的预算比例可能就不再合适了。完整流程应该是先定位倾斜 key再优化数据分发方式最后根据新的 Shuffle 特征微调内存分配。5.5 一份我自己的排查顺序清单踩过很多坑之后我整理出这样一套排查步骤。第一步看任务失败时间点和日志报错确认是 OOM、GC 还是被 YARN 杀。第二步打开 Spark UI 或 Flink UI观察 Executor/TaskManager 的内存曲线、GC 时间、溢写数据量。第三步分析输入数据大小和 Shuffle 规模判断是数据量异常还是计算模型异常。第四步检查代码里是否有 collect、缓存、广播变量失控等情况。第五步再回头调整参数一次只改一个变量避免多参数联动无法判断效果。这套清单看着简单但能避免很多无头苍蝇式的调试。有人一看到 OOM 就先把内存调大跑一次还是失败然后又调更大最后节点资源耗尽整个集群被拖垮。与其这样不如先用数据说话把内存的实际使用情况摸清楚再做有针对性的调整。6. 一点后话做资源分配优化这些年我自己最大的体会是内存计算框架的调优不是把参数堆到最大值而是理解内存模型之后做一套合理的预算分配。每次接到新的任务我会先确认它的数据规模、计算特征、Shuffle 程度、状态大小然后才动参数。跑完一遍之后再用监控数据验证之前的假设而不是让任务蒙着眼跑下去。最后再分享一个小技巧如果你负责的集群上有很多不同类型的作业建议给它们分别建立内存参数模板比如离线大 Shuffle 模板、迭代计算模板、实时流处理模板。切换模板比每次手动改配置要省心得多也能减少因为参数混用导致的隐性故障。内存计算这块门道不算浅但只要你把内存模型和任务特征这条主线抓住大部分资源分配问题都能顺藤摸瓜解决。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑