想象你是一家快递分拣中心的调度员:包裹全天源源不断涌来,你既不会等仓库堆满才发车,也不会来一件就派一辆车,而是每 5 分钟装满一车准时发走——这正是 Spark Structured Streaming 最核心的手艺:微批处理。为了不让晚到的包裹扰乱统计,中心还立了一条“截单线”,凡寄件时间早于这条线的包裹不再追算,这条线就叫水位线(watermark)。更妙的是,白天实时分拣和夜间批量盘点如今共用一套作业流程,实时与离线不再各写一套代码,这就是流批一体。本文借快递分拣中心的日常,带你拆解这套引擎的微批模型、水位线、延迟数据策略与输出模式,让你真正理解它为什么能成为现代数据管道的统一底座。

岗位关联

掌握 Structured Streaming 的微批模型、水位线与输出模式,你就能用一套 DataFrame 代码同时服务批流两种场景,显著降低数据管道的开发与维护成本,成为真正懂流批一体的数据工程师。

目录
  1. 可视化演示
  2. 流批一体架构
  3. 微批处理模型
  4. 输入数据源
  5. 水位线机制
  6. 延迟数据策略
  7. 输出模式
  8. 流批落地实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示Spark Structur的核心机制,通过源头/管道/节点/终端四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Spark Structur:Stru Structured Stre 微批处理模型与执行机制 核心机制可视化 Spark Structur整体架构与关键组件

流批一体架构

快递分拣中心过去有两条生产线:白天用“即时派送线”处理实时包裹,夜间用“批量盘点线”核对库存。两条线各自招人、各自培训、各自记账,一旦库存对不上,两个班组互相扯皮。后来中心引入了同一套“一票到底”的作业流程——不管包裹是白天到的还是夜间补录的,都走同一个单据模板、同一套分拣规则,问题立刻少了一半。

这套“一票到底”对应到 Spark 中,就是 Structured Streaming 的流批一体架构。传统上,批处理用 Spark DataFrame 离线跑定时任务,流处理用 Spark Streaming(DStream)按 RDD 微批处理,两套 API、两套调优手册。Structured Streaming 把两者统一为一张无限增长的 DataFrame——无论数据是静止的文件还是源源不断的消息,都抽象成同一张表,用同一套 SQL 和 DataFrame 算子操作。

区别只在于执行方式:对静态数据,一次查询读完所有行;对动态数据,查询被切成无限多个微批次持续执行,每一次微批的结果增量追加到结果表,形成“持续查询”。换句话说,同样的 groupBy、filter、join 代码,数据源换成流就是流处理,换成文件就是批处理,这就是流批一体的核心:逻辑统一、执行分层。

所以当老板问“白天和夜间的活儿能不能合并”,答案就在这张不断变长的台账里:不管包裹何时来、批量来还是零散来,都统一登记、统一分拣。流批一体,本质是把“两套人马两套账”压缩成“一套人马一套账”,省下的不只是代码量,更是两个班组对不上账时的无尽扯皮。

Rt=Rt−1∪Δt\displaystyle R_{t} = R_{t-1} \cup \Delta_{t}
累计结果 = 旧结果 + 本批新增,像分拣中心的总台账不断追加新记录。
  1. 结果表 $R_t$截至第 t 个微批结束时,查询输出的累计结果,相当于分拣中心的总台账,记录了当前所有包裹的分拣状态。
    RtR_t
  2. 上一批结果 $R_{t-1}$上一个微批结束时沉淀的旧台账,它是历史的累积,状态一旦产出就不再回退。
    Rt−1R_{t-1}
  3. 增量 $\Delta_t$第 t 个批次新到达并计算出的增量结果,比如本批刚分拣完成的 120 件包裹。
    Δt\Delta_t
  4. 并集运算 $\cup$把新增量并入旧台账,整个操作只增不改,保证持续查询的结果单调累加、不丢数据。
    ∪\cup

微批处理模型

分拣中心的发车时间固定为每 5 分钟一班:5 分钟内到的包裹挤进同一班车,整点还没到的就等下一班。这样既不会为单个包裹浪费一趟车,也不会让包裹滞留太久。这个“5 分钟一班”就是微批处理的脉搏——Structured Streaming 默认把到达的数据按时间片切成若干个小批,每个小批交给 Spark 批处理引擎完整跑一轮作业。

