Spark SQL 与 Catalyst:从 SQL 文本到物理算子树
上一篇解决了 Spark 的容错机制。这一篇进入 SQL 引擎的核心——Catalyst 优化器。 Spark SQL 不只是"在 Spark 上跑 SQL"。它是一个完整的查询编译器,把 SQL 文本或 DataFrame API 调用翻译成优化后的物理执行计划。这个翻译过程由 Catalyst 优化器驱动,经过四个阶段:解析、分析、优化、物理计划生成。 本文只抓一个问题:一条 SQL 从文本到可执行的物理算子树,中间经历了什么变换。 下图展示了 Catalyst 四阶段流水线和一条 SQL 在各阶段的变换过程: 四阶段流水线 12345678910111213141516171819202122232425SQL 文本 / DataFrame API │ ▼┌─────────────────┐│ ① Parsing │ SQL 文本 → Unresolved Logical Plan│ (ANTLR 解析器) │ 列名和表名尚未绑定到实际 schema└────────┬────────┘ │ ...
容错机制:Lineage、Checkpoint 与推测执行
上一篇解决了 BlockManager 的存储管理。这一篇进入容错机制——Spark 如何在节点故障时恢复计算。 分布式系统的容错通常有两条路线:数据副本(每份数据存多份,一份丢了从副本恢复)和计算重放(不存副本,丢了从头重算)。Spark 选择了第二条路线——通过 RDD 的 Lineage(血统)记录计算路径,丢失的分区沿着 Lineage 重算。 本文只抓一个问题:Lineage 容错的优势和代价,以及 Checkpoint 和推测执行如何补充 Lineage 的不足。 下图对比了无 Checkpoint 时的全链重算和有 Checkpoint 时的截断恢复: Lineage 容错原理 每个 RDD 记录了自己的 dependencies——从哪些父 RDD 经过什么变换得来。当一个分区丢失时(Executor 故障、磁盘损坏),Spark 不需要从副本恢复,只需要找到该分区的父 RDD 分区,重新执行 compute 函数。 12345678910RDD-0 (HDFS) → RDD-1 (map) → RDD-2 (filter) → RDD-3 (reduceByK...
存储体系:Block Manager、广播变量与累加器
上一篇解决了 Tungsten 的内存管理和代码生成优化。这一篇进入存储体系——Spark 如何管理分布在集群中的数据块。 BlockManager 容易被忽略,因为用户代码很少直接和它打交道。但 RDD 的 persist/cache、Shuffle 的中间文件、广播变量的分发、累加器的聚合,底层全部通过 BlockManager 完成。它是 Spark 存储层的统一入口。 本文只抓一个问题:BlockManager 如何在内存和磁盘之间管理数据块,以及广播变量和累加器在这个体系中的位置。 BlockManager 架构 每个 Executor 上运行一个 BlockManager 实例,Driver 上也有一个(主要用于接收广播变量和累加器结果)。BlockManager 内部包含三个核心组件: 1234BlockManager├── MemoryStore 内存存储(堆内/堆外)├── DiskStore 磁盘存储(本地目录)└── BlockTransferService 网络传输(Netty) MemoryStore 负责在 JVM 堆内或堆外内存...
内存管理与 Tungsten:堆外内存、序列化与代码生成
上一篇解决了 Shuffle 的物理机制。这一篇进入内存管理——Spark 如何突破 JVM 的内存瓶颈。 Spark 是一个 JVM 应用,天然受制于 Java 的内存模型:对象头开销大、GC 停顿不可控、序列化效率低。Project Tungsten 是 Spark 为解决这三个问题发起的底层优化计划,它从三条线同时推进——堆外内存管理、二进制数据格式、全阶段代码生成。 本文只抓一个问题:Tungsten 的三条优化线分别解决了什么问题,以及 Spark 的统一内存管理模型如何在执行内存和存储内存之间做动态调配。 下图展示了 Executor 的统一内存管理模型——Execution Memory 和 Storage Memory 之间的动态借用机制: Java 对象的内存开销 一个 Java 字符串 “abcd” 在堆内占多少字节? 12345678910111213java.lang.String 对象: 对象头: 12 bytes (64-bit JVM, 压缩指针) hash: 4 bytes (int) value[]: 4 bytes ...
Shuffle 机制:数据跨分区交换的代价与优化
上一篇解决了 DAGScheduler 和 TaskScheduler 的分工。这一篇进入 Shuffle 机制——Stage 之间数据交换的物理过程。 Shuffle 容易被理解成"把数据从一组节点传到另一组节点"。更准确的说法是:Shuffle 是一个分布式的排序-分区-传输流水线,它把上游 Stage 每个分区的输出按 key 的目标分区号排序写入磁盘,然后由下游 Stage 的 Task 跨网络拉取属于自己的那部分数据。 本文只抓一个问题:SortShuffleManager 的 Shuffle Write 和 Shuffle Read 两个阶段各做了什么,以及为什么 Shuffle 是 Spark 作业的头号性能瓶颈。 下图展示了 Shuffle 的完整数据流——从 Map 端排序写盘到 Reduce 端拉取归并: Shuffle 的全局视角 一个 Shuffle 操作(如 reduceByKey)在物理层面涉及两组 Task: 12345678910111213上游 Stage (M 个 ShuffleMapTask) 下游 S...
DAG Scheduler 与 Task Scheduler:从逻辑计划到物理执行
上一篇解决了 Stage 的划分规则——遇到 ShuffleDependency 就切一刀。这一篇进入调度器内部。 Stage 划分出来之后,谁来决定 Stage 的提交顺序?谁来把 Stage 拆成 Task 发给 Executor?Spark 用两层调度器分工完成这件事:DAGScheduler 负责 Stage 级别的依赖分析和提交顺序,TaskScheduler 负责 Task 级别的资源分配和执行调度。 本文只抓一个问题:一个 action 触发之后,从 Job 到 Stage 到 Task 再到 Executor,调度链路上每一步发生了什么。 调度全景 12345678910111213141516171819202122232425用户代码: rdd.count() │ ▼ SparkContext.runJob() │ ▼ DAGScheduler.submitJob() │ 构建 Stage DAG,按依赖顺序提交 ▼ DAGScheduler.submitStage() │ 检查父 S...
宽依赖与窄依赖:Stage 是怎样划分出来的
上一篇拆解了 RDD 的五大属性,其中 dependencies 决定了 RDD 之间的依赖类型。这一篇进入依赖类型的核心区分——宽依赖和窄依赖——以及它如何直接决定 Stage 的划分。 宽依赖和窄依赖容易被理解成"一对一"和"多对多"的分区关系。更准确的说法是:窄依赖意味着子 RDD 的每个分区只依赖父 RDD 的固定少数分区,可以在单个 Task 内完成计算;宽依赖意味着子 RDD 的分区需要读取父 RDD 所有分区的数据,必须等待一次全量数据交换(Shuffle)。 本文只抓一个问题:DAGScheduler 按什么规则把 RDD 依赖图切分成 Stage。 下图展示了 Stage 划分的核心规则——窄依赖 pipeline 执行,宽依赖切出新 Stage: 窄依赖的三种形态 窄依赖(NarrowDependency)的定义是:父 RDD 的每个分区最多被子 RDD 的一个分区使用。在 Spark 源码中,NarrowDependency 有两个具体子类: OneToOneDependency:父子分区一一对应。map、filte...
RDD:弹性分布式数据集的五大属性
上一篇确立了 Spark 的核心是一张 DAG,而 DAG 的节点就是 RDD。这一篇进入 RDD 本身。 RDD 容易被理解成"分布式的数组"或者"分布在多台机器上的数据集合"。更准确的说法是:RDD 是一份计算配方,记录了数据从哪来、经过什么变换、丢失一个分区后怎么重算。 本文只抓一个问题:RDD 的五大属性分别控制了什么,以及这五个属性如何支撑 Spark 的调度和容错。 五大属性总览 Spark 源码中 RDD 的抽象类定义了五个方法,每个方法对应一个核心属性: 123456RDD[T]├── getPartitions: Array[Partition] ← 数据怎么切分├── getDependencies: Seq[Dependency[_]] ← 上游是谁├── compute(split, context): Iterator[T] ← 一个分区怎么算├── partitioner: Option[Partitioner] ← 按什么规则分区└── getPreferredLocations(s...
导读:为什么 Spark 的核心是一张 DAG
Spark 常被介绍为"比 MapReduce 快 100 倍的计算引擎"。这个说法把重点放错了地方。速度差异只是结果,不是原因。真正的区别在于执行模型:MapReduce 是一条固定的 Map-Shuffle-Reduce 三段流水线,而 Spark 是一个可以表达任意有向无环图(DAG)的执行引擎。 本篇只抓一个问题:Spark 的 DAG 执行模型到底和 MapReduce 的线性管道有什么本质区别,以及这个区别如何贯穿整个系列。 MapReduce 的线性管道 MapReduce 的执行模型可以用一句话概括:读数据 → Map → 写磁盘 → Shuffle → 写磁盘 → Reduce → 写数据。 1HDFS → Map Task → Local Disk → Shuffle → Local Disk → Reduce Task → HDFS 这条管道有两个结构性约束: 第一,每个 MapReduce 作业只有一轮 Map 和一轮 Reduce。如果业务逻辑需要多轮聚合(比如先按用户分组,再按地区汇总),就必须把逻辑拆成多个 MapReduce 作...
从规则到统计:人工智能七十年的两条路线之争
人工智能的历史,本质上是一部关于"什么是智能"的争论史。 从 1950 年代至今,这个领域经历了至少两次大规模繁荣和两次寒冬,催生了符号主义、连接主义、统计学习、深度学习、强化学习等多个范式。这些范式的兴衰,表面上看是技术路线的竞争,底层驱动力却是对一个哲学问题的不同回答:智能究竟是"规则的执行"还是"数据的涌现"? 一、两种智能观的源头 1.1 符号主义:智能就是符号操作 1943 年,Warren McCulloch 和 Walter Pitts 发表了一篇论文,用数理逻辑描述神经元的行为。这篇论文同时启发了两个方向——一个方向关注"逻辑"本身,另一个方向关注"神经元网络"本身。前者演化为符号主义(Symbolism),后者演化为连接主义(Connectionism)。 符号主义的哲学根基可以追溯到莱布尼茨。莱布尼茨试图创建一种"通用逻辑演算"来表达所有人类思想,17 世纪的霍布斯则主张推理"不过是计算"。这条线索在 20 世纪中叶被 ...

