状态、水印 、checkpoint
一、状态
Spark 的状态 = 计算过程中需要持久化的中间结果 / 历史数据
状态分为三类:
① 轻量级状态:
分区级聚合,状态与分区强绑定,仅存储在当前分区内,Spark 全自动托管, 无需设置 Checkpoint、TTL,无需手动管理存储,默认内存优先存储,当单个分区的状态数据过大时,Spark4.x 会自动触发「磁盘溢出」,无需手动配置;
比如:
批处理:df.groupBy("id").agg(sum("value"), avg("score"))
流式微批:streamDF.groupBy("user_id").count()
② 重量级状态:
1.状态全局共享,可跨分区、跨节点、跨批次访问;
2.强制配置:必须显式配置 Checkpoint,否则直接运行报错;
3.必配 TTL 防止状态无限膨胀,支持动态修改无需重启任务;
主要使用applyInPandasWithState类,
配置「Checkpoint」:实现状态的持久化和故障恢复,防止状态丢失;
配置「TTL」:一般使用GroupStateTimeout,它的过期时间强行和水位线一样,水位线过期了那么就开始清理状态
https://www.doubao.com/chat/collection/34538004818812930?type=Thread
一、spark系统崩溃以后,重启时需要记得:
任务配置必须一致:重启时spark-submit的参数(如 Executor 数量、内存)、代码逻辑、检查点路径必须和崩溃前完全一致,否则会报 “检查点不兼容”;
正确代码:加checkpointLocation
query = parsed_df.writeStream
.foreachBatch(write_to_mysql)
.outputMode("append")
.option("checkpointLocation", "file:///tmp/spark_checkpoint") # 检查点记进度
.start()
第一步:处理 1-10 条数据,写入 MySQL 后,检查点会记录 “Kafka 偏移量到 10,这批数据已处理”;
第二步:崩溃重启后,Spark 读取检查点→知道 “已经处理到 10,该处理 11-20 条”→不会重复写入 1-10 条。
数据库写入支持幂等性
def write_to_mysql(batch_df, batch_id):
# 用INSERT ... ON DUPLICATE KEY UPDATE:重复则覆盖,不新增
batch_df.write.format("jdbc")
.option("url", "jdbc:mysql://localhost:3306/test")
.option("dbtable", "user_orders")
.option("user", "root")
.option("password", "123456")
.option("sql", """
INSERT INTO user_orders (order_id, user_id, amount, order_time)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE -- 幂等核心:重复则更新,不新增
user_id=VALUES(user_id),
amount=VALUES(amount),
order_time=VALUES(order_time)
""")
.mode("append")
.save()
输出模式:
append,upate,complete
append,一个窗口期内,每个输出新增不可变的数据,比如日志收集,不可用于聚合计算类数据输出,因为如果聚合类
update,一个窗口期内,每次输出变化的数据,比如聚合类订单金额总数,一个窗口10秒钟,10秒内金额总数不断增长
complete,一个窗口期内,全量输出数据,这种适合排名前十位数据显示,
二、水印
核心作用:清理过期窗口状态,防止 OOM,是窗口聚合的 “标配”;
配置口诀:先加水印,后聚合;水印列 = 窗口列;时长 = 窗口 + 乱序缓冲;
平衡原则:水印时长不是越大越好,也不是越小越好 —— 刚好覆盖 “最大乱序时间” 即可(生产环境通常 5-15 分钟)。
水印的核心要求是:必须加在所有 “有状态操作” 之前,示例如下:
order_watermark_df = parsed_order_df.withWatermark("create_time", "15 minutes") # 水印核心行
4. Spark SQL:清洗+去重+聚合(仅核心逻辑)
order_watermark_df.createOrReplaceTempView("tmp_order")
清洗(过滤取消单)+ 去重(基于order_id)+ 关联商品品类(Hive极简维度表)
clean_sql = """
SELECT DISTINCT o.order_id, o.province, o.pay_amount, o.create_time, g.category_name
FROM tmp_order o
LEFT JOIN (SELECT goods_id, category_name FROM dwd.dim_goods_core WHERE is_valid=1) g
ON o.goods_id = g.goods_id
WHERE o.order_status != 'CANCEL' AND o.pay_amount > 0
"""
clean_order_df = spark.sql(clean_sql)
注意:parsed_order_df先加水印,再运行spark sql
迟到数据
迟到数据是指超过水印时间的数据,采用离线兜底策略,Spark 默认会直接丢弃迟到数据,但可以通过侧输出流把这些数据单独捕获,写入专门的 “迟到数据表”,给数据流标记 “迟到数据标签”,Spark 会把超过水印的订单分流到侧输出流,主流程仍按正常逻辑处理,侧输出流单独存储迟到数据,迟到的数据在第二天凌晨再计算一次,计算完以后将数据更新到
三、checkpoint
Checkpoint 是 Spark Structured Streaming 中,将流式任务的核心运行元数据 + 持久化状态数据,定期序列化写入分布式存储(HDFS/OSS/S3)的核心容错机制。Spark4.x 中,状态管理与 Checkpoint 是强依赖关系 —— 无 Checkpoint,所有持久化状态都是「内存级临时数据」
阶段 1:任务初始化阶段(首次启动 / 无历史 Checkpoint)
若 Checkpoint 目录为空 → Spark4.x 从头消费数据源(如 kafka 的 earliest offset),初始化内存状态,同时启动「增量写入」机制。
阶段 2:任务运行阶段(增量写入 + 定期快照,核心核心)
Spark4.x 默认开启「增量 Checkpoint」:仅将本次批次更新的状态数据、变更的元数据写入 Checkpoint,而非全量覆盖;对比全量写入,IO 开销降低 80%+,性能提升显著。
写入触发时机:由配置的 checkpointInterval 控制(默认 10 分钟),到达时间阈值后,异步执行写入操作,
阶段 3:故障重启 / 断点续算阶段
任务重启时,Spark4.x 会优先读取指定的 Checkpoint 目录,加载元数据 + 状态快照 + 消费 offset;
恢复规则:① 消费 offset 恢复到故障前的位置,继续消费新数据;② 所有状态数据恢复到故障前的累计值,无丢失、无重复;③ 水印阈值、触发器配置完全恢复,业务逻辑无感知。
风险 5:状态数据不一致 → 统计结果失真
现象
任务重启后,状态数据恢复成功,但统计的累计值(如销售额、订单数)与故障前不一致,出现少算 / 多算的情况。
原因
任务被强制 kill(如kill -9),导致最后一批次的状态数据未写入 Checkpoint;
WAL 预写日志被手动关闭(spark.sql.streaming.wal.enabled=false),状态更新无原子性保障。
解决方案
优雅停止任务:使用spark.stop()或集群的优雅停止命令,禁止强制 kill;
Spark4.x 中 WAL 默认开启,绝对禁止手动关闭,这是状态原子性的最后一道保障。
浙公网安备 33010602011771号