因此它并非真正的毫秒级流处理(那是 Flink 的拿手戏),而是一个以“秒级延迟”换取“批处理级吞吐和容错”的折中方案。微批的执行链路是:新数据按 offset 落盘记录 → 触发一次批任务 → 结果写入外部系统并提交 offset。由于每个微批都是完整作业,失败后可以从上次提交点重放,天然具备端到端恰好一次(exactly-once)的语义。

触发间隔由 .trigger(processingTime="5 seconds") 控制,间隔越小延迟越低、调度开销越大;间隔越大吞吐越高、数据可见越晚。设计微批模型时,要充分评估业务对延迟的容忍度,再决定这个“发车间隔”的粒度。

把“发车间隔”调小,包裹更早起运但车次变多、油耗上升;调大,运输成本下降但用户等得更久。微批模型的设计哲学正是如此——用可调节的间隔,在延迟与吞吐之间找到属于你业务的平衡点,这也是它区别于逐条流引擎的最鲜明特征。

N=⌊TI⌋\displaystyle N = \left\lfloor \frac{T}{I} \right\rfloor
总批次数 = 总时长 ÷ 触发间隔,向下取整,像发车班次一样只算完整班次。
  1. 运行时长 T从查询启动到停止的总时间,比如分拣中心从早上 8 点干到晚上 8 点,共 12 小时。
    TT
  2. 触发间隔 I相邻两个微批之间的时间间隔,类似“5 分钟一班”的发车间隔,由 trigger 参数决定。
    II
  3. 向下取整 $\lfloor \cdot \rfloor$不满一班的零头不算新的一班车,因为最后一班必须拉满才能发。
    ⌊TI⌋\left\lfloor \frac{T}{I} \right\rfloor
  4. 批次数 N总共触发的微批次数,它决定了调度开销与端到端延迟的上限,是容量规划的重要参数。
    NN

输入数据源

包裹不会凭空出现在分拣中心:有的来自上门取件,有的来自客户自送,还有的来自外地中转站。来源不同,交接单的格式也不同,但进入中心后统统变成统一包裹,进入同一条分拣线。Structured Streaming 的输入源也是这个道理——不同来源的数据形态各异,接入后全部映射为统一的行结构(DataFrame)。

常见的内置源有四类:File 源监控指定目录下新增的文件,适合日志落盘场景;Kafka 源订阅 topic 消息流,是生产环境最主流的高吞吐接入方式;Socket 源从 TCP 端口读字符串,常用于本地联调测试;Rate 源以固定速率产生测试数据,专门用来做压测和验证逻辑。它们各自的容错机制也不同:File 源记录已消费文件路径防重复,Kafka 源按分区记录 offset 以便中断后恢复。

速率控制也很关键。Rate 源每分钟能精确产生指定行数,让开发者在没有真实数据时就能验证窗口和水位线的行为;Kafka 源则通过限流参数控制每个微批拉取的上限,防止消费速度跟不上生产速度导致堆积。

无论包裹从哪个门进来,登记成统一单据后,分拣规则不用改一个字。输入源的多样性是流批一体的第一道闸门——把不同来源的“方言”翻译成同一种“普通话”,后端的处理逻辑才能真正复用,这也是为什么 File 和 Kafka 能接入同一个查询的原因。

Nt=r×t\displaystyle N_{t} = r \times t
Rate 源每秒造 r 行数据,跑 t 秒共产生 r×t 行,像传送带匀速滚来包裹。
  1. 速率 rRate 源的生成速率,单位是“行/秒”,相当于传送带每秒送来的包裹数量。
    rr
  2. 时长 t从开始生成到当前时刻经过的秒数,即传送带已经运转了多久。
    tt
  3. 累计行数 $N_t$到 t 秒时产生的总数据量,用于估算测试规模、验证消费能力和观察背压现象。
    NtN_t

水位线机制

分拣中心最头疼的是包裹上的寄件时间:有的包裹在路上堵了两小时才到,系统该把它归到“上午 10 点”那批还是“现在”这批?如果按到达时间算,聚合结果就乱套了。于是中心立了一条规则:所有包裹都按寄件时间归类,同时设一条“截单线”——凡寄件时间早于截单线的包裹,不再追算。

