Spark Core 是整个 Spark 生态的基石,SQL、Streaming、MLlib、GraphX 都构建在它之上。本篇把 Spark Core 的核心概念串成一条线:为什么需要 Spark → RDD 是什么 → 怎么算 → 怎么容错 → 集群怎么跑起来。一天搞定核心原理。
一、为什么需要 Spark
MapReduce 是第一代分布式计算框架,但有两个硬伤:
- 计算结果必须落盘:Map 输出写本地磁盘,Reduce 再读,I/O 成瓶颈;
- 不适合迭代计算:机器学习、图计算要反复迭代,每次都读写磁盘,慢得无法接受。
Spark 的解法:
- 把数据放在内存中迭代计算;
- 用 RDD(Resilient Distributed Dataset,弹性分布式数据集) 作为统一数据抽象;
- 用血统(Lineage) 而非副本实现容错。
Spark 生态:
Spark Core(RDD、调度、内存管理)
├── Spark SQL 结构化数据处理
├── Spark Streaming 流式计算
├── MLlib 机器学习
└── GraphX 图计算
二、RDD:Spark 的核心数据抽象
RDD 是一个不可变的、分区的、容错的分布式数据集。
RDD 的五大核心属性
1. partitions 分区列表:数据被切成多少份
2. compute(p, split) 计算函数:每个分区怎么算
3. dependencies 依赖列表:由哪些父 RDD 转换而来
4. partitioner 分区器:可选,按什么规则分区
5. preferredLocations 首选位置:可选,每个分区在哪个节点算最快
关键点:RDD 不存数据,只存"怎么算"的描述。真正执行时才从父 RDD 拉数据计算。
RDD 的"弹性"体现在哪
- 自动重算:分区丢失时根据血统重算,不需要副本;
- 自动伸缩 :分区数可动态调整(
repartition、coalesce); - 内存不足溢写:内存不够时自动溢写到磁盘。
三、RDD 的创建与算子
创建方式
scala
// 1. 从集合创建
val rdd1 = sc.parallelize(1 to 100, numSlices = 8)
// 2. 从外部存储创建
val rdd2 = sc.textFile("hdfs://namenode:8020/data.txt")
// 3. 从其他 RDD 转换
val rdd3 = rdd2.flatMap(_.split(" "))
算子分两类
| 类型 | 特点 | 常见算子 |
|---|---|---|
| Transformation(转换) | 返回新 RDD,惰性执行 | map、flatMap、filter、reduceByKey、groupByKey、join、distinct |
| Action(行动) | 触发计算,返回结果 | collect、count、first、take、reduce、foreach、saveAsTextFile |
关键规则:只有遇到 Action 才真正执行计算。 之前的 Transformation 只是记录计算逻辑。
四、惰性执行与依赖
惰性执行(Lazy Evaluation)
scala
val rdd1 = sc.textFile("data.txt") // 不执行
val rdd2 = rdd1.flatMap(_.split(" ")) // 不执行
val rdd3 = rdd2.map((_, 1)) // 不执行
val rdd4 = rdd3.reduceByKey(_ + _) // 不执行
rdd4.collect() // 触发执行!
好处:可以全局优化(如合并多个 map)、避免中间结果落盘、按需计算。
窄依赖 vs 宽依赖
这是 RDD 最核心的概念,决定了 Stage 的切分。
| 类型 | 定义 | 特点 | 例子 |
|---|---|---|---|
| 窄依赖 | 父分区最多被一个子分区依赖 | 无需 Shuffle,可流水线执行 | map、filter、union |
| 宽依赖 | 父分区被多个子分区依赖 | 需要 Shuffle,必须等父 Stage 完成 | groupByKey、reduceByKey、join、distinct |
Shuffle 原理
宽依赖触发 Shuffle,分两阶段:
- Shuffle Write:Map 端按分区器把数据写到本地磁盘的多个 bucket 文件;
- Shuffle Read:Reduce 端从各 Map 节点拉取属于自己的 bucket,合并后聚合。
Shuffle 涉及磁盘 I/O、网络传输、序列化,是性能瓶颈。优化方向:能不 Shuffle 就不 Shuffle,必须 Shuffle 时合理设置分区数。
五、容错机制:血统 + 缓存
血统(Lineage)
RDD 记录自己由哪些父 RDD、经过什么算子转换而来。分区丢失时,Spark 根据血统从最近的父 RDD 重算丢失的分区,不需要重算全部。
Cache / Persist
被多次使用的 RDD(如迭代计算)应该缓存,避免每次重算。
scala
val rdd = sc.textFile("data.txt").flatMap(_.split(" ")).cache()
rdd.count() // 第一次:计算并缓存
rdd.collect() // 第二次:直接读缓存
存储级别:
| 级别 | 说明 |
|---|---|
MEMORY_ONLY |
只存内存(默认) |
MEMORY_AND_DISK |
内存不够溢写磁盘 |
MEMORY_ONLY_SER |
序列化存内存,省空间 |
*_2 |
存两份副本 |
经验 :只缓存多次使用的 RDD;优先 MEMORY_ONLY;用完 unpersist() 释放。
Checkpoint
血统太长时重算代价大,用 Checkpoint 把 RDD 写到 HDFS,切断血统。
scala
sc.setCheckpointDir("hdfs://namenode:8020/ck")
rdd.checkpoint()
rdd.count() // 触发 checkpoint
六、运行架构
Spark 采用 Master-Worker 架构,核心角色:
| 角色 | 职责 |
|---|---|
| Driver | 运行 main,创建 SparkContext,构建 DAG,切分 Stage,调度 Task |
| Master | 管理 Worker,为 Application 分配资源(Standalone 模式) |
| Worker | 管理本节点资源,启动 Executor,向 Master 汇报 |
| Executor | 执行 Task,管理内存和磁盘,缓存 RDD |
核心概念层级
Application(一次 spark-submit)
└── Job(一个 Action 触发一个)
└── Stage(按宽依赖切分)
└── Task(一个分区对应一个,最小执行单元)
Task 数 = 分区数,同一 Stage 内的 Task 并行执行。
DAG 调度
- DAGScheduler:把 Job 按宽依赖切分成多个 Stage,生成 TaskSet 提交给 TaskScheduler;
- TaskScheduler :把 Task 按数据本地性分配给 Executor 执行;
- 数据本地性级别 :
PROCESS_LOCAL>NODE_LOCAL>RACK_LOCAL>ANY,优先调度到离数据最近的节点。
部署模式
| 模式 | Driver 位置 | 适用场景 |
|---|---|---|
| Local | 本地 JVM | 开发调试 |
| Standalone | 提交端 | Spark 专用集群 |
| YARN Client | 提交端 | 调试、需看控制台 |
| YARN Cluster | AM 内部(集群内) | 生产环境推荐 |
| Kubernetes | Pod 内 | 容器化环境 |
七、任务提交流程(YARN Cluster 模式)
1. spark-submit 向 YARN RM 提交应用
2. RM 在某个 NM 上启动 ApplicationMaster(AM)
3. AM 内部启动 Driver,运行 main,创建 SparkContext
4. Driver 向 RM 申请 Container 资源
5. NM 在 Container 中启动 Executor
6. Executor 向 Driver 注册
7. 遇到 Action → DAGScheduler 切 Stage → TaskScheduler 下发 Task
8. Executor 执行 Task,ShuffleMapTask 写磁盘,ResultTask 返回结果
9. Driver 回收结果,所有 Job 完成后 SparkContext.stop(),释放资源
八、Spark Core 核心参数
| 参数 | 默认值 | 说明 |
|---|---|---|
spark.executor.memory |
1g | 每个 Executor 内存 |
spark.executor.cores |
1 | 每个 Executor 核数 |
spark.executor.instances |
- | Executor 数量 |
spark.driver.memory |
1g | Driver 内存 |
spark.default.parallelism |
- | RDD 默认并行度 |
spark.task.maxFailures |
4 | Task 最大失败次数 |
spark.speculation |
false | 推测执行 |
spark.locality.wait |
3s | 数据本地性等待时间 |
spark.driver.maxResultSize |
1g | Driver 回收结果上限 |
九、典型坑点
坑点 1:map 里的代码不执行
map 是 Transformation,惰性执行。要用 foreach 才会触发。
坑点 2:groupByKey 导致 OOM
groupByKey 把同一 Key 的所有值拉到一个分区,数据量大时 OOM。优先用 reduceByKey(Map 端先聚合,减少 Shuffle 量)。
坑点 3:外部大变量导致每个 Task 序列化一份
用广播变量:
scala
val bc = sc.broadcast(largeMap)
rdd.map(x => bc.value.get(x))
坑点 4:collect() 拉取海量数据导致 Driver OOM
数据量大时用 take(n)、saveAsTextFile 或 foreach,不要 collect 全量。
坑点 5:repartition 与 coalesce 混用
repartition:触发 Shuffle,可增可减分区;coalesce:默认不 Shuffle,只能减分区。
减少分区优先用 coalesce。
坑点 6:YARN 上 Executor 被 kill
Container killed by YARN for exceeding memory limits:Executor 实际内存超过 executor.memory + memoryOverhead。调大 spark.yarn.executor.memoryOverhead 或减小 executor 内存。
十、本篇小结
Spark Core 的核心是这几条线:
- 数据抽象:RDD 不存数据,只存「分区 + 计算函数 + 依赖 + 分区器 + 首选位置」;
- 执行模型:Transformation 惰性记录,Action 触发计算,窄依赖流水线,宽依赖 Shuffle;
- 容错:血统重算 + Cache/Checkpoint 优化;
- 架构:Driver 调度,Executor 执行,Master/Worker 管资源;
- 调度:DAGScheduler 切 Stage,TaskScheduler 按数据本地性分配 Task;
- 流程:spark-submit → 启动 Driver → 申请 Executor → 切 Stage → 执行 Task → 回收结果。
把这些打通,后续看 SQL、MLlib 都是在 Core 之上的封装。