读者在 数据湖与开放表格式 第 19 章已经看到:Flink 作业按 checkpoint 间隔把 CDC 事件写进 Iceberg,Committer 在 checkpoint 完成后再做表提交。那条链路回答的是 数据怎么落进表。但同一套实时管道里还有另一组问题:乱序到达的点击事件怎么算对「过去五分钟 UV」、作业重启后为什么不会从 offset 0 重算整个历史、状态膨胀和背压从哪来——这些属于 流计算引擎侧,lakehouse 系列刻意留空,由本系列补齐。
本文是流式数据处理系列的 第 1 篇,不教任何引擎的安装命令,而是建立三个会贯穿全系列 18 篇的心智模型:
- 流式数据平台 = 持久日志(Kafka)+ 有状态计算(Flink)+ 交付语义(EOS)+ 下游衔接(湖 / OLTP / 服务)。
- 数据流是可重放的 append-only 日志;与 lakehouse「不可变文件 + 元数据指针」在分层上对称。
- 批、流、微批 的差异不在「有没有窗口」,而在 延迟、吞吐、语义、状态 四个维度如何取舍。
后文默认读者具备基本分布式概念(建议 distributed 系列 里日志复制相关篇),但不假设写过 Flink 算子或调过 Kafka 副本。
环境说明:本机为 WSL2(Linux 6.6.87.2)、i9-12900K / 32 GiB,未安装 JVM、Kafka、Flink。本文概念与架构结论来自 Apache Kafka / Flink 官方文档、Dataflow Model 论文(Akidau et al.);不包含未在本机执行的 benchmark 数字或伪造的命令输出。文末给出可复现的本地实验入口。
版本锚定:Kafka 3.x(KRaft);Flink 1.20+ / 2.x 主线。云托管 MSK / Managed Flink 的内部调度与定价不在本系列范围。
一、批处理、流处理与微批:四个维度
「批 vs 流」常被简化成「离线 vs 实时」,这个二分法在工程上不够用。同一条业务链路里,延迟目标、吞吐形态、容错语义、状态驻留方式 往往同时变化。下表用四个正交维度对比三种典型范式(来源:Flink Documentation Batch and Stream Processing;Dataflow Model 论文 Section 2–3)。
| 维度 | 批处理(Batch) | 流处理(Stream) | 微批(Micro-batch) |
|---|---|---|---|
| 延迟 | 分钟~小时~天(等数据齐) | 毫秒~秒(事件到达即算) | 秒~分钟(按固定间隔切 mini-batch) |
| 吞吐 | 高(顺序扫描、向量化、大 I/O) | 中高(逐条/小批,shuffle 与状态 I/O 开销) | 中高(批内向量化,批间有调度间隙) |
| 语义 | 输入边界固定;失败重跑整批 | 无界输入;checkpoint / savepoint 界定一致性点 | 每 micro-batch 一个 job 或 stage;失败重跑该批 |
| 状态 | 通常无跨批驻留状态;每 job 重算 | Keyed State、窗口 state 跨事件驻留 | 批间 state 可保留(Structured Streaming)或每批重建 |
1.1 批处理:边界清晰的输入集合
经典批 job(MapReduce、Spark batch、SQL
INSERT OVERWRITE ... SELECT)假设
输入在 job 开始前已经确定:HDFS
目录里有哪些文件、分区 dt=2026-06-30
是否齐全。失败时
重跑同一输入边界,输出替换或追加到目标表。状态如果存在,多半是
中间 shuffle spill 或外部 sort,job
结束即释放。
这与 columnar-engine 系列 的 OLAP 扫描模型一致:列存引擎优化的是 大表一次性读透,而不是单条记录的增量更新路径。
1.2 流处理:无界输入与持续计算
流 job 的输入 没有天然结束(除非是有界流,如读历史文件回放)。引擎把到达的 record 当作 无限序列,在 算子本地或 RocksDB 里维护状态(计数、窗口聚合、join 缓冲)。失败重启时不能「从头重扫 HDFS 目录」,而要 从 checkpoint 恢复状态 + source offset(本系列 第 10 篇)。
事件时间(event time)与 watermark 解决乱序:业务时间戳为 10:00:05 的事件可能在 10:00:20 才到达网络。流引擎必须在 不完整输入 下仍输出 可解释的中间结果,并在 watermark 推进后 关闭窗口(第 2、3 篇)。
1.3 微批:用批引擎模拟流
Spark Structured Streaming(早期 DStream 已属遗留 API,本系列不展开)把无界源切成 一系列小 DataFrame,每个 trigger 间隔触发一次 micro-batch job。延迟下界是 trigger 间隔;语义上接近 at-least-once 批叠加,exactly-once 依赖 checkpoint 目录与 sink 幂等(第 18 篇 引擎对照)。
微批的优势是 复用成熟批优化器(Catalyst、Whole-Stage Codegen);代价是 亚秒级延迟 往往不如原生流引擎(Flink、Kafka Streams)直接。选型不在本篇做排名,第 18 篇 给决策树。
1.4 四个维度如何一起读
| 业务诉求 | 优先看的维度 | 典型选择 |
|---|---|---|
| 报表 T+1 | 延迟可高、语义简单 | 批 / 湖仓 SQL |
| 实时大屏秒级 | 延迟 + 状态 | Flink + Kafka |
| 近实时数仓分钟级 | 吞吐 + 运维复杂度 | 微批或 Flink 大窗口 |
| 金融对账 exactly-once | 语义 + 状态 | Flink EOS + Kafka 事务(第 14–15 篇) |
流处理不是「更快的批」,而是 在输入无界、到达乱序的前提下,定义何时输出、如何容错、状态存哪 的另一套问题域。
二、可重放日志:流平台的核心抽象
Kafka 设计文档把 topic 描述为 commit log(来源:Kafka Documentation,Design):分区内的 record 按 单调递增 offset append,consumer 通过 拉取 + 提交 offset 推进进度。这条 log 有三个工程性质:
- 持久化:副本 ISR 保证故障不丢已 ack 的数据(第 5 篇)。
- 可重放:同一 consumer group 换起始 offset,或新 group 从头读,log 内容不变。
- 解耦:producer 与 consumer 通过 log 间接通信,速率不匹配由 consumer lag 显式暴露。
flowchart LR
P["Producer<br/>append"]
L["Partition Log<br/>offset 0,1,2,..."]
C1["Consumer A<br/>offset=2"]
C2["Consumer B<br/>offset=5"]
P --> L
L --> C1
L --> C2
Flink 从 Kafka Source 读数据时,消费进度与算子 state 一起写入 checkpoint(第 10 篇)。因此 log 是真相来源(source of truth),Flink 状态是 为了低延迟计算而衍生的派生数据——丢失 state 可从 log 重放重建(代价是重算时间与资源)。
2.1 与 lakehouse 的分层对称
lakehouse 系列 把 对象存储上的不可变 Parquet/ORC 文件 当作真相,表格式(Iceberg / Hudi / Delta)用 元数据 snapshot 指向「哪些文件属于当前表版本」。流式入湖(lakehouse 第 19 章)是 log → 文件 → snapshot;本系列是 log → 有状态计算 → sink(湖 / 服务 / 另一 topic)。
| 层 | Lakehouse | 流处理(本系列) |
|---|---|---|
| 持久真相 | 对象存储数据文件 + 表 metadata | Kafka partition log(及可选湖表) |
| 增量指针 | snapshot / commit | consumer offset + checkpoint id |
| 计算 | Spark/Flink/Trino 批扫描 | Flink 持续算子 + state |
| 一致性点 | 表 commit(CAS) | checkpoint 完成 + 2PC commit(第 15 篇) |
读者从 lakehouse/19 进入本系列,可把 Committer 提交间隔 与 Flink checkpoint 间隔 理解为 同一旋钮的两端:前者决定湖表可见频率与小文件(第 17 篇),后者决定 状态快照频率与故障恢复 RPO。
2.2 分区内有序、分区间无序
Kafka 只保证单分区内 record 顺序与 offset 单调(来源:Kafka Documentation,Design)。跨分区没有全局顺序。流 job 若要做 全局计数,要么 单分区瓶颈,要么 keyBy 后按 key 局部有序(第 4、8 篇)。这是 水平扩展与顺序性 的经典权衡,与 distributed 系列 里「复制日志有序但分片」同构。
三、Lambda 与 Kappa:架构边界,不是宗教
Lambda 架构(Marz, 2011 前后业界归纳)在工程上常表现为:
- Speed layer:流引擎做低延迟近似或增量。
- Batch layer:批引擎做全量校正、复杂 join。
- Serving layer:合并两层结果给查询。
Kappa 架构(Kreps, 2014)主张 只用流:批处理是对 同一条 log 的历史 replay(换起始 offset、加大并行度),不维护两套代码路径。
本系列 站在「日志即真相、流引擎为主力」一侧,但不否定批式回补:
- 历史回填:新指标要对过去 90 天重算,从 Kafka 保留期或湖表批读往往比拉长流 state TTL 更便宜。
- 复杂全局 join:批扫描两张大表仍比流 join 状态缓冲更易运维。
- 入湖 compaction:lakehouse 第 17 章 的合并是小文件治理,与 Kappa「单管道」并不冲突。
flowchart TB
subgraph lambda ["Lambda(双路径)"]
L1["Kafka log"] --> S["Speed: Flink"]
L1 --> B["Batch: Spark SQL"]
S --> V["Serving / 湖表"]
B --> V
end
subgraph kappa ["Kappa(单 log 多消费模式)"]
L2["Kafka log"] --> F["Flink 实时"]
L2 --> R["Replay 批式读同一 log"]
F --> OUT["下游"]
R --> OUT
end
工程判断:两套代码(Lambda)的维护成本 vs 单流 replay 的资源成本。本系列后续章节提供 checkpoint、state backend、EOS 工具,使 Kappa 路径在生产上可运维;不在此篇宣判某种架构「胜出」。
四、流表对偶与有状态计算
流表对偶(stream-table duality,Flink Documentation Stream Table Duality;Dataflow Model)指:
- 流 → 表:把无限 record 序列按 key + 时间 解释成 随时间变化的表(changelog:insert/update/delete)。
- 表 → 流:把表的每次变更 emit 成 record 流(CDC 即此类,第 16 篇)。
有状态流算子在做的事,往往是 维护一张不可全量物化的「动态表」:
| 算子 | 动态表语义 |
|---|---|
keyBy + sum |
按 key 聚合的累加列 |
| 滚动窗口 count | 每个窗口区间一行聚合结果 |
| 流式 join | 两表按 join key 的临时匹配状态 |
状态 就是这张动态表的
物理存储(内存 HashMap 或 RocksDB LSM,第
9、12 篇)。批 SQL 的 GROUP BY dt
每批重扫;流窗口 state 驻留到 watermark
关闭窗口,磁盘与 checkpoint 体积随 key 基数
× 窗口个数 增长(第 3、13 篇)。
4.1 无状态 vs 有状态算子
| 类型 | 示例 | 失败恢复 |
|---|---|---|
| 无状态 | map、filter、无状态
flatMap |
重放上游即可 |
| 有状态 | keyBy
后聚合、窗口、ProcessFunction + ValueState |
必须恢复 state + offset |
无状态算子 chain 在单 Task 内 forward 传递(第 7
篇);一旦 keyBy,state 按
KeyGroup 分片,shuffle 不可避免(第
8 篇)。
4.2 与 OLTP 的边界
postgresql-kernel 系列 的行存引擎在 进程内 B-Tree 上维护事务一致性;流 state 在 分布式算子 上维护 最终一致或 exactly-once 语义下的派生视图。流处理 不替代 OLTP;CDC 把 OLTP 变更 投影 到 log 再算(lakehouse/19 + 本系列 16–17)。
五、全系列地图:四层栈
本系列 18 篇按 传输 → 计算 → 语义 → 衔接 四层组织(详见 系列 index):
flowchart TB
subgraph transport ["传输层 Kafka 4–6"]
K4["04 日志与分区"]
K5["05 ISR 与 Consumer"]
K6["06 事务 Producer"]
end
subgraph compute ["计算层 Flink 7–13"]
F7["07 运行时"]
F8["08 DataStream"]
F9["09 Keyed State"]
F10["10 Checkpoint"]
F11["11 Savepoint"]
F12["12 RocksDB Backend"]
F13["13 状态调优"]
end
subgraph semantics ["语义层 14–15"]
S14["14 交付语义"]
S15["15 两阶段提交 EOS"]
end
subgraph integrate ["衔接层 16–18"]
I16["16 Debezium CDC"]
I17["17 流式入湖深化"]
I18["18 背压与引擎对照"]
end
BASE["01–03 流处理基础"] --> transport
BASE --> compute
transport --> semantics
compute --> semantics
semantics --> integrate
LH["lakehouse/19 入湖侧"] -.-> S15
LH -.-> I17
LSM["lsm-tree RocksDB"] -.-> F12
5.1 第一部分:流处理基础(第 1–3 篇)
| 篇 | 核心问题 |
|---|---|
| 本篇 | 批/流/微批、日志模型、Lambda/Kappa、系列地图 |
| 第 2 篇 | 事件时间、processing time、watermark、迟到数据 |
| 第 3 篇 | 滚动/滑动/会话窗口、Trigger、Evictor |
读完 1–3 篇,应能回答:为什么乱序下仍要定义 watermark、窗口 state 与批式 GROUP BY 差在哪。
5.2 与先修系列的衔接
| 先修 | 本系列如何使用 |
|---|---|
| lakehouse/19 | 入湖 Writer/Committer;本系列讲引擎侧 watermark/checkpoint |
| lsm-tree | MemTable/SSTable 对照 RocksDB state backend(12–13) |
| distributed | 日志复制、一致性词汇对照 ISR / EOS |
六、端到端数据流:一个 CDC 管道实例
把抽象叠到一个具体形状(不写虚构延迟数字):
sequenceDiagram
participant DB as MySQL
participant DEB as Debezium
participant K as Kafka
participant F as Flink
participant ICE as Iceberg
DB->>DEB: binlog 事件
DEB->>K: append CDC topic
K->>F: Source 消费 partition
Note over F: keyBy + 窗口 / 清洗<br/>state + watermark
F->>F: checkpoint barrier 对齐
F->>ICE: 2PC 提交 snapshot
- Debezium 把行级变更写成 Kafka record(第 16 篇)。
- Flink 按 事件时间 开窗或去重,状态在 RocksDB(第 12 篇)。
- checkpoint 完成 后 Iceberg committer 提交新 snapshot(第 15、17 篇),与 lakehouse/19 的入湖协议对齐。
故障点分布在每一环:Kafka ISR 收缩、Flink checkpoint 超时、Iceberg commit 冲突——第 18 篇 收束诊断清单。
七、交付语义预览:为什么「日志 + checkpoint」不够
Exactly-once 不是 Kafka 或 Flink 单独保证的,而是 Source 语义 × 引擎语义 × Sink 语义 的组合,端到端由 最弱环 决定(第 14 篇):
| 层级 | 典型机制 |
|---|---|
| Source | offset 提交与 checkpoint 绑定 |
| 引擎 | barrier 对齐 state 快照 |
| Sink | 幂等写、事务、2PC |
Kafka 事务 producer(第 6 篇)与 Flink 两阶段提交 sink(第 15 篇)衔接,才能把 「算过了」 与 「写进了」 钉在同一一致性点。本篇只建立词汇;证明与配置留到后文。
八、阅读路径建议
| 读者背景 | 建议路径 |
|---|---|
| 数据平台端到端 | 1 → 4 → 7 → 10 → 15 → 17 → 18 |
| 从 lakehouse/19 来 | 2 → 3 → 10 → 15 → 17 |
| Kafka 运维转流计算 | 4 → 5 → 6 → 7 → 10 → 14 |
| 只关心窗口与乱序 | 1 → 2 → 3 → 8 |
九、有界流与无界流
Flink 把输入分为 bounded 与 unbounded(来源:Flink Documentation,Overview):
| 类型 | 含义 | 典型 Source | 作业结束 |
|---|---|---|---|
| 无界 | 持续到达,无预设 end | Kafka、MQTT、Socket | 手动 cancel 或失败 |
| 有界 | 读完即结束 | 文件 batch read、有限历史 replay | FINISHED 状态 |
有界流 可以 用流引擎跑:读 HDFS/NAS 上固定目录,event time 仍有效——相当于 对静态数据集做流式解释。批引擎与流引擎的边界在此 模糊:Flink 批模式(Batch 执行模式)与 DataStream 读有界源 共享算子库,差异在 调度与 shuffle 实现(本系列不展开 Batch 模式调优)。
无界流作业必须面对 checkpoint forever、state TTL(第 9 篇)、lag 监控(observability 系列)。Kafka log 物理上可删除旧 segment(retention),与 算子 state 保留策略 独立:offset 已 checkpoint 的数据仍可删,但 replay 窗口 受 retention 限制。
stateDiagram-v2
[*] --> Unbounded: Kafka 持续 append
[*] --> Bounded: 文件 Source EOF
Unbounded --> RUNNING: 长期 RUNNING
Bounded --> FINISHED: 读完后 FINISHED
Unbounded --> CANCELED: 运维 cancel
Kappa 历史回补 常把 有界 replay(从旧 offset 读)与 无界 tail 接成同一作业逻辑——log retention 必须 覆盖回补区间。
十、状态、时间与语义:三维依赖
流作业设计常在三轴上同时做决策:
flowchart TB
T["时间语义<br/>event / processing"]
S["状态形态<br/>无 / keyed / 窗口"]
D["交付语义<br/>ALO / EOS"]
T --> S
S --> D
T --> D
| 组合 | 说明 |
|---|---|
| Event time + 窗口 state + EOS | 实时 KPI 对账默认路径(本系列主线) |
| Processing time + 无 state + at-most-once | 监控探针、允许丢失 |
| Event time + 长 session state + ALO | 需下游幂等;checkpoint 仍保 state |
时间语义 决定窗口边界(第 2–3 篇);状态 决定 checkpoint 体积与 RocksDB 压力(第 9–13 篇);交付语义 决定 Kafka / 湖 sink 如何配置(第 14–15 篇)。三维 不可单独选型。
十一、本系列边界与不展开话题
与 系列 index 一致,下列内容 刻意不写:
| 不展开 | 原因 |
|---|---|
| Flink SQL / Kafka Streams DSL 大全 | 机制篇用 DataStream 钉概念 |
| Spark DStream 遗留 API | 业界迁移到 Structured Streaming,仅 第 18 篇 对照 |
| 云厂商 Managed Flink / MSK 内部实现 | 与开源内核机制无关 |
| Pulsar / Redpanda 独立成篇 | 第 18 篇 一句对照 |
承诺展开:Kafka 3.x log + ISR、Flink checkpoint + RocksDB state、EOS 与 lakehouse/19 入湖对齐、Debezium CDC(第 16 篇)、背压与故障(第 18 篇)。
十二、本地实验入口(可选)
下列步骤 未在本写作环境执行,读者可在 Docker Compose(Kafka KRaft + Flink 1.20+)下复现「日志 + 有状态计数」最小闭环:
# 1. 启动 Kafka + Flink(使用官方或 flink-docs 示例 compose,版本自行 pin)
# 2. 创建 topic
kafka-topics.sh --create --topic clicks --partitions 3 --replication-factor 1 ...
# 3. 提交 Flink WordCount 或自带 KafkaSource 的 keyBy+sum 示例
flink run -p 3 examples/streaming/WordCount.jar
# 4. 用 console producer 写入 JSON:{"user_id":"u1","ts":1710000000000}
# 5. 观察 Flink Web UI:Source subtask lag、算子背压(背压详解见第 18 篇)实验目的:验证 log 可重放、offset 与 subtask 绑定、有状态算子重启后从 checkpoint 继续——不在此篇采集吞吐数据。
十三、术语表
| 术语 | 含义 |
|---|---|
| Event time | record 业务发生时间,由字段携带 |
| Processing time | 算子机器本地时钟处理时刻 |
| Watermark | 事件时间进度标记,驱动窗口关闭(第 2 篇) |
| Checkpoint | 分布式一致性快照,含 state + offset(第 10 篇) |
| Keyed State | 按 key 分片的有状态存储(第 9 篇) |
| Consumer lag | 消费 offset 与 log end 的差 |
| EOS | End-to-end exactly-once 语义(第 14–15 篇) |
| Changelog | 表变更流,insert/update/delete 序列 |
十四、常见问题
| 问题 | 简短回答 | 深入 |
|---|---|---|
| 流处理能否完全取代批? | 否;历史回填、超大 join、低成本 ad-hoc 扫描仍偏批 | 第三节 |
| Kafka 是否必须? | 本系列以 Kafka 为主线;其他 log(Pulsar 等)在第 18 篇对照一句带过 | 第 4–6 篇 |
| 入湖小文件谁负责? | 引擎 checkpoint 间隔 + 表 compaction 治理共同决定 | lakehouse/17、本系列 17 |
| Flink SQL 学不学? | 本系列用 DataStream 讲机制;SQL 为表层语法,不单独成篇 | index 边界说明 |
十五、小结
流处理的核心不是「把批 job 跑快一点」,而是在 无界、乱序、可重放日志 上定义 何时输出、状态存哪、如何容错。Kafka 提供 分区有序的可持久 log;Flink 提供 有状态计算与 checkpoint;EOS 与入湖需要 与 lakehouse 表提交协议对齐(lakehouse/19、本系列 15、17)。
下一篇进入 事件时间、处理时间与 watermark:三种时间语义如何在同一作业里共存,以及乱序下窗口何时该关闭。
参考资料
- Apache Kafka Documentation, Design(commit log、分区有序性)。
- Apache Flink Documentation, Batch and Stream Processing / Stream Table Duality。
- Akidau, T. et al., The Dataflow Model(事件时间、watermark、窗口抽象)。
- Kreps, J., Questioning the Lambda Architecture(Kappa 与 log replay 论述,B 级工程观点)。
- 本系列 index(18 篇依赖与阅读路径)。
- lakehouse 第 19 章(入湖侧与引擎侧分工)。
- distributed 系列(日志复制与一致性直觉)。
返回 系列目录 | 上一篇:系列 index | 下一篇:事件时间、处理时间与 Watermark
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【流式数据处理】背压、故障模式与引擎对照
收束流式数据处理系列:Flink credit-based 背压如何沿算子链传播、Web UI 指标怎么读;数据倾斜、checkpoint 超时连锁、Kafka rebalance 风暴、RocksDB OOM、savepoint 不兼容五类生产故障的诊断与止血;Flink / Kafka Streams / Spark Structured Streaming / RisingWave 在状态模型、交付语义、运维与入湖成熟度上的对照表与选型决策树,不做排名。
【流式数据处理】Kafka · Flink · 状态 · Exactly-Once
承接数据湖流式入湖:从 Kafka 日志与副本语义,到 Flink 事件时间、watermark、窗口、RocksDB 状态与 checkpoint,再到端到端 exactly-once 与 Debezium CDC 入湖。面向数据平台与实时工程师,补全批式湖仓之外的实时计算层。
【流式数据处理】Checkpoint 机制:Barrier 对齐与一致性快照
从 Chandy-Lamport 分布式快照到 Flink aligned/unaligned checkpoint:CheckpointCoordinator 触发—ack—完成生命周期,Kafka source 如何把 partition offset 写入 checkpoint,以及 interval、timeout、min-pause、concurrent checkpoints 的调优边界。
【流式数据处理】交付语义:从 at-most-once 到 exactly-once
用 Source、引擎、Sink 三层模型拆解 at-most-once、at-least-once、exactly-once 的组合规则与最弱环决定律;对照 Flink checkpoint 模式、Kafka 事务与幂等 producer、重复消费/重复写入的三类修复手段,为两阶段提交 sink 铺垫。