这条截单线就是 watermark(水位线)。它定义在事件时间(event time)上,而不是系统处理时间。水位线的计算公式是:当前观测到的最大事件时间,减去一个允许的延迟阈值 τ。τ 就是对“路上堵多久”的最坏估计,比如允许最晚的包裹比最前沿晚到 2 分钟,那么水位线就是“最前沿 - 2 分钟”。

水位线驱动两类关键动作:一是在窗口聚合时,只有事件时间大于等于水位线的数据才被纳入计算;二是水位线推进后,早于它的窗口状态会被安全清理,释放内存。如果没有水位线,所有历史窗口都要常驻状态内存,数据量一大必然内存爆炸。配合窗口函数使用时,窗口本身按事件时间切分,水位线负责决定窗口何时可以安全关闭。

截单线不能拍脑袋定:τ 设太大,状态清理慢、内存压力大;τ 设太小,部分包裹被过早放弃。就像分拣中心要根据历史路况而不是理想车速来定截单时间,watermark 的 τ 也必须来自对真实数据延迟的观察——先统计线上数据的事件时间分布,再定一个覆盖绝大多数正常情况的阈值。

W=max⁡(eventTime)−τ\displaystyle W = \max(\text{eventTime}) - \tau
水位线 = 当前最晚事件时间 - 容忍阈值,相当于截单线随最晚寄件时间动态右移。
  1. 最大事件时间 $\max(\text{eventTime})$到目前为止所有事件中寄件时间最新的那一条,代表数据流的“最前沿”位置。
    max⁡(eventTime)\max(\text{eventTime})
  2. 延迟阈值 τ业务允许的最大迟到量,比如“允许包裹在路上耽搁最多 2 分钟”,由 withWatermark 参数传入。
    τ\tau
  3. 求差值用最前沿减去容忍度,得到截单线;寄件时间早于这条线的事件被视为彻底迟到。
    max⁡(eventTime)−τ\max(\text{eventTime}) - \tau
  4. 水位线 W动态推进的截单线。由于 max(eventTime) 不断更新,水印线像潮水一样持续向右移动,早于它的事件不再参与窗口计算。
    WW

延迟数据策略

即使立了截单线,依然有包裹在截单后姗姗来迟。这时分拣员面临三个选择:把它塞回已经发走的车里(天方夜谭);为它再派一辆专车(成本爆炸);或者按规则判断它还值不值得追。Structured Streaming 的策略就是第三种——用规则做“值得不值得”的判断。

判断规则很简洁:事件时间大于等于当前水位线、且属于未过期窗口的数据,仍可参与窗口聚合,结果会以更新方式输出;事件时间小于水位线的数据,则被判定为彻底迟到,默认丢弃。注意,水位线本身会随新到达的最大事件时间右移,所以“迟到”的边界是动态的——一个 11:55 的事件,在水位线还是 11:50 时到达仍可正常计算,只有当水位线推进到 11:56 之后它才失去资格。

体现在实现上:withWatermark("ts", "2 minutes") 设定 τ 后,再配合窗口聚合,Spark 会为每个窗口维护状态直到水位线越过窗口末尾;在这之前到达的旧数据都能愉快地加入,越过之后状态被清理,再来的旧数据直接进不了门。这就是“窗口保留期”的概念——窗口的生死由水位线裁决。

这就像快递柜的代收规则:包裹晚到了,但只要还在“代收期”内就能入柜;超期未取的,柜子清空也没办法。延迟数据策略的关键,不是无脑接纳所有迟到者,而是用一条清晰的水位线,把“还能救”和“没必要救”分开,既保准确又控成本。

Accepted  ⟺  eventTime≥W\displaystyle \text{Accepted} \iff \text{eventTime} \geq W
迟到事件能否被接受,看它的寄件时间是否仍在水位线右侧。
  1. 事件时间 eventTime迟到包裹上原始的寄件时间,是判定的唯一依据,与系统处理时间无关。
    eventTime\text{eventTime}
  2. 当前水位线 W由水位线公式动态算出的截单线,会随着更大事件时间的出现不断右移。
    WW
  3. 比较运算 $\geq$寄件时间不早于截单线,是“还有救”的硬条件,只有满足它才可能参与聚合。
    eventTime≥W\text{eventTime} \geq W
  4. 接受条件 Accepted事件时间达标之外,还要求对应窗口尚未过期,两个条件同时成立才被真正接受并计入结果。
    Accepted\text{Accepted}

