巷口新开了一家叫“一茶二室”的奶茶店。楼下是中央备料间,每天打烊后,店员把全天几百张小票汇总,一次性煮好珍珠、泡好茶底——这是“批”;楼上现制台则完全不同,顾客每下一单,店员立刻拿杯、加料、封口——这是“流”。过去店里两本菜谱、两套流程:备料间按“批量清单”走,现制台按“实时接单”走,同一杯奶茶的口味标准竟常常对不上。店长小林做了个决定:把备料和现制合并成同一本菜谱、同一个厨房,既支持一次备一大缸,也支持一杯一杯现做。这本“统一菜谱”,就是大数据世界里传说中的“流批一体”——用一套 API 和一套引擎,同时驾驭离线批处理和实时流处理,彻底告别两套代码、两份数据的时代。

岗位关联

流批一体是实时数仓工程师的“降本增效必修课”:从两套代码的对账地狱,走向一套 API 的优雅统一,是架构能力的分水岭。

目录
  1. 可视化演示
  2. 流批一体概述
  3. DataStream统一API
  4. 时间语义
  5. 水位线机制
  6. 动态表机制
  7. Changelog机制
  8. 实时数仓演进
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

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

统一引擎 (Flink) Watermark 同一套API处理流+批 批处理链路 (Spark/Hive) 流批一体可视化:对比统一引擎与Lambda架构,展示水位线机制和动态表概念

流批一体概述

一茶二室的楼下备料间里,店员等着打烊后把所有小票一次性算清:今天珍珠用了多少、茶底煮了几锅,数据是“收齐了才算”;楼上现制台则是来一单做一杯,永远不知道下一个顾客点什么,数据是“源源不断”的。店长小林发现,两种活法本质都是“把订单变成奶茶”,只是数据的“边界”不同。

大数据里的批处理,就是楼下备料间的逻辑:把一段时间内收齐的数据一次性算完,有头有尾;流处理则是楼上现制台的逻辑:数据像河水一样不断涌来,来一条处理一条,没有尽头。过去很多公司为了兼顾两者,架了两套系统——离线用 Spark,实时用 Flink,同一指标要写两遍代码,像两本菜谱分别管两个厨房,口径还经常对不上。

流批一体的核心思想,正是小林的那本“统一菜谱”:一套 API、一套引擎,通过切换执行模式同时支持批和流。在它看来,批不过是“有界的流”——水流到了终点,把终点前的所有事件一起算完而已。

小林把那两个厨房合并后,店员们终于不用再记两套流程;同样,企业把两套引擎收编成一套后,维护成本直线下降,指标口径也第一次真正统一了。不过,光有理念还不够,要让一个厨房兼容两种火候,靠的是那本写满基础操作的“统一菜谱”——DataStream API。

Dstream={e1,e2,e3,… }(unbounded),Dbatch={d1,d2,…,dn}(bounded)\displaystyle D_{\text{stream}} = \{ e_1, e_2, e_3, \dots \} \quad \text{(unbounded)}, \qquad\displaystyle D_{\text{batch}} = \{ d_1, d_2, \dots, d_n \} \quad \text{(bounded)}
流式数据是“没有句号的集合”,批式数据是“已经收尾的集合”;流批一体就是让同一套算子同时读得懂这两种集合。
  1. 流式数据 D_stream用集合形式表示源源不断的订单序列,e₁ 是第一杯、e₂ 是第二杯……顾客随时会下新的单,所以这个集合永远等不到最后一个元素,叫无界。
    Dstream={e1,e2,e3,… }D_{\text{stream}} = \{ e_1, e_2, e_3, \dots \}
  2. 批式数据 D_batch表示已收齐的一整批小票,从 d₁ 到 d_n,数量 n 是确定的,像打烊后整理好的所有单据,叫有界。
    Dbatch={d1,d2,…,dn}D_{\text{batch}} = \{ d_1, d_2, \dots, d_n \}
  3. 统一泛化流批一体把“流”看成“还没结束的批”,把“批”看成“已经结束的流”,于是两种形态都可以交给同一套处理逻辑,只是执行时机不同。
    Dstream=lim⁡n→∞DbatchD_{\text{stream}} = \lim_{n \to \infty} D_{\text{batch}}

DataStream统一API

林记的菜谱上写满了基础操作:“加珍珠”“去冰”“换大杯”“按口味分组”。不管是楼下大批量备料,还是楼上现做单杯,店员都是照着同一套操作来组合,只是执行节奏不同。DataStream 之于 Flink,就是这本菜谱。

