水位线与状态管理:流处理的时间语义
上一篇解决了 Structured Streaming 的基本模型。这一篇进入时间语义——事件时间、水位线和状态管理。 流处理中的"时间"不是一个简单的概念。一条消息有两个时间戳:事件时间(Event Time,事件实际发生的时间)和处理时间(Processing Time,Spark 收到消息的时间)。两者可能差距很大——网络延迟、消费积压、跨时区传输都会导致消息乱序到达。 本文只抓一个问题:Watermark 如何告诉 Spark “多晚的数据还值得等”,以及 State Store 如何持久化窗口状态。 事件时间 vs 处理时间 123456789101112事件发生 网络传输 Spark 收到 t=10:00:01 ~~~延迟 3s~~~ t=10:00:04 (Processing Time) t=10:00:02 ~~~延迟 1s~~~ t=10:00:03 t=10:00:03 ~~~延迟 8s...
Structured Streaming:把流当成无界表
上一篇解决了数据源 API 和谓词下推。这一篇进入流处理——Structured Streaming。 Spark 的流处理经历了两代:DStream(基于 RDD 的离散化流)和 Structured Streaming(基于 DataFrame/Dataset 的结构化流)。DStream 已经停止演进,Structured Streaming 是当前唯一推荐的流处理 API。 Structured Streaming 的核心模型可以用一句话概括:把流数据看成一张不断追加新行的无界表,复用 Spark SQL 的 Catalyst 优化器和 Tungsten 执行引擎来处理。 本文只抓一个问题:无界表模型如何工作,以及 MicroBatch 执行引擎如何把连续的流切成离散的批次。 无界表模型 12345678传统流处理的心智模型: 消息 → 处理函数 → 输出 (一条一条处理,每条消息触发一次计算)Structured Streaming 的心智模型: 数据源不断追加行到一张"输入表" 查询在整张表上持续运行 结果写入一张"输出表&qu...
数据源 API 与谓词下推:让存储层帮忙过滤
上一篇解决了 AQE 的运行时优化。这一篇进入数据源层——Spark 如何把计算推到存储层执行。 查询优化不只发生在执行引擎内部。如果存储层能在读取数据时就过滤掉不需要的行和列,引擎需要处理的数据量会大幅减少。Spark 通过 DataSource API 和 Catalyst 的优化规则把这个能力标准化了:谓词下推(Predicate Pushdown)让存储层只返回满足条件的行,列裁剪(Column Pruning)让存储层只返回查询需要的列。 本文只抓一个问题:谓词下推和列裁剪如何从 Catalyst 的逻辑计划传递到数据源实现。 DataSource API 的两代演进 Spark 的数据源 API 经历了两代: 12345678910DataSource V1 (Spark 1.3+): 接口: InputFormat / OutputFormat + createRelation 谓词下推: 通过 PrunedFilteredScan trait 可选实现 问题: API 不够灵活,不支持流式读写,难以做细粒度优化DataSource V2 (Spark 2.3...
Adaptive Query Execution:运行时改写执行计划
上一篇解决了 DataFrame 和 Dataset 的 API 演进。这一篇进入运行时优化——Adaptive Query Execution(AQE)。 传统查询优化器的一个根本问题是:优化发生在执行之前,依赖的统计信息可能不准确或完全缺失。AQE 把优化推迟到执行过程中——在每个 Shuffle 边界,收集实际的数据统计信息,然后用这些精确的统计重新优化后续 Stage 的执行计划。 本文只抓一个问题:AQE 的三个核心优化分别解决什么问题,以及运行时优化的反馈循环如何工作。 运行时反馈循环 AQE 的工作方式可以用一句话概括:执行一个 Stage → 收集 Shuffle 输出的统计信息 → 用统计信息重新优化下一个 Stage 的计划 → 执行下一个 Stage。 123456789101112传统执行: 编译期确定完整计划 → Stage 0 → Stage 1 → Stage 2 (统计信息可能不准,计划一旦确定不可修改)AQE 执行: 编译期确定初始计划 → 执行 Stage 0 → 收集 Stage 0 的 Shuffle 输出统计 →...
DataFrame 与 Dataset:类型安全与性能的折中
上一篇解决了 Catalyst 优化器的四阶段流水线。这一篇进入用户 API 层——RDD、DataFrame、Dataset 三代 API 的演进逻辑。 三者的区别容易被简化为"RDD 是低级 API,DataFrame 是高级 API"。更准确的说法是:三代 API 在类型安全和执行优化之间做了不同的取舍。RDD 完全类型安全但无法被 Catalyst 优化;DataFrame 完全被 Catalyst 优化但放弃了编译期类型检查;Dataset 试图兼顾两者,但付出了 Encoder 序列化/反序列化的代价。 本文只抓一个问题:三代 API 在内部表示、优化路径和序列化机制上的具体差异。 三代 API 的内部表示 1234567891011121314RDD[Person] └─ 内部存储: JVM 堆上的 Java/Scala 对象 └─ 优化路径: 无(用户代码是黑盒,Spark 无法查看函数内部) └─ 类型信息: 编译期完整保留(泛型参数 T = Person)DataFrame (= Dataset[Row]) └─ 内部存储: Uns...
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...





