异步读取器、精准写入、水位线

一、异步读取器
1.1 背景
在4.x以前只能先读取完数据再计算,在拉取数据源的时候,CPU会持续等待造成资源浪费,
1.2 目标
不让 CPU 等数据、不让内存爆掉,在4.x版本采用多条并行的线程,边拉取边计算不会造成资源浪费,
1.3 解决方案
核心必配配置,可以在spark-defaults.conf配置,也可以通过代码配置
1.3.1 spark.sql.datasource.async.enabled 是否开启异步读取器,默认值:true(Spark 4.x 默认开启)
1.3.2 spark.io.executor.threads 每个 Executor 的异步读取数据IO线程池大小,默认为8,也就是每个Executor都有8个线程同时在读取数据
1.3.3 spark.sql.datasource.async.read.buffer.size
异步读取器的性能提升是分场景的,不是万能的,按收益从高到低排序:
✅ 收益极高(必用):云对象存储(S3/OSS/COS)、远程 JDBC / 数据库、跨机房存储、大文件 Parquet/ORC 读取;
✅ 收益中等:本地磁盘的大文件读取、HDFS 集群读取;
❌ 收益极低(建议关闭):本地小文件读取、内存数据源(比如spark.createDataFrame),此时异步的线程切换开销会大于收益,建议手动关闭异步读取器。

二、精准一次Exactly-Once
1.1 概念
精准一次是指生产端的数据具备持久化和偏移量、写入端的数据源具有事务或幂等功能,这样计算端可以重复计算(为了快速一般会使用checkpoing保持状态),Structured Streaming的精准一次目前只能在微批模式下,连续模式生产不可用,基于批次的原子一致性保障
1.2 链路
精准一次是全链路技术闭环的结果:Kafka幂等读取 + Checkpoint原子快照 + 两阶段提交(2PC) + 外部存储幂等/事务写入,每一个作用如下:
Kafka有偏移量具备幂等读取功能,checkpoingt能原子性保存“数据偏移量和中间状态”,两阶段提交则绑定"结果写入外部存储"和"偏移量/状态快照提交",最后通过外部存储的幂等/事务能力
最终实现「数据源读取 → Spark 计算 → 结果写入」全链路数据不丢、不重。
1.3 前提条件
1.3.1 数据源必须支持「偏移量持久化 + 幂等读取」:比如 Kafka,能记录消费的分区偏移量,重复读取同一个偏移量区间的数据,结果完全一致;如果是本地文件、Socket 这类无状态数据源,永远实现不了精准一次。
1.3.2 结果层必须支持「幂等写入 / 事务写入」
1.4 为什么可以实现精准一次
核心灵魂:两阶段提交(2PC)—— Spark 精准一次的核心保障,这个机制的唯一目的就是:杜绝「数据写入成功但偏移量没更新」或「偏移量更新但数据没写入」的一致性问题,步骤如下:
✔ 第一阶段【预提交 / 准备阶段】:Spark 将本次批次计算好的窗口 GMV 结果,写入到 Doris 的「临时缓冲区 / 临时分区」,此时数据已经落地但并未生效,业务侧无法查询到该部分数据;
✔ 第二阶段【正式提交 / 确认阶段】:如果预提交无异常,Spark 会尝试原子化同步执行两个核心操作 → ① 触发 Doris 的确认逻辑,将临时缓冲区的 GMV 数据「正式生效」,写入目标物理表;② 将本次批次的「最新 Kafka 偏移量 + 分层存储的状态快照」原子化写入 Checkpoint;
失败兜底逻辑:如果上述两个操作中任意一步失败(比如 Doris 写入失败、Checkpoint 写入失败、网络抖动),Spark 会立即「放弃本次提交」,不会更新任何偏移量和状态,下次动态触发生效时,会重新拉取该批次数据、重新计算、重新写入,保证数据绝对不丢失;这句话是重点,不会更新偏移量和状态,写入了doris也不怕,因为下次重新计算时doris有幂等性
最终的数据一致性,不是靠 Doris 回滚实现的,而是靠 Spark 的「重跑机制」+ Doris 的「幂等写入」共同兜底,这样共同成就了不丢不重复
1.5 问题解答
1 归纳哪些数据存储做为精准一次的写入数据源
答:Doris / ClickHouse,主键 / 唯一键(Unique Key)原生幂等写入,重复写入自动覆盖
Kafka:原生支持幂等生产 + 事务生产,Spark 写入 Kafka 时可开启事务,保证消息只被写入一次;Spark 的 Kafka Sink 原生支持两阶段提交,结合 Kafka 的事务机制,无丢无重;
湖仓类表(Iceberg / Hudi / Delta Lake):原生支持事务 + 幂等写入,Spark 和湖仓表深度集成,原生支持两阶段提交
MySQL/PG:给 MySQL 表新增「业务唯一键」:比如订单统计场景加「订单 ID」,窗口 GMV 场景加「窗口时间 + 省份」;
写入语法改造:将普通INSERT改为 INSERT INTO ... ON DUPLICATE KEY UPDATE(主键 / 唯一键冲突时,覆盖更新而非新增)。
适配逻辑:改造后具备原生幂等写入能力,Spark 重复提交时,MySQL 自动覆盖旧数据,最终结果一致;
无法满足的数据源:
❌ 本地文件 / HDFS 普通文件:无事务、无幂等,写入后无法回滚 / 去重;
❌ Socket / 消息队列(无事务的小众队列):无持久化、无幂等,数据易丢失易重复。
ES:去除异常刷数据和增加数据版本号以后,数据重复写概念极少但无法根除,
根因分析:ES的写入是"内存缓冲区→磁盘段文件→物理磁盘" 三层异步机制(记住这三层都是异步),refresh_interval=0s仅缩短了「内存→磁盘段文件」的刷新间隔,
这是最高频的重复来源,核心链路不可逆:Spark 微批写入 ES,文档成功进入 ES 内存(ES 返回写入成功),但未完成 refresh(仍不可见)--->后续 Spark 写入 Checkpoint 偏移量失败
(网络抖动 / 磁盘压力等),判定批次提交失败--->下次触发时 Spark 重跑该批次,向 ES 写入同_id、同版本号的文档;此时 ES 窗口期未过,旧文档仍不可见,判定为新文档重复写入,刷盘
后形成重复数据;
MONGODB:和MYSQL类似,采用唯一索引 + replaceOne+upsert=true,replaceOne代表更新,upsert代表有就更新,没有就插入

三、窗口和水位线
窗口(Window) 决定了数据该“去哪儿”(归类),而 水位线(Watermark) 决定了数据何时“不再被处理”以及状态何时“被清除”(过期)
窗口 (Window):定义的是时间区间(空间范围)。它将无界的数据流切分成一段段的桶。
例子:每 5 分钟一个窗口。[10:00, 10:05) 是一个窗口,[10:05, 10:10) 是下一个。
水位线 (Watermark):定义的是处理进度(时间阈值)。它衡量系统对“延迟数据”的忍耐极限。
例子:水位线 = 当前最大事件时间 - 10 分钟。

posted @ 2026-01-01 09:59  秋水依然  阅读(8)  评论(0)    收藏  举报