输出模式

包裹分拣完成,如何通知收件人?有人喜欢每件到货就单独通知(叮咚响不停);有人只想知道物流状态发生变化的那几件;还有人要求把全部在途包裹清单一次性发给他。输出模式正是回答“怎么对外发布结果”这件事——引擎算完结果后,以什么粒度把结果写出去。

Structured Streaming 提供三种输出模式。Append 只输出本次微批新增的行,适合无聚合或已满足水印约束的查询,像“每件到货单独发一条通知”;Update 输出本批次中被更新(新增或修正)的行,是最通用的聚合输出方式,像“只发状态变化的那几件”;Complete 每次输出全量结果表,直观但代价高,像“每次把完整清单重新发一遍”。

模式选错会直接报错或拖垮性能:Append 模式用于聚合查询时必须设置 watermark,否则 Spark 无法知道某行是否会被后续更新;Complete 模式要求全部结果常驻内存,状态无限增长时是灾难;Update 模式不要求保存全量,是多数流式聚合的首选。选择时还要看下游:写 Kafka 常配 Append,写 MySQL/Redis 常配 Update,画大屏快照才用 Complete。

通知方式没有绝对好坏:怕吵的用户选 Update 就够了,要全量快照的选 Complete。输出模式的意义,就是让你把“引擎内部怎么算”和“下游怎么用”解耦——算归算,发归发,各取所需,这也是流式查询能灵活对接多样下游的关键设计。

OtAppend=ΔRt,OtUpdate=U(Rt),OtComplete=Rt\displaystyle O_t^{Append} = \Delta R_t, \quad O_t^{Update} = U(R_t), \quad O_t^{Complete} = R_t
三种模式分别是:只发新增行、只发变化行、全量重发。
  1. 追加输出 $O_t^{Append} = \Delta R_t$只把第 t 批新产生的结果行推给下游,像每件到货单独发一条通知,最省流量。
    OtAppend=ΔRtO_t^{Append} = \Delta R_t
  2. 更新输出 $O_t^{Update} = U(R_t)$只推送结果表中本次发生变化(新增或修正)的行,适合聚合结果持续更新的场景。
    OtUpdate=U(Rt)O_t^{Update} = U(R_t)
  3. 全量输出 $O_t^{Complete} = R_t$每次把整张结果表发出去,直观完整但传输和序列化开销最大。
    OtComplete=RtO_t^{Complete} = R_t
  4. 变化行子集 $U(R_t) \subseteq R_t$变化行一定是全量的子集,这决定了三种模式的信息量与成本是递增关系,选型要匹配下游消费需求。
    U(Rt)⊆RtU(R_t) \subseteq R_t

流批落地实践

当你把实时分拣和夜间盘点统一成一套作业流程后,最直接的变化是什么?新员工只需要培训一次;两本台账变成一本;出问题时不用再判断是哪个班组的锅。流批一体在工程上的收益,也同样清晰——它不是一个炫技概念,而是一套可执行的落地路径。

落地路径通常分三步:先用批处理把历史全量数据灌入目标表(如 HDFS、Kudu、ClickHouse、Delta Lake);再启动同一套 Structured Streaming 查询消费增量流,结果以 Update 模式写入同一张表;最后用水位线保证迟到数据正确合并。批与流的代码差异,只体现在入口是 read 还是 readStream,业务逻辑的 DataFrame 算子完全一致。

收益方面有三点:开发成本上,一套逻辑两处复用,省去双套代码的编写和联调;维护成本上,指标口径只维护一份,不会出现“实时看 100、离线看 90”的口径冲突;管道效率上,批流共用同一套 shuffle 与调度基础设施,资源利用率更高。团队排障时也只需查一套日志、看一套监控,故障定位时间显著缩短。

从两班人马到一班人马,省下的不只是工资,更是两班交班时说不清的误会。流批一体落地,让数据团队从“实时离线两线作战”变成“一套逻辑走天下”——这正是 Structured Streaming 给现代数据管道带来的最大礼物,也是它在流批一体架构中占据核心位置的根本原因。