DataStream 统一 API 的意思是:无论是实时流还是离线批,你都用同一组算子来编写代码——map 做转换、filter 做过滤、keyBy 做分组、window 开窗口、aggregate 做聚合、sink 写出结果。批处理不再需要另一套“Spark 方言”,因为批数据在 API 眼里只是“有界的流”:给同一个任务切到 BATCH 执行模式,引擎就按有界输入做整体调度;切回 STREAMING 模式,就恢复逐条消费、增量计算。

这套设计让“算子一致性”成为天然属性:同一个窗口聚合函数,在流模式下按水位线增量触发,在批模式下按输入边界整体切分,代码一字不改。

有了这本统一菜谱,店长终于不用再教两批店员。但小林很快发现一个新问题:统计“3 点钟卖了多少杯”时,楼下按小票时间算,楼上按接单时间算,两边数字又打架了——问题的根源,藏在“时间”这个看似简单的词里。

Job=Source→Map→Filter→KeyBy→Window→Aggregate→Sink\displaystyle \text{Job} = \text{Source} \to \text{Map} \to \text{Filter} \to \text{KeyBy} \to \text{Window} \to \text{Aggregate} \to \text{Sink}
一条算子链就像一份菜谱的操作序列,流与批只是“上菜节奏”不同,操作本身完全共享。
  1. Source / Sink链的两端分别负责读入数据和写出结果,流的 Source 是 Kafka 这类消息队列,批的 Source 是文件系统里的历史数据。
    Source→⋯→Sink\text{Source} \to \cdots \to \text{Sink}
  2. 中间算子链Map、Filter、KeyBy、Window、Aggregate 依次对数据进行加工,每个算子只做一件事,像菜谱上“先加料、再封口”的步骤。
    Map→Filter→KeyBy\text{Map} \to \text{Filter} \to \text{KeyBy}
  3. 同一链两种模式这条链在 STREAMING 模式下逐条响应,在 BATCH 模式下整体调度,代码本体不因模式而重写。
    Job=同一算子链(执行模式)\text{Job} = \text{同一算子链}(\text{执行模式})

时间语义

顾客小票上印着“14:32“,这是她实际点单的时刻;厨房忙碌时,店员可能 14:38 才看到这张小票开始制作。小林把这两个时间分别记了下来:前者叫”事件时间“,后者叫”处理时间”。统计 14:30 那一档的销量,到底该信哪个?

在流处理中,“事件时间”是业务实际发生那一刻的时间,它永远不会变,就像小票上的印字;“处理时间”是数据进入引擎、被算子处理的时刻,像店员拿到小票开始做奶茶的时刻,受机器负载、网络延迟影响,充满不确定性;还有第三种“注入时间”,是数据进入系统时被统一打上的时间戳,介于两者之间。

如果按处理时间统计,一台机器卡顿 5 分钟,“3 点档”的数据就可能被记到“3 点 05 分档”;只有事件时间才能还原业务真相。所以流批一体里,事件时间才是默认的“裁判”,而它必须给出一个数学承诺:

选定事件时间后,小林面临下一个难题:网络会让订单乱序到达——3 号桌的小票可能 4 号桌之后才送到柜台。他需要一把“等多久才算齐”的尺子,这就是水位线。

tevent(e)≤tproc(e),Δ=tproc(e)−tevent(e)≥0\displaystyle t_{\text{event}}(e) \le t_{\text{proc}}(e), \qquad\Delta = t_{\text{proc}}(e) - t_{\text{event}}(e) \ge 0
事件时间永远不晚于处理时间:小票先打印出来,厨房才可能开始做,两者之间的差就是系统的处理延迟。
  1. 事件时间 t_event(e)事件 e 在业务世界发生的时间戳,比如小票上的 14:32,它不随系统状态改变。
    tevent(e)t_{\text{event}}(e)
  2. 处理时间 t_proc(e)引擎实际处理这条数据的时钟时刻,比如店员 14:38 才开始做,它受负载影响。
    tproc(e)t_{\text{proc}}(e)
  3. 非负延迟因为事件必须先发生才可能被处理,所以处理时间不会早于事件时间,差值就是系统带来的延迟,永远大于等于零。
    Δ=tproc(e)−tevent(e)\Delta = t_{\text{proc}}(e) - t_{\text{event}}(e)

