大数据系列(五) Flink:真正的实时流处理,毫秒级延迟怎么做到的?
Flink:真正的实时流处理,毫秒级延迟怎么做到的?
大数据系列第 5 篇:Spark Streaming 是"伪实时"?来看看原生流处理引擎 Flink 是怎么做到毫秒级延迟的。
先搞清楚一个问题:什么叫"实时"?
咱们经常听到"实时计算"这个词,但不同人理解的"实时"差别可大了:
- 老板理解的实时:我刷新一下页面,数据就更新了(秒级)
- 风控系统理解的实时:用户刚提交一笔交易,毫秒级内判断是不是欺诈(毫秒级)
- IoT 系统理解的实时:传感器数据产生后,立刻触发告警(亚秒级)
Spark Streaming 的微批次模型,延迟通常在1-5 秒。对于大多数业务场景(比如实时报表、实时监控),这够用了。但对于风控、高频交易、实时推荐这些场景,秒级延迟可能意味着几百万的损失。
这时候,Flink 就登场了。
Flink 的核心设计理念:流处理是本质,批处理是特例
Flink 和 Spark 在流处理上的根本分歧在于:
| 框架 | 核心模型 | 流处理实现 | 延迟 |
|---|---|---|---|
| Spark Streaming | 批处理 | 把流切成微批次 | 秒级 |
| Flink | 流处理 | 逐条处理数据 | 毫秒级 |
Flink 的团队认为:批处理只是"有界流"(数据有明确的开始和结束),流处理是更通用的模型。
┌─────────────────────────────────────────────────────────────────┐ │ Flink 的流批一体视角 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 传统视角: │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 批处理 │ 不一样 │ 流处理 │ │ │ │ 历史数据 │ │ 实时数据 │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ Flink 视角: │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 流处理(Stream Processing) │ │ │ │ │ │ │ │ 有界流(Bounded) 无界流(Unbounded) │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ │ │ 数据有开始和结束 │ │ 数据持续产生 │ │ │ │ │ │ │ │ 没有明确结束 │ │ │ │ │ │ 例如:HDFS 文件 │ │ 例如:Kafka 日志 │ │ │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ │ │ │ │ │ 批处理 = 有界流上的流处理 │ │ │ │ 流处理 = 无界流上的流处理 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘同一套 API,同一套引擎,有界流和无界流都能处理。这就是 Flink 说的"流批一体"。
Flink 的架构:JobManager + TaskManager
Flink 的架构和 Spark 有点像,也是主从结构:
┌─────────────────────────────────────────────────────────────────┐ │ Flink 架构(人话版) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ JobManager("项目经理") │ │ │ │ │ │ │ │ • 接收作业提交,生成执行计划 │ │ │ │ • 把任务分配给 TaskManager │ │ │ │ • 定时触发 Checkpoint(全局快照) │ │ │ │ • 发现故障时,协调恢复 │ │ │ │ │ │ │ │ 高可用:多个 JobManager,ZooKeeper 选主 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ │ 分配任务 / 心跳检测 │ │ │ │ │ ┌───────────────────────────┼───────────────────────────┐ │ │ │ TaskManager 集群("干活的小弟") │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ TM1 │ │ TM2 │ │ TM3 │ │ TM4 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Slot 1 │ │ Slot 1 │ │ Slot 1 │ │ Slot 1 │ │ │ │ │ │ Slot 2 │ │ Slot 2 │ │ Slot 2 │ │ Slot 2 │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ │ │ │ 每个 Slot 是一个资源单元,运行一个 Task 的并行实例 │ │ │ │ 多个 Slot 共享 TM 的 JVM 进程 │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘JobManager是"项目经理",负责统筹全局。TaskManager是"干活的小弟",负责执行具体的计算任务。每个 TaskManager 里有若干个Slot,相当于"工位",一个 Slot 跑一个 Task。
时间语义:数据处理的时间,到底用哪个?
这是流处理里最容易让人迷糊的概念。Flink 支持三种时间:
Processing Time:处理时间(机器本地时间)
数据到达 Flink 算子时,算子所在机器的当前时间。
数据产生时间:10:00:01 网络传输延迟:2 秒 到达 Flink 时间:10:00:03 Processing Time:10:00:03 特点:简单、低延迟、但结果不确定 适用:实时监控大屏、对精确性要求不高的场景问题:如果机器负载高,处理变慢了,同样一条数据可能被分到不同的窗口里。结果就不确定了。
Event Time:事件时间(数据自带的时间戳)
数据本身携带的时间,比如日志里的timestamp字段。
数据内容:{"user_id": 123, "action": "click", "timestamp": "10:00:01"} Event Time:10:00:01(数据里的 timestamp) 特点:结果确定、能处理乱序数据 适用:计费、统计报表、需要准确结果的场景问题:数据可能乱序到达。比如 10:00:03 的数据先到了,10:00:01 的数据后到。怎么知道"10:00:00-10:00:05 这个窗口的数据都到齐了"?
Flink 的解决方案是Watermark(水位线)。
Watermark:处理乱序数据的神器
Watermark 是 Flink 最核心的创新之一。它的作用就是告诉系统:“Event Time 小于等于 X 的数据,应该都已经到了。”
┌─────────────────────────────────────────────────────────────────┐ │ Watermark 机制示意 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Event Time 轴(数据实际产生的时间) │ │ ─────────────────────────────────────────────────────────► │ │ 0 1 2 3 4 5 6 7 8 9 10 │ │ │ │ 数据到达顺序(乱序): │ │ │ │ Event Time: 3 1 5 2 7 4 6 8 9 10 │ │ ● ● ● ● ● ● ● ● ● ● │ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │ │ Watermark: -2 -2 -2 -2 -1 -1 -1 0 0 0 │ │ (允许 3 秒乱序) │ │ │ │ Watermark = 当前最大 Event Time - 允许的最大乱序时间 │ │ │ │ 窗口 [0, 5) 什么时候触发? │ │ • 当 Watermark ≥ 5 时触发 │ │ • 也就是 Event Time 达到 8 的时候(Watermark = 8 - 3 = 5) │ │ • 此时认为 Event Time ≤ 5 的数据都已经到了 │ │ │ │ 如果还有迟到的数据(Event Time ≤ 5 但 Watermark 之后才到)? │ │ • 允许迟到(Allowed Lateness):窗口触发后再等一段时间 │ │ • 侧输出流(Side Output):超时的数据放到单独的流里处理 │ │ │ └─────────────────────────────────────────────────────────────────┘Watermark 的本质是用延迟换准确性。你允许数据乱序 3 秒,那窗口就要多等 3 秒才触发。乱序时间越长,延迟越大,但结果越准确。
// 设置 Event Time 和 Watermarkenv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);dataStream.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forBoundedOutOfOrderness(Duration.ofSeconds(3))// 允许 3 秒乱序.withTimestampAssigner((event,timestamp)->event.getEventTime())).keyBy(event->event.getUserId()).window(TumblingEventTimeWindows.of(Time.minutes(1)))// 1 分钟滚动窗口.allowedLateness(Time.seconds(10))// 窗口触发后,再允许 10 秒迟到.aggregate(newCountAggregate());窗口机制:无界流怎么切分成有界数据集?
窗口是流处理的核心概念,Flink 提供了丰富的窗口类型:
┌─────────────────────────────────────────────────────────────────┐ │ Flink 窗口类型(人话版) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. 滚动窗口(Tumbling Window) │ │ │ │ 时间: 0 5 10 15 20 25 30 │ │ 窗口: [0,5) [5,10) [10,15) [15,20) [20,25) [25,30) │ │ ├────┤├────┤├─────┤├─────┤├─────┤├─────┤ │ │ 窗口之间不重叠,固定大小 │ │ 适用:每 5 分钟统计一次 PV/UV │ │ │ │ 2. 滑动窗口(Sliding Window) │ │ │ │ 时间: 0 5 10 15 20 25 30 │ │ 窗口: [0,10) │ │ [5,15) │ │ [10,20) │ │ [15,25) │ │ [20,30) │ │ 窗口大小 10s,滑动步长 5s,窗口之间有重叠 │ │ 适用:计算最近 10 分钟的平均值,每 5 分钟更新一次 │ │ │ │ 3. 会话窗口(Session Window) │ │ │ │ 数据: ● ● ● ● ● ● │ │ 时间: 0 5 10 12 18 30 32 │ │ 窗口: [0,10] (10 秒没数据,窗口关闭) │ │ [10,18] (6 秒 < 10 秒间隔,同一窗口) │ │ [30,32] (新会话) │ │ 动态大小,由数据活动间隙触发关闭 │ │ 适用:用户行为分析,一个会话内的操作 │ │ │ └─────────────────────────────────────────────────────────────────┘Checkpoint:故障了怎么办?
分布式环境下机器随时可能挂,Flink 怎么保证"数据不丢、结果不重"?
答案是Checkpoint(检查点)——定期给作业拍个"快照",保存所有算子的状态。故障时从最近的快照恢复,重新处理。
┌─────────────────────────────────────────────────────────────────┐ │ Flink Checkpoint 机制(简化版) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ JobManager │ │ │ │ │ │ "大家注意,开始 Checkpoint 了!" │ │ ▼ │ │ Source ──→ [Map] ──→ [KeyBy] ──→ [Window] ──→ [Sink] │ │ │ │ │ │ │ │ │ │ 收到 Checkpoint 信号,保存自己的状态(如 Kafka 偏移量) │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ │ Barrier ─────────────────────────────────────────────► │ │ (屏障,像游泳比赛的发令枪) │ │ │ │ 每个算子收到 Barrier 后: │ │ 1. 暂停处理新数据 │ │ 2. 把当前状态(如窗口里的数据、计数器的值)保存到持久存储 │ │ 3. 确认保存成功后,继续处理数据 │ │ │ │ 所有算子都确认后,这个 Checkpoint 就算完成了 │ │ │ │ 故障恢复: │ │ • 从最近的 Checkpoint 恢复所有算子的状态 │ │ • Source 从保存的偏移量重新消费数据 │ │ • 保证 "恰好一次"(Exactly-Once)处理 │ │ │ └─────────────────────────────────────────────────────────────────┘Checkpoint 基于Chandy-Lamport 分布式快照算法。核心思想是用一个 Barrier 把数据流切成"快照前"和"快照后"两个阶段,保证快照的一致性。
Flink 的 Checkpoint 有几个特点:
- 异步执行:保存状态的过程不阻塞数据流处理
- 增量 Checkpoint:只保存变化的部分,减少开销
- 可配置间隔:默认几秒到几分钟一次,根据业务需求调整
Exactly-Once:端到端的一致性保证
Checkpoint 保证了 Flink 内部的状态一致性,但数据最终要写到外部系统(如 Kafka、MySQL、HBase)。如果 Checkpoint 成功了,但 Sink 写数据失败了,怎么办?
Flink 提供了两种方案:
方案一:幂等写入
Sink 支持幂等更新(同样的数据写多次,结果一样)。比如:
- HBase:同样的 rowkey 写多次,结果覆盖为同一个值
- Elasticsearch:同样的 document ID 写多次,结果一样
方案二:两阶段提交(2PC)
┌─────────────────────────────────────────────────────────────────┐ │ 两阶段提交(Two-Phase Commit) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Flink Job Kafka Producer Kafka Broker │ │ │ │ │ │ │ │ Checkpoint 时 │ 1. preCommit() │ │ │ │───────────────────►│─────────────────────►│ 预提交事务 │ │ │ │ │ │ │ │ Checkpoint 成功 │ 2. commit() │ │ │ │───────────────────►│─────────────────────►│ 正式提交 │ │ │ │ │ │ │ │ Checkpoint 失败 │ 3. abort() │ │ │ │───────────────────►│─────────────────────►│ 回滚事务 │ │ │ │ 关键点: │ │ • Checkpoint 成功 = 数据一定已经写入外部系统 │ │ • Checkpoint 失败 = 数据不会写入外部系统(回滚) │ │ • 实现端到端的 Exactly-Once 语义 │ │ │ └─────────────────────────────────────────────────────────────────┘两阶段提交的核心逻辑:
- 预提交(Pre-Commit):Checkpoint 时,Sink 先把数据写到外部系统,但不正式提交
- 正式提交(Commit):Checkpoint 成功后,Sink 正式提交事务
- 回滚(Abort):Checkpoint 失败时,Sink 回滚事务,数据不会真正写入
Kafka Sink、JDBC Sink 等都支持两阶段提交。
Flink vs Spark Streaming:到底选哪个?
| 维度 | Flink | Spark Streaming |
|---|---|---|
| 处理模型 | 原生流处理(逐条处理) | 微批次(切小片处理) |
| 延迟 | 毫秒级(< 100ms) | 秒级(1-5s) |
| 时间语义 | Event Time 原生支持 | Structured Streaming 支持 |
| 状态管理 | 内置,强大 | 需借助外部系统 |
| Checkpoint | 轻量级,异步 | 基于 RDD Checkpoint(写 HDFS) |
| Exactly-Once | 原生支持端到端 | Structured Streaming 支持 |
| 反压(Backpressure) | 自动 | 需手动配置 |
| SQL 支持 | Flink SQL / Table API | Spark SQL |
| 机器学习 | 有限支持 | MLlib 完善 |
| 生态成熟度 | 流处理生态强 | 批处理生态强 |
选型建议:
- 需要毫秒级延迟(风控、实时推荐、IoT 告警)→Flink
- 需要秒级延迟,且已有 Spark 生态(实时报表、监控)→Spark Streaming / Structured Streaming
- 需要批流统一,且以批处理为主 →Spark
- 需要真正的流批一体,以流处理为主 →Flink
小结
今天咱们聊了 Flink:
- 核心设计:流处理是本质,批处理是特例(有界流)
- 时间语义:Processing Time(简单但不准)、Event Time(准确但需要 Watermark)
- Watermark:用延迟换准确性,处理乱序数据的神器
- 窗口:滚动、滑动、会话,满足各种统计需求
- Checkpoint:分布式快照,保证故障恢复不丢数据
- Exactly-Once:两阶段提交实现端到端一致性
Flink 的价值在于:它证明了低延迟和强一致性不是二选一。通过 Watermark 处理乱序、通过 Checkpoint 保证容错、通过两阶段提交实现端到端一致,Flink 在毫秒级延迟的场景下提供了企业级的可靠性保证。
你在生产环境用过 Flink 吗?是处理什么场景的?有没有被 Watermark 和窗口的交互搞晕过?欢迎聊聊~