Cunified=Cdev+Cops≪Cbatch+Cstream+Calign\displaystyle C_{unified} = C_{dev} + C_{ops} \ll C_{batch} + C_{stream} + C_{align}
统一后总成本远小于双栈成本之和,因为隐藏的“口径对齐”成本直接归零。
  1. 统一开发成本 $C_{dev}$一份 DataFrame 逻辑批流两边复用,新增需求只需改一处,开发边际成本很低。
    CdevC_{dev}
  2. 统一运维成本 $C_{ops}$调度、监控、排障只维护一套系统,告警和日志链路单一清晰。
    CopsC_{ops}
  3. 双栈总成本 $C_{batch}+C_{stream}+C_{align}$传统方案下两套系统的开发运维成本,外加实时结果和离线结果对不上时反复沟通核对的口径对齐成本。
    Cbatch+Cstream+CalignC_{batch} + C_{stream} + C_{align}
  4. 远小于号 $\ll$统一后口径对齐成本归零,总成本显著低于双栈方案,这也是流批一体最诱人的经济账。
    ≪\ll

AI 应用场景

实时风控与交易反欺诈

阶段:数据接入与实时特征计算

解决的问题:传统风控用 T+1 离线批处理,欺诈发生一天后才被发现。AI 场景需要毫秒级识别异常行为,而支付交易流天然是事件流,正适合用 Structured Streaming 的微批+水位线体系做实时特征聚合。

具体案例:支付平台把 Kafka 中的交易事件接入流,withWatermark("eventTime", "30 seconds") 后按“用户 + 1 分钟窗口”统计支付次数,再与离线训练的欺诈模型打分结合,交易频次超过阈值立即触发风控告警,秒级输出风险名单。

实时用户行为分析大屏

阶段:指标计算与可视化

解决的问题:运营想看实时的 PV、UV 和点击漏斗。过去用批处理每分钟全量重算,资源浪费严重且指标延迟高;AI 辅助的推荐系统更需要实时反馈用户行为变化。

具体案例:电商平台用 File 源监听埋点日志目录,groupBy(window("ts", "1 minute"), "page") 聚合 PV/UV,用 Complete 模式把全量指标写入 Redis,前端大屏每秒订阅刷新,配合 AI 推荐服务实时调整首页商品排序。

湖仓一体增量入湖

阶段:数据同步与湖仓构建

解决的问题:传统数据湖用凌晨批任务全量同步,时效差且重复计算;AI 模型训练需要 T+0 数据,但历史回填和增量更新要两套代码。流批一体正好解决这一矛盾。

具体案例:数据平台用同一套 DataFrame 逻辑,先跑一次批处理把历史数据写入 Delta Lake,再切换 readStream 消费 Kafka 增量以 Update 模式入湖。批与流共享一套转换代码,水位线保证迟到数据正确合并,湖仓数据始终一致。

Python 演示

python49 行
# -*- coding: utf-8 -*-"""Structured Streaming 水位线窗口聚合演示前置: 运行 nc -lk 9999, 逐行输入: 2024-01-01 10:00:00,U001,home"""from pyspark.sql import SparkSessionfrom pyspark.sql.functions import split, window  def create_spark():    spark = (SparkSession.builder             .appName("StreamingWatermarkDemo")             .master("local[2]")             .config("spark.sql.shuffle.partitions", "2")             .getOrCreate())    spark.sparkContext.setLogLevel("WARN")    return spark  def main():    spark = create_spark()    lines = (spark.readStream             .format("socket")             .option("host", "localhost")             .option("port", 9999)             .load())     fields = split(lines.value, ",")    events = lines.select(        fields.getItem(0).cast("timestamp").alias("ts"),        fields.getItem(1).alias("uid"),        fields.getItem(2).alias("page")    )     with_watermark = events.withWatermark("ts", "2 minutes")    counts = (with_watermark              .groupBy(window("ts", "1 minute", "30 seconds"), "page")              .count())     query = (counts.writeStream             .outputMode("update")             .format("console")             .trigger(processingTime="5 seconds")             .start())     query.awaitTermination()  if __name__ == "__main__":    main()
目的:用 PySpark 演示一个带水位线的流式窗口聚合——从 Socket 读取“时间戳,用户,页面”格式的事件流,按 1 分钟滑动窗口统计每个页面的访问量,每 5 秒触发一次微批,以 Update 模式输出到控制台。
  1. .format("socket")声明从 Socket 端口读取流式数据,相当于快递中心从“上门取件通道”收货。
  2. .option("host", "localhost")绑定本地地址,配合 nc -lk 9999 手动发送测试数据。
  3. fields.getItem(0).cast("timestamp")把字符串列转成时间戳类型,确保窗口能按事件时间切分。
  4. withWatermark("ts", "2 minutes")设置 2 分钟延迟阈值,告诉引擎“路上堵 2 分钟以内的包裹都算数”。
  5. window("ts", "1 minute", "30 seconds")定义 1 分钟窗口、30 秒滑动间隔,窗口按事件时间对齐。
  6. .outputMode("update")聚合结果以更新模式输出,只打印本批发生变化的分组计数。
  7. .trigger(processingTime="5 seconds")每 5 秒触发一个微批,就是“5 分钟一班车”的发车间隔。
  8. query.awaitTermination()阻塞主线程持续运行流查询,直到手动停止。