水位线机制

店长小林定了一条规矩:统计 3:00 的订单时,只等到 3:05,误差 5 分钟内的迟到小票都算数,超过 5 分钟就不再等了。这 5 分钟就是“耐心阈值”δ;"3:05"这个时刻,就是店长心中的水位线——它宣告:所有事件时间不晚于 3:00 的订单,此刻应该基本到齐了。

水位线(Watermark)正是这样一把“数据到齐”的尺子:它由已到达事件的最大事件时间减去容忍阈值 δ 得到。水位线越往前推,引擎就越有底气触发窗口:当水位线推进到某个窗口的结束时刻时,这个窗口就“封盘开奖”——像店长宣布“3:00 的账可以算了”。

如果数据提前到达或乱序到达,水位线负责兜底:窗口触发前到达的旧数据正常计入;触发后、水位线允许范围内仍会再等一等(allowedLateness);迟得太离谱的数据则走旁路输出(side output)进“待复核筐”,绝不让它污染已发出的结果。

水位线让“何时算齐”有了数学依据,但窗口结果要如何高效地更新?小林发现,光有静态账本不够——他需要一块会自己跳数字的“看板”。

W(P)=max⁡e∈S(P)tevent(e)−δ\displaystyle W(P) = \max_{e \in S(P)} t_{\text{event}}(e) - \delta
水位线 = 已到事件的最大事件时间 − 容忍迟到量。它回答的是“我们敢算到哪一刻为止”。
  1. S(P)到处理时刻 P 为止,引擎已经收到的全部事件集合,相当于柜台前堆着的所有小票。
    S(P)S(P)
  2. max t_event(e)从已收到的事件里找出事件时间最大的那一个,代表“最新鲜的业务时刻”。
    max⁡e∈S(P)tevent(e)\max_{e \in S(P)} t_{\text{event}}(e)
  3. 减去 δ后退 δ 作为安全余量,意思是“最新消息说到了 3:10,但我担心还有迟到货,所以只敢确认 3:05 之前的账”。
    max⁡e∈S(P)tevent(e)−δ\max_{e \in S(P)} t_{\text{event}}(e) - \delta
  4. 窗口触发条件当水位线 W(P) 大于等于窗口 [a, b) 的结束时刻 b 时,窗口立即触发计算并输出结果。
    W(P)≥bW(P) \ge b

动态表机制

林记前台挂着一块“实时爆款排行看板”,每卖出一杯,看板上对应口味的计数就 +1。小林从不让人每分钟重算整块板,而是“来一条改一次”;打烊后,他又用全天小票重算一遍整块板做对账。同样一块看板,白天增量更新,晚上全量重算——这就是动态表的流与批两面。

动态表(Dynamic Table)是把无限数据流映射成一张“一直在变”的表:每条流入数据都触发一次行级更新,就像每杯奶茶让看板 +1。在这张表上执行的 SQL 叫“连续查询”:它不是跑一次就结束,而是随着新数据的到来持续输出新结果——白天那套增量更新逻辑。

引擎能做到“来一条改一次”,靠的是查询重写:把用户写的 SQL 自动改写成增量算子,只计算“新来的数据影响了哪些旧结果”,再维护一份状态把它们累积起来。最后,查询结果表每变一次就吐出一条变更流,重新变成下游可消费的数据流。

看板之所以能自己跳数字,是因为它背后有人把每一次“新增一杯”“改大杯”“退单”都记成了变更日志——这,就是 Changelog 机制。

Stream→convertDynamic Table→queryResult Table→convertChangelog Stream\displaystyle \text{Stream} \xrightarrow{\text{convert}} \text{Dynamic Table} \xrightarrow{\text{query}} \text{Result Table} \xrightarrow{\text{convert}} \text{Changelog Stream}
一条流水线完成“流变表、表上查、结果回流”:流的无限性被表承接,表的查询被流驱动。
  1. 流到表把无限流逐条“落”成一张不断追加和更新的动态表,对应看板上的每一次 +1。
    Stream→convertDynamic Table\text{Stream} \xrightarrow{\text{convert}} \text{Dynamic Table}
  2. 连续查询SQL 被改写成增量算子,数据每到达一条,只重算受影响的那一小块结果,而不是全板重扫。
    Dynamic Table→queryResult Table\text{Dynamic Table} \xrightarrow{\text{query}} \text{Result Table}
  3. 表回流结果表的每次变化都转化成一条变更记录流,供下游实时消费,相当于看板每跳一次就广播一次。
    Result Table→convertChangelog Stream\text{Result Table} \xrightarrow{\text{convert}} \text{Changelog Stream}

