我接触Spark这几年,印象最深的一次排查经历,是一个跑得好好的ETL作业突然卡了大半天,所有任务状态都显示“等待”,Spark UI里节点列表一片飘红。折腾了许久才定位到,问题出在Scheduler和BlockManager之间的元数据会话上。从那以后我看Spark作业的角度就不一样了:以前只关心算子写得对不对,后来更关心调度器和块管理器到底是怎样配合的。今天这篇,我就把Scheduler与BlockManager的交互流程完整拆开,从阶段划分、任务下发、数据定位,到Shuffle读写链路上的底层细节,全部过一遍。适合正在做Spark应用开发、搞数据平台运维,或者想对分布式计算框架做二次扩展的工程师,看完你至少能明白一个任务从提交到结果返回,走的每一步到底是谁在帮谁。
1. 两个组件到底管到什么边界:调优前必须厘清的职责
很多人一开始对Scheduler和BlockManager的理解是割裂的,一个管“任务怎么排”,一个管“数据存哪”。这个认知不算错,但如果只停留在这一层,你会很难解释很多Spark的诡异现象,比如:为什么任务总是调度到没有数据的节点?为什么ShuffleRead偶尔卡到令人抓狂?为什么同一个作业换个executor数量,性能天差地别?
这些问题背后,本质上是两个角色在流程上的深度耦合。所以先别急看代码,我们先把职责边界划清楚。
1.1 调度器内部其实还有两层
Scheduler这个名字,严格来说应该拆成两层:DAGScheduler和TaskScheduler。DAGScheduler面向的是Job级调度,它把RDD的依赖关系图切割成一串Stage,每个Stage内部生成一批任务,这批任务会交给TaskScheduler。TaskScheduler面向的是Task级调度,它负责把这些任务真正丢到某个Executor上执行。
在整个调度链路里,DAGScheduler关心的是“这个任务需要的数据在哪”,TaskScheduler关心的是“哪个Executor适合执行这个任务”。后者往往是前者的执行结果。也就是说,DAGScheduler基于数据分布做出候选节点列表,TaskScheduler根据Executor的资源情况、可用性、白名单等做最终裁决。
这套分工直接影响了你看到的现象:如果你在Spark UI里看到某个Stage有任务一直处于“调度等待”状态,不一定就是资源不足,更可能是DAGScheduler给出了一个“首选位置列表”,但TaskScheduler发现这些节点已经不可用或没有资源,于是只能退而求其次等待或换节点。
1.2 BlockManager不是简单缓存
BlockManager是Spark存储层的核心组件,但它远不止“缓存”这么简单。每个Executor上都会有一个BlockManager实例,负责该Executor进程内的数据块写入、读取、复制和淘汰。Driver端还有一个BlockManagerMaster,专门维护所有Executor的Block元数据信息,包括哪个Block在哪个Executor上、块大小、存储级别、最后更新时间等。
很多人在定位OOM问题时只盯住堆内存,容易忽略BlockManager在其中的角色。BlockManager本身管理的不只是RDD缓存数据,还包括Shuffle过程中的中间文件、广播变量、任务结果数据等。它底层可能用内存、磁盘,也可能两者都用。关键点在于,BlockManager是个跨组件的数据中枢,而不是简单的“cache层”。
1.3 为什么交互如此关键
Scheduler要做出合理的调度决策,就必须知道数据在哪;而数据在哪这件事,只有BlockManager最清楚。所以Scheduler在生成任务阶段会有意识地调用BlockManagerMaster来查询依赖数据的位置,再结合RDD本身的优先位置定义,产出最终的本地位信息。
反过来,BlockManager也依赖Scheduler把任务调度到对的位置,否则它辛辛苦苦缓存好的块就没法被“就近消费”。如果任务被调度到了离数据很远的节点,一次Fetch就是一次网络跨机架甚至跨机房传输,成本极大。
所以这两者是一条绳子上的两个蚂蚱:Scheduler拿BlockManager的数据分布信息做决策,BlockManager靠Scheduler把计算推到数据旁边去。你说到底是调度决定存储,还是存储决定调度?我觉得这是一个螺旋上升的耦合关系,谁先下手,取决于当前阶段的执行状态和触发源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 任务从DAG变成可执行单元:Scheduler主动问BlockManager要位置的完整过程
2.1 RDD分区如何映射到Block
在Spark里,RDD的每个分区并不是直接对应一个物理数据块。但有趣的是,Spark内部很多模块确实会以“Block”的概念来衡量数据位置。比如HadoopRDD的每个分区会对应一个InputSplit,这个Split至少包含一个HDFS Block的位置列表。而在CacheManager里,RDD分区一旦被缓存,就会以“rdd_分区ID”的形式注册成BlockManager中的Block。
这层映射关系非常关键,因为Scheduler的分区本地性判断,本质上是在问:“这个RDD分区如果在BlockManager里有缓存,缓存块在哪些Executor节点?”如果再往下钻一层,即