常见误区

误区Structured Streaming 名字里带 Streaming,我以为它跟 Flink 一样是来一条处理一条的毫秒级实时引擎,延迟越低越好。

正解它本质是微批模型,数据先攒一个批间隔再计算,端到端延迟通常是秒级而非毫秒级。这就像快递中心不会为单个包裹单独发一辆车。如果业务要求毫秒级响应,应评估 Flink 等真正的流引擎;如果秒级够用,微批的吞吐和容错优势更划算。技术选型先问延迟需求,再选引擎。

为何常见:名字叫 Streaming,加上 Spark 生态知名度高,很容易让人把它和逐条流处理划等号。

误区水位线阈值设得越大,丢的数据越少。为了不漏数据,我把 τ 设为 10 小时,这样总安全了吧?

正解τ 越大,状态保存越久、清理越慢,内存和磁盘压力越大,还延迟了结果可见性。正确做法是根据业务可容忍的迟到来定 τ——像根据历史路况而不是理想车速定截单时间,可接受延迟是 2 分钟就不要设 10 分钟。水印不是“越大越好”的保险丝,而是一个要精算的成本参数。

为何常见:人们往往只看到“丢数据”的风险,忽略状态膨胀的成本,以为大水印是免费的。

误区我听说做窗口聚合必须设置 withWatermark,否则查询会直接报错。

正解并没有这个强制要求。聚合查询能否运行取决于输出模式:Append 模式必须设水印(否则引擎不知道某行是否还会更新);Update/Complete 模式不设水印也能跑,但状态会无限增长。水印解决的是“状态何时可清理”,不是“能不能聚合”。就像快递柜必须有代收期限,但有没有代收期限都不影响柜子能放几个包裹。

为何常见:网上很多示例都写了 withWatermark+window,看起来像标配,实际上两者是正交概念。

误区只要数据到达时晚于水位线,就立刻被丢掉,不会进入任何计算。

正解丢弃判定用的是事件时间与当前水位线的动态比较。一个事件时间 11:55 的数据,在水位线还是 11:50 时到达仍可参与计算;只有当水位线推进到 11:56 之后它才被丢弃。这就像快递代收:只要还在代收期内就能入柜,清柜后才真没戏。

为何常见:把水位线理解成固定不变的时间戳,忽略了它随数据流动态推进的本质。

误区流批一体就是同一个程序既能跑批也能跑流,完全不用改任何配置。

正解准确说法是同一套 DataFrame 逻辑复用,但入口不同(read 与 readStream)、触发方式不同、水位线策略只对流生效,批量跑时还要准备历史数据。就像白天实时分拣和夜间盘点虽然用同一套作业手册,但班次编排和交接单仍然不同。理解“逻辑统一、执行分层”,才不会被宣传话术误导。

为何常见:宣传文案把“逻辑统一”包装成“代码万能”,让人忽略了执行层的差异。

面试题

Q1. Structured Streaming 的核心执行模型是?

单选题 · easy

  • A.逐条处理模型
  • B.微批处理模型← 正确答案
  • C.全量批处理模型
  • D.随机采样模型

答案:B。Structured Streaming 采用微批模型,不是来一件处理一件,而是攒一个时间片后整批计算——就像快递中心每 5 分钟装满一车发走。逐条处理是 Flink 等真正流引擎的做法,全量批处理则是离线任务的特征。

Q2. 水位线 watermark 的计算依据是?

单选题 · easy

  • A.系统当前处理时间
  • B.最大事件时间减延迟← 正确答案
  • C.批次触发时间间隔
  • D.窗口聚合结果条数