Changelog机制

一位顾客点了中杯奶茶,两分钟后改口要大杯。小林不会让店员“再做一杯新的”然后不管旧杯,而是记一条变更:撤掉中杯、加上大杯;若顾客退单,就记一条删除。厨房里永远只有一杯正在制作的奶茶,但账本上清清楚楚躺着变更记录。

Changelog(变更日志)机制正是这套账本:它把动态表的每一次变化记录成三类操作——插入(+I)、更新(-U 删旧行、+U 加新行)、删除(-D)。下游不需要重新读整张表,只要顺着变更日志一条条“重放”,就能重建出任何时刻的结果表。

这带来一个强大的能力:增量物化。最终结果不再是“每来一条数据就全量重算”,而是“初始快照 + 不断追加的增量变更”。配合 Flink 的检查点机制,变更日志还能保证一致性——系统崩溃后从头重放日志,账本分毫不差,就像录像带可以倒带重播。

账本清楚了,小林终于可以回答最本质的问题:既然批和流都能用同一套工具搞定,那过去为什么要养两个厨房?答案写在一段狼狈的演进史里。

R(τ)=R(0)+∑i=1kΔi,Δi∈{+I,−U,+U,−D}\displaystyle R(\tau) = R(0) + \sum_{i=1}^{k} \Delta_i, \qquad\Delta_i \in \{ +I, -U, +U, -D \}
任意时刻的结果 = 初始快照 + 从第 1 条到第 k 条变更的累积;每一条变更都是四种操作之一。
  1. R(0) 初始快照结果表的起点状态,比如打烊前最后一次全量对账的账本。
    R(0)R(0)
  2. ΣΔᵢ 增量累积把 k 条变更逐条累加到快照上,每一条都精确描述“哪一行发生了什么变化”。
    ∑i=1kΔi\sum_{i=1}^{k} \Delta_i
  3. 四类变更+I 表示新增一行,-U/+U 表示先删旧行再插新行(即更新),-D 表示删除一行。
    Δi∈{+I,−U,+U,−D}\Delta_i \in \{ +I, -U, +U, -D \}

实时数仓演进

最早,林记是标准的“双厨房”:楼下备料间按天出“全天报表”,楼上现制台按秒出“实时数字”,两本账经常对不上,店长每天要花大量时间“对账”;后来改成“全流水线现制”,所有数据都走流式管道,但重算历史时效率太低;直到统一菜谱出现,才真正做到一个厨房两种火候。

这套演进对应着实时数仓的三代架构。第一代叫 Lambda 架构:离线批链路算全量数据,实时流链路算增量数据,最后合并出结果——但两条链路两套代码,结果冲突时要靠“对账”解决,代价高昂。第二代叫 Kappa 架构:干脆让所有数据都以流的方式处理,需要历史就从头回放消息队列——代码只有一套了,但回放重算既慢又费资源。

流批一体是第三代:在一套引擎里,流模式负责实时增量,批模式负责历史上卷,两种执行模式共享同一套 API 与算子。批不再是“另一个系统”,而是“流引擎的一种特殊执行模式”。这正是它相对 Lambda 和 Kappa 的最优解:一套代码、两种模式、一个口径。

小林终于关掉了那间备料间。如今,一茶二室用一本菜谱管住了两种节奏:批量备料是“把流暂停后整体算”,现制单杯是“边来边算”。而大数据世界里的流批一体,也在实时数仓的舞台上扮演着同样的角色。

Lambda: R=Batch(D)⊕Stream(ΔD)\displaystyle \text{Lambda: } R = \text{Batch}(D) \oplus \text{Stream}(\Delta D)
Kappa: R=Stream(D)\displaystyle \text{Kappa: } R = \text{Stream}(D)
Lambda 要“合并两条链”才能得到最终结果,Kappa 把一切都交给流,而流批一体让批成为流引擎的一种执行模式,彻底消解了两套系统。
  1. Lambda 的合并全量批结果与增量流结果通过 ⊕ 运算合并,为此必须维护两套代码并对账。
    Batch(D)⊕Stream(ΔD)\text{Batch}(D) \oplus \text{Stream}(\Delta D)
  2. Kappa 的全流一切数据都走流式管道,历史结果通过从源头回放数据重建。
    Stream(D)\text{Stream}(D)
  3. 流批统一的换代新一代架构让批任务在流引擎内以 BATCH 模式执行,API 统一、口径统一。
    统一代码(STREAMING/BATCH)\text{统一代码}(\text{STREAMING/BATCH})

