水位线与状态管理:流处理的时间语义
上一篇解决了 Structured Streaming 的基本模型。这一篇进入时间语义——事件时间、水位线和状态管理。
流处理中的"时间"不是一个简单的概念。一条消息有两个时间戳:事件时间(Event Time,事件实际发生的时间)和处理时间(Processing Time,Spark 收到消息的时间)。两者可能差距很大——网络延迟、消费积压、跨时区传输都会导致消息乱序到达。
本文只抓一个问题:Watermark 如何告诉 Spark “多晚的数据还值得等”,以及 State Store 如何持久化窗口状态。
事件时间 vs 处理时间
1 | |
Structured Streaming 通过 withWatermark 声明事件时间列和允许的最大迟到时间:
1 | |
Watermark 的推进机制
Watermark 的定义:当前所有已处理数据中最大的事件时间减去允许的迟到阈值。
1 | |
Watermark 只会向前推进,永远不会倒退。即使某个 batch 的数据事件时间很早,Watermark 也不会降低。这保证了已关闭的窗口不会被重新打开。
Watermark 的阈值是一个权衡:阈值越大,能容纳的迟到数据越多,但窗口关闭越慢、状态保留越久、内存消耗越大。阈值越小,内存占用小、延迟低,但更多迟到数据会被丢弃。
State Store:状态的持久化
有状态操作(聚合、去重、session 窗口)需要在 batch 之间保留中间状态。Structured Streaming 使用 State Store 来管理这些状态。
1 | |
默认的 State Store 实现是 HDFSBackedStateStore:状态以 HashMap 形式存在 Executor 内存中,每个 batch 结束后把增量变更(delta)写入 checkpoint 目录。每隔 N 个 batch 写一次全量快照,防止 delta 文件累积过多。
故障恢复时,State Store 从最近的快照加上后续的 delta 文件重建状态。这个过程可能需要几秒到几十秒,取决于状态大小和 delta 数量。
Watermark 的另一个作用是状态清理:当 Watermark 推进超过某个窗口的结束时间时,该窗口的状态可以安全清除。没有 Watermark 的聚合操作,状态会无限增长。
窗口类型
Structured Streaming 支持三种窗口:
1 | |
实验:观察 Watermark 行为
1 | |
在 Spark UI 的 Structured Streaming 页面可以看到 “Aggregation State” 指标,显示 State Store 中的 key 数量。随着 Watermark 推进,过期窗口被清理,key 数量保持稳定而不是无限增长。
模式提炼
Watermark 体现了延迟-完整性权衡的通用模式:系统必须在"等待更多数据以获得更准确的结果"和"尽快输出结果以降低延迟"之间做选择。Watermark 是这个权衡的显式参数化。
同样的权衡在分布式系统中反复出现。Google Cloud Dataflow 的 Watermark 机制是 Structured Streaming 的直接灵感来源。数据库的 MVCC 读取需要决定"读快照的时间戳"——等太久影响延迟,快照太旧可能读到不一致的数据。分布式一致性中的"最终一致"也是同一个权衡:允许临时不一致来换取更低的延迟。
工程迁移表
| Spark 概念 | Flink 对应 | Kafka Streams 对应 | 数据库对应 |
|---|---|---|---|
| Watermark | Watermark(相同概念) | Stream Time | MVCC 读快照时间戳 |
| 事件时间窗口 | EventTime Window | TimeWindows | 时间范围分区 |
| State Store | Keyed State (RocksDB) | State Store (RocksDB/InMemory) | 物化视图状态 |
| Checkpoint | Checkpoint(Chandy-Lamport) | Changelog + Standby | 数据库检查点 |
| 迟到数据丢弃 | Late element 侧输出 | Grace period | 无直接对应 |
常见误解
误解一:“Watermark 保证不丢数据”。Watermark 定义了一个截止时间,在此之后到达的数据会被丢弃。这是设计如此,不是 bug。如果业务不能容忍丢弃,需要加大 Watermark 阈值(代价是更高的延迟和更大的状态)。
误解二:“没有 Watermark 的聚合查询可以正常运行”。可以运行,但状态会无限增长。每个新 key 的状态永远不会被清理,最终耗尽 Executor 内存。生产环境中有状态查询必须配置 Watermark。
误解三:“State Store 的状态存在 HDFS 上”。状态的工作副本在 Executor 内存中(HashMap),HDFS 上只存检查点(快照 + delta)。正常执行时状态读写不涉及 HDFS I/O,只有 batch 结束时的检查点写入和故障恢复时的状态加载需要访问 HDFS。
练习
-
对同一个流式查询分别设置 Watermark 阈值为 10 秒和 10 分钟,观察 State Store 中的 key 数量变化趋势。Watermark 越大,峰值 key 数量越多还是越少?
-
构造一个 session 窗口查询(Spark 3.2+),用 Rate Source 生成数据。在某个时刻暂停输入 30 秒再恢复,观察 session 窗口是否在暂停期间关闭。
-
查看 Checkpoint 目录的 state/ 子目录,找到快照文件和 delta 文件。统计 10 个 batch 后的文件数量。修改 spark.sql.streaming.stateStore.maintenanceInterval 后再观察。
系列导航
| 序号 | 主题 | 状态 |
|---|---|---|
| 00 | 导读:为什么 Spark 的核心是一张 DAG | 已完成 |
| 01 | RDD:弹性分布式数据集的五大属性 | 已完成 |
| 02 | 宽依赖与窄依赖:Stage 是怎样划分出来的 | 已完成 |
| 03 | DAG Scheduler 与 Task Scheduler:从逻辑计划到物理执行 | 已完成 |
| 04 | Shuffle 机制:数据跨分区交换的代价与优化 | 已完成 |
| 05 | 内存管理与 Tungsten:堆外内存、序列化与代码生成 | 已完成 |
| 06 | 存储体系:Block Manager、广播变量与累加器 | 已完成 |
| 07 | 容错机制:Lineage、Checkpoint 与推测执行 | 已完成 |
| 08 | Spark SQL 与 Catalyst:从 SQL 文本到物理算子树 | 已完成 |
| 09 | DataFrame 与 Dataset:类型安全与性能的折中 | 已完成 |
| 10 | Adaptive Query Execution:运行时改写执行计划 | 已完成 |
| 11 | 数据源 API 与谓词下推:让存储层帮忙过滤 | 已完成 |
| 12 | Structured Streaming:把流当成无界表 | 已完成 |
| 13 | 水位线与状态管理:流处理的时间语义 | 本篇 |
| 14 | Spark on YARN / K8s:资源管理与部署模式 | 下一篇 |
| 15 | 动态资源分配与作业调度:多租户集群的资源博弈 | |
| 16 | 性能诊断:Spark UI、Event Log 与调优思路 | |
| 17 | Spark 与 Flink:两种计算引擎的设计选择 |
参考资料
- Apache Spark 官方文档:Handling Late Data and Watermarking. https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#handling-late-data-and-watermarking
- Tyler Akidau et al. The Dataflow Model: A Practical Approach to Balancing Correctness, Latency, and Cost in Massive-Scale, Unbounded, Out-of-Order Data Processing. VLDB 2015.
- Tyler Akidau. Streaming 101 and Streaming 102. O’Reilly blog series.
- Apache Spark 源码:
sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/https://github.com/apache/spark