答案:B。水位线 = max(事件时间) - 延迟阈值 τ,它基于事件时间(寄件时间)计算,与系统当前处理时间无关,它划出的是“截单线”,决定迟到数据是否还有资格参与计算。

Q3. 某流事件时间最大值是 12:00,τ=2 分钟,当前水位线是?

单选题 · mid

  • A.12:00
  • B.12:02
  • C.11:58← 正确答案
  • D.10:00

答案:C。根据公式 W = max(eventTime) - τ,12:00 减去 2 分钟得到 11:58。水位线永远不大于当前最晚事件时间,12:02 不可能;它只跟最前沿和容忍阈值有关,与 10:00 无关。

Q4. 事件时间早于水位线的数据会被?

单选题 · mid

  • A.立即进入窗口计算
  • B.等待下次批次重试
  • C.判定迟到并丢弃← 正确答案
  • D.更新历史聚合结果

答案:C。事件时间早于水位线意味着超过了“截单线”,被判定为彻底迟到、默认丢弃。就像包裹过了代收期才送到代收点,柜子已清空无法入柜。只有事件时间仍在水位线右侧的数据才有资格参与窗口聚合。

Q5. 带聚合的查询使用 Append 模式时,必须满足?

单选题 · mid

  • A.必须设置 watermark← 正确答案
  • B.必须设置批间隔 0
  • C.必须使用 Complete
  • D.必须关闭状态存储

答案:A。Append 模式只输出新增行,但聚合结果会随迟到数据更新,不设 watermark 引擎就无法判断某行是否还会变化,因此会直接报错。设置 watermark 后,满足水印约束的聚合才能安全使用 Append。

Q6. 关于微批触发间隔的描述,正确的是?

单选题 · hard

  • A.间隔越大延迟越低
  • B.间隔影响端到端延迟← 正确答案
  • C.间隔必须小于水位线
  • D.间隔只能设为 5 秒

答案:B。微批触发间隔相当于发车间隔,它直接决定端到端延迟的上限——间隔越大,数据要等更久才能被处理。间隔越小延迟越低而非越高;间隔和水位线没有大小约束;间隔可配置,5 秒只是常见值。

Q7. 下列哪个是 Structured Streaming 内置输入源?

单选题 · easy

  • A.Redis 源
  • B.Kafka 源← 正确答案
  • C.MySQL 源
  • D.MQTT 源

答案:B。内置源包括 File、Kafka、Socket 和 Rate 四类。Kafka 是高吞吐生产环境的首选。Redis、MySQL、MQTT 需要借助外部 connector 或自研 receiver 才能接入,不算内置源。

Q8. 关于 Structured Streaming 输出模式的说法,正确的有?

多选题 · mid

  • A.追加模式只输出新增行← 正确答案
  • B.更新模式输出全量结果
  • C.完整模式每次输出全量← 正确答案
  • D.三种模式延迟完全相同

答案:AC。Append 只输出新增行,Complete 每次输出全量结果表,这两种说法正确。更新模式只输出本批发生变化的行,而不是全量;三种模式输出粒度不同导致传输开销不同,延迟并不相同——Complete 全量输出开销最大,延迟也更高。

Q9. 关于水位线机制的说法,正确的有?

多选题 · hard

  • A.基于事件时间计算← 正确答案
  • B.帮助清理过期状态← 正确答案
  • C.阈值越大内存越大← 正确答案
  • D.阈值必须大于窗口

答案:ABC。水位线基于事件时间计算,推进后帮助清理过期窗口状态、释放内存,这两点正确。阈值越大状态保留越久、内存压力越大也正确。但阈值与窗口长度没有必然的大小关系,D 错误——截单时间不一定非要超过窗口长度。

Q10. 流批一体架构带来的收益包括?

多选题 · extreme

  • A.一套逻辑复用流批← 正确答案
  • B.降低开发维护成本← 正确答案
  • C.统一指标计算口径← 正确答案
  • D.完全消除流延迟

答案:ABC。流批一体用同一套 DataFrame 逻辑同时服务批和流,显著降低开发和维护成本,且指标口径只维护一份,避免实时离线数值对不上,A、B、C 都正确。但微批模型本质仍有秒级延迟,不可能“完全消除”流延迟,D 是过度宣传的误区。