AI 应用场景

实时数仓中的流批统一 ETL

阶段:数据加工与指标计算

解决的问题:传统 Lambda 架构要用 Spark 维护离线 ETL、用 Flink 维护实时 ETL,同一份订单数据写两套 SQL,大促时对账天天加班。流批一体让同一份 Flink SQL 既跑实时链路又切批模式算全量历史,一套代码保住了口径统一。

具体案例:某电商平台把订单明细接入 Kafka,用 Flink SQL 定义“订单宽表”动态表,实时模式产出秒级 GMV 大屏;T+1 凌晨切到 BATCH 模式跑同一 SQL 做全量对账,两边的 GMV 数字终于分毫不差。

金融交易反欺诈实时指标

阶段:实时特征计算

解决的问题:支付交易天然乱序,按处理时间统计会造成误判。用事件时间定义“30 秒内同卡交易次数”,配合水位线容忍网络抖动,迟到交易进旁路输出做人工复核,既保住实时性又不冤枉好用户。

具体案例:某支付平台的风控引擎每秒处理数万笔交易,Flink 按事件时间开滑动窗口统计频次,水位线偏移设为 2 秒;超过窗口才到的交易自动进入 side output,由离线复核模型二次判断。

物联网设备状态实时看板

阶段:设备状态聚合与推送

解决的问题:海量设备上报乱序、时断时续,用普通表更新会丢状态。用动态表 + Changelog 维护“设备最新状态表”,每台设备的每一次状态变更都生成 +U 变更日志,下游大屏按日志增量刷新。

具体案例:某工业互联网平台接入十万台设备,Flink 将上报流映射为动态表,连续查询产出实时健康度指标,Changelog 流推送到 WebSocket 大屏,同时数据湖按批模式每晚重算全量报表。

Python 演示

python54 行
# -*- coding: utf-8 -*-"""流批一体最小演示:一套窗口求和逻辑,流式与批式两种执行模式复用"""  def window_sum(events, start, end):    """同一套窗口计算逻辑:流模式增量跑、批模式整体跑都调用它"""    return sum(v for t, v in events if start <= t < end)  class UnifiedPipeline:    """同一份代码,两种执行模式"""     def __init__(self, events, mode="streaming", max_delay=1.0):        self.events = events          # 按到达顺序排列的事件        self.mode = mode              # streaming / batch        self.max_delay = max_delay    # 水位线容忍的最大迟到量        self._count = 0               # 触发窗口次数     def run(self, win_size=5):        if self.mode == "batch":            ordered = sorted(self.events, key=lambda e: e[0])            max_t = max(t for t, _ in ordered)            boundary = win_size            while boundary <= max_t + win_size:                self._emit(ordered, boundary - win_size, boundary, "batch")                boundary += win_size        else:            self._stream_run(win_size)        return self._count     def _emit(self, seq, start, end, label):        total = window_sum(seq, start, end)        print("%s: 窗口[%d, %d) total=%d" % (label, start, end, total))        self._count += 1     def _stream_run(self, win_size):        watermark = 0.0        boundary = win_size        arrived = []        for t, v in self.events:            arrived.append((t, v))            watermark = max(watermark, t - self.max_delay)            if watermark >= boundary:                self._emit(arrived, boundary - win_size, boundary, "stream")                boundary += win_size  if __name__ == "__main__":    # 事件(3,7)最后才到,属于迟到的乱序数据    orders = [(1, 8), (2, 5), (5, 3), (4, 9), (6, 6), (8, 2), (7, 4), (3, 7)]    print("== 流模式:水位线推进、窗口增量触发 ==")    UnifiedPipeline(orders, "streaming").run(5)    print("== 批模式:同样一套窗口求和逻辑整体切窗 ==")    UnifiedPipeline(orders, "batch").run(5)
目的:用 50 行代码演示“同一套窗口求和逻辑”如何分别以流模式和批模式运行,直观看到迟到数据对两种模式结果的影响。
  1. def window_sum(events, start, end):这就是“统一菜谱”里的那道基础菜:窗口求和,流批两种模式都调用它。
  2. return sum(v for t, v in events if start <= t < end)只统计落在 [start, end) 窗口内的事件,边界规则清晰。
  3. ordered = sorted(self.events, key=lambda e: e[0])批模式的“打烊整理”:先按事件时间排好序,再整体切窗。
  4. watermark = max(watermark, t - self.max_delay)流模式每来一条数据就前推一次水位线,容忍 max_delay 的迟到。
  5. if watermark >= boundary:水位线越过窗口边界才触发计算,像店长确认“账可以算了”才动手。
  6. self._emit(arrived, boundary - win_size, boundary, "stream")增量触发时复用的是同一份 window_sum,体现“API 统一”。
  7. UnifiedPipeline(orders, "streaming").run(5)同一个类只改一个参数就能切换执行模式,正是流批一体的精髓。

常见误区

误区流和批本质一样,区别只是处理速度快慢,把批任务的并行度调低就变成流了。

正解两者是计算模型的不同:批处理面向有界数据,先收齐再整体调度;流处理面向无界数据,来一条处理一条,靠水位线决定何时“算齐”。就像备料是“收工后统一做一大缸”,现制是“来一单做一杯”,不是“做得慢的备料”。

为何常见:因为很多批任务确实可以靠降低延迟跑得“像流”,让人误以为只是性能差异,忽略了有界与无界在触发机制上的本质区别。

误区为了不丢数据,把水位线偏移量 δ 设得很大,这样迟到数据都能等得到。

正解δ 是“等待迟到数据的耐心”,耐心越大,结果产出越迟。δ 过大时,窗口要等很久才触发,实时性被拖垮;δ 过小又丢数据。正确做法是按业务可接受的延迟和迟到比例折中,像店长只等 5 分钟,而不是等一小时。

为何常见:丢数据看起来很可怕,初学者容易忽略“实时性”同样是硬指标,误把水位线当成越大越好的保险丝。

误区处理时间是系统真实时钟,肯定比事件时间可信,统计时直接用处理时间就行。

正解处理时间反映的是“引擎什么时候处理”,不是“业务什么时候发生”。机器一卡,3 点的订单可能被记到 3 点 05 分;只有事件时间(小票时间)才忠实记录业务真相。处理时间唯一准确的用途是监控系统自身健康度。

为何常见:因为直觉上“我看到的时刻”就是真相,忽略了数据在传输和排队中产生的延迟,这正是引入事件时间和水位线的初衷。

误区动态表和数据库物化视图一样,靠定时任务扫全表刷新。

正解动态表是“来一条数据就增量更新一行”,由 Changelog 流驱动,绝不定期全表扫描。连续查询会把 SQL 改写成增量算子,只重算受影响的部分。好比前台看板每卖一杯就 +1,而不是每分钟派人去数一遍全场。

为何常见:大家熟悉传统数据库的物化视图,潜意识里用“定期重算”理解一切,忽略了流式引擎“事件驱动”的本质。

误区Lambda 既能跑批又能跑流,虽然两套代码但都上线了,何必折腾着去统一。

正解Lambda 的两套代码意味着双倍开发、双倍维护,更致命的是两条链路算同一指标可能结果不一致,每晚会话全在对账。流批一体用一套 API 两种执行模式,从根上消灭了对账,Kappa 又弥补了 Lambda 的短板,是架构演进的方向。

为何常见:Lambda 提出早、资料多,很多团队“能用就好”;但只有踩过双链路对账的坑,才会理解统一 API 的价值。

面试题

Q1. 流批一体最核心的思想是什么?

单选题 · easy

  • A.用两套 API 分别开发流和批任务
  • B.一套 API 加一套引擎双执行模式← 正确答案
  • C.把所有离线任务都改造成流任务
  • D.把所有实时任务都改造成批任务

答案:B。流批一体 = 一套 API + 一套引擎,通过切换 STREAMING / BATCH 执行模式同时支持流与批,像一本菜谱管两种火候。A 是两套代码的老路,C、D 是极端化改造,都违背“统一”的初衷。

Q2. 时间语义中「事件时间」指的是什么?

单选题 · easy

  • A.数据进入处理引擎时的时刻
  • B.业务实际发生那一刻的时间← 正确答案
  • C.数据写入备份存储的时刻
  • D.窗口触发进行计算的时间

答案:B。事件时间 = 小票时间,是业务发生时刻;处理时间是引擎处理时刻;注入时间是进入系统的时刻。B 与“小票印的时间”一一对应,其他选项分属处理时间、存储时间和触发时间。

Q3. 关于处理时间与事件时间的关系,正确的是?

单选题 · easy

  • A.处理时间肯定等于事件时间
  • B.事件时间一定晚于处理时间
  • C.处理时间通常晚于事件时间← 正确答案
  • D.两者之间没有任何关联

答案:C。事件先发生、后处理,所以处理时间通常晚于(至少不早于)事件时间,差值即延迟。A 只在理想零延迟下成立,B 把因果倒置,D 忽略了“先发生才能被处理”的约束。

Q4. 水位线(Watermark)的核心作用是什么?

单选题 · mid

  • A.标记最早到达的数据所在窗口
  • B.表示数据足够齐并触发窗口计算← 正确答案
  • C.替代事件时间参与聚合运算
  • D.压缩中间数据以节省存储开销

答案:B。水位线 = 最大事件时间 − 容忍迟到量,它宣告“到这一刻为止的数据视为到齐”,越过窗口边界就触发该窗口计算。它不标记最早数据、不替代事件时间、不压缩存储,B 是其唯一职责。

Q5. 动态表与普通静态表最大的不同是什么?

单选题 · mid

  • A.动态表只能被写入不能查询
  • B.动态表数据量固定不随流变化
  • C.动态表随流入数据持续更新结果← 正确答案
  • D.动态表只用于离线报表的查询

答案:C。动态表把无限流映射成不断变化的表,连续查询随每条新数据增量更新结果,像看板每卖一杯就 +1。A、D 与查询能力矛盾,B 恰好说反了动态的本质。

Q6. Changelog 机制包含哪三类变更操作?

单选题 · mid

  • A.增加、复制、移动三大类操作
  • B.插入、更新、删除三大类操作← 正确答案
  • C.启动、暂停、停止三大类操作
  • D.读取、写入、清空三大类操作

答案:B。Changelog 记录行级变更,用 +I(插入)、-U/+U(更新)、-D(删除)描述结果表的每次变化。B 对应账本上“记一笔、改一笔、划一笔”,其他选项是文件系统或进程概念的混淆。

Q7. 把水位线偏移量 δ 调得过大,会带来什么后果?

单选题 · hard

  • A.结果更及时但数据丢失变多
  • B.窗口结果的产出延迟变大← 正确答案
  • C.事件时间会被系统自动修改
  • D.批处理执行模式无法运行

答案:B。δ 是“等迟到数据的耐心”,等得越久结果出得越晚。δ 过大时,窗口要等水位线慢慢爬到边界,实时性被拖垮。它不会修改事件时间、不丢数据、与批模式无关。

Q8. DataStream 实现流批一体的关键点是什么?

单选题 · hard

  • A.为流批各维护一套不同的算子
  • B.切换执行模式复用同一套代码← 正确答案
  • C.把流式数据全部落盘再计算
  • D.把实时任务全部改成批任务

答案:B。DataStream 统一 API 让 map、filter、window 等算子与执行模式解耦,切到 STREAMING 就逐条增量、切到 BATCH 就整体调度,代码不重写。A 回到两套代码,C、D 都否定了实时能力。

Q9. Lambda 架构存在的主要痛点有哪些?

多选题 · mid

  • A.实时与离线两套代码维护成本高← 正确答案
  • B.两条链路的计算结果容易不一致← 正确答案
  • C.业务指标的计算口径难以统一← 正确答案
  • D.完全无法处理任何实时数据流

答案:ABC。Lambda 用批链算全量、流链算增量,两套代码导致维护贵、对账难、口径分裂,对应 A、B、C。D 说反了,Lambda 恰恰包含实时链路,只是代价高昂。

Q10. 数据源长时间空闲导致水位线不推进时,Flink 可以采用哪些手段恢复窗口触发?

多选题 · extreme

  • A.配置空闲源超时自动推进水位线← 正确答案
  • B.把事件时间改为处理时间语义
  • C.直接删除所有等待触发的窗口
  • D.给数据源打上空闲标记忽略← 正确答案

答案:AD。空闲源没有新事件推水位线,会导致窗口永不触发。Flink 提供空闲源超时机制(withIdleness)自动按其他分区推进水位线,A、D 是官方做法。B 是偷换语义,C 直接丢数据,都不可取。