2.1"语义保证"(Delivery Guarantees)
语义保证是衡量系统可靠性的核心指标。在数据恢复中起着关键作用,但并不是只影响故障恢复。
- At-Most-Once语义:任务在无故障运行时,可能会出现数据丢失。在数据处理过程中,由于网络抖动等原因,部分数据在算子传递到输入日志文件过程中丢失,但系统不会进行重处理,这就导致数据丢失。
- At-Least-Once语义:即使任务无故障运行,也可能出现数据重复处理的情况。数据在网络传输或算子处理过程中,可能会因为一些异步操作或缓冲区的问题,导致部分数据被多次处理。
- At-Most-Once(最多一次)
- 定义:数据可能被处理 0 次或 1 次,但绝不会被重复处理。
即:如果数据在传输或处理过程中丢失,系统不会重试,直接跳过。
- 实现原理:
不做任何容错机制,数据从源端发出后,不记录偏移量(Offset)、不保存状态快照(Checkpoint),处理完成后也不确认 "已处理"。一旦发生故障,未处理的数据会丢失,且不会重新处理。
- 适用场景:
对数据准确性要求极低,更看重处理速度和资源开销的场景,例如:
-
- 实时监控非关键指标(如网站瞬时访问量,丢失少量数据不影响整体趋势);
- 日志采集的 "尽力而为" 模式(如非核心业务的调试日志)。
- 优缺点:
- 优点:性能最好(无容错开销)、延迟最低。
- 缺点:数据可能丢失,准确性无法保证。
- At-Least-Once(至少一次)
- 定义:数据至少被处理 1 次,但可能被重复处理。
即:确保数据不会丢失,但可能因重试导致重复。
- 实现原理:
- 记录数据处理的进度(如 Kafka 的 Offset),但 "标记已处理" 的操作可能滞后于实际处理;
- 发生故障时,从最近的进度记录(如未更新的 Offset)重新处理数据,导致部分数据被重复处理。
例如:处理完数据后,先输出结果,再更新 Offset。若更新 Offset 前发生故障,重启后会从旧 Offset 重新处理,导致结果重复。
- 适用场景:
允许数据重复,但绝对不能丢失的场景,例如:
-
- 计费相关的初步数据采集(重复可后续通过幂等性处理修正);
- 实时报警(重复报警总比漏报好)。
实时ETL:保证不丢失,允许重复但可控
- 优缺点:
- 优点:保证数据不丢失,容错机制简单(仅需记录进度)。
- 缺点:可能产生重复数据,需下游系统做去重处理(如依赖业务主键去重)。
- Exactly-Once(精确一次)
- 定义:数据恰好被处理 1 次,既不丢失也不重复,是最高级别的语义保证。
- 实现原理:
结合了 "状态快照" 和 "分布式事务":
-
- Checkpoint 机制:定期将处理状态(如计算中间结果、Offset)保存为快照;
- 两阶段提交(2PC):确保 "处理数据" 和 "更新进度 / 输出结果" 的原子性(要么都成功,要么都失败)。
例如:Flink 的 Exactly-Once 通过 Checkpoint + 与外部系统(如 Kafka、MySQL)的事务集成实现,故障恢复时从 Checkpoint 恢复状态,确保数据不会重复处理。
- 适用场景:
- 对数据准确性要求极高的核心业务,例如:金融交易(如实时对账、转账金额统计);
- 关键指标计量(如用户付费转化、订单量统计)。
- 优缺点:
- 优点:数据零丢失、零重复,准确性最高。
- 缺点:实现复杂,会引入一定的性能开销(Checkpoint、事务处理),延迟略高。
- 三者对比与选择建议
|---------------|----------|----------|----------|----------------|
| 语义 | 数据丢失 | 数据重复 | 性能开销 | 适用场景 |
| At-Most-Once | 可能 | 不会 | 极低 | 非关键监控、日志采集 |
| At-Least-Once | 不会 | 可能 | 中 | 允许重复的核心数据采集 |
| Exactly-Once | 不会 | 不会 | 较高 | 金融交易、精确计量等核心场景 |
补充:如何实现更高语义?
- At-Least-Once + 幂等性:若下游系统支持幂等操作(如根据唯一 ID 更新数据,重复执行结果不变),可通过 "At-Least-Once 处理 + 下游去重" 间接实现类似 Exactly-Once 的效果,且性能更好。
- 端到端语义:上述语义通常指 "Flink 内部处理",若需保证 "从 Source 到 Sink 全链路" 的语义,需 Source 和 Sink 都支持事务(如 Kafka 作为 Source,MySQL 作为 Sink 时使用事务写入)。
理解这三种语义是设计流处理系统的基础,需根据业务对 "准确性" 和 "性能" 的要求选择合适的方案,并非所有场景都需要追求 Exactly-Once。
- 性能开销差异:核心源于容错机制的复杂度
性能开销主要体现在 CPU、内存、I/O 三个维度,本质是 "为了保证语义可靠性,系统需要额外做多少工作"。
|-------------------|-----------------------------------------------------------------|-----------------------------------------------------|----------------------------------------------------------------|------------------------------------------------------------------------------------|-----------------|
| 语义类型 | 核心容错逻辑 | CPU 开销 | 内存开销 | I/O 开销 | 整体开销对比(相对值) |
| At-Most-Once | 无任何容错:不记录偏移量(Offset)、不做状态快照(Checkpoint)、不确认处理结果。 | 几乎无额外开销(仅处理业务逻辑)。 | 无需缓存状态数据(如中间计算结果、Offset),内存仅用于实时业务处理。 | 无额外 I/O(不写快照、不更新 Offset 日志、不做事务确认)。 | 基准值(1x) |
| At-Least-Once | 轻量容错:仅记录处理进度(如 Kafka Offset),但 "更新进度" 可能滞后于 "业务处理"。 | 略增(需周期性序列化 / 反序列化 Offset 等轻量状态),额外 CPU 占比通常<10%。 | 需缓存少量状态(如当前消费的 Offset),内存占用增加通常<20%。 | 中等增加(周期性写入 Offset 到本地或分布式存储,如 Flink 的状态后端),I/O 吞吐量增加 10%-30%。 | 1.1x - 1.3x |
| Exactly-Once | 复杂容错: 1. 定期做全量状态快照(Checkpoint); 2. 用两阶段提交(2PC)保证 "处理 + 输出" 原子性。 | 显著增加(状态序列化 / 反序列化、2PC 协调、事务日志处理),额外 CPU 占比 20%-50%。 | 需缓存大量状态(全量中间计算结果、Offset、事务元数据),内存占用增加 50%-150%(若状态大,可能依赖磁盘溢出)。 | 大幅增加(Checkpoint 快照写入分布式存储如 HDFS/S3、2PC 的预提交 / 确认日志、与外部系统的事务交互),I/O 吞吐量增加 50%-200%。 | 1.5x - 3x |
关键影响因素(为什么差异不固定?)
- 状态大小:Exactly-Once 的 Checkpoint 开销与状态大小强相关。若业务状态很小(如仅统计计数),Checkpoint 耗时短,开销接近 At-Least-Once;若状态极大(如保存百万用户的会话数据),Checkpoint 序列化 / 传输耗时剧增,开销可能达到 At-Most-Once 的 3 倍以上。
- Checkpoint 频率:Flink 默认 Checkpoint 间隔为 100ms-1s,间隔越短,Exactly-Once 的 I/O 开销越大(频繁写快照),但故障恢复数据丢失越少;间隔越长,开销降低,但恢复时重复处理的数据越多。
- 外部系统兼容性:若 Sink(如 MySQL)支持事务(如 Flink 的 JDBC Sink 用事务写入),Exactly-Once 无需额外开销;若不支持,需通过 "幂等写入 + 状态记录" 间接实现,CPU / 内存开销会进一步增加。
- 数据延迟差异:核心源于 "同步 / 异步容错操作"
数据延迟指 "数据从 Source 产生到 Sink 输出的时间差",容错操作若为同步(需等待操作完成再处理下一条数据),延迟会显著增加;若为异步(后台并行处理,不阻塞业务逻辑),延迟影响较小。
|-------------------|------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------|---------------------------|
| 语义类型 | 延迟机制 | 典型延迟范围(基于普通业务场景) | 延迟对比(相对 At-Most-Once) |
| At-Most-Once | 无任何同步容错操作,数据 "来了就处理,处理完就输出",延迟仅由业务逻辑决定。 | 毫秒级(1ms-10ms) | 基准值(1x) |
| At-Least-Once | 仅异步更新 Offset(如后台周期性写入 Offset,不阻塞数据处理),延迟增加极小。 | 毫秒级(1ms-15ms) | 1x - 1.2x |
| Exactly-Once | 存在同步等待逻辑: 1. Checkpoint 触发时,需等待所有算子完成状态快照(短暂阻塞数据流入); 2. 2PC 提交时,需等待 Sink 确认事务提交(如 MySQL 事务落盘)。 | 低延迟场景(Checkpoint 间隔 100ms):10ms-50ms; 高可靠性场景(Checkpoint 间隔 1s):50ms-200ms。 | 1.5x - 20x(极端场景) |
延迟差异的直观案例
- 日志采集场景(At-Most-Once):数据从 Kafka 流入 Flink,仅做简单过滤后输出到 Elasticsearch,延迟稳定在 5ms 左右。
- 用户行为统计(At-Least-Once):统计实时 UV,异步更新 Kafka Offset,延迟约 8ms,偶尔因 Offset 写入波动到 12ms。
- 实时对账(Exactly-Once):对接银行流水和订单系统,Checkpoint 间隔 500ms,用 2PC 保证事务一致性,延迟通常在 30ms-80ms,Checkpoint 触发瞬间可能飙升到 100ms 以上。
- 总结:如何根据业务选择?
- 优先选 At-Most-Once:若业务对 "数据丢失" 不敏感,只追求极致性能和低延迟(如非核心业务的实时监控、日志预览)。
- 优先选 At-Least-Once:若业务 "绝对不能丢数据",但能接受重复(且下游可通过主键去重,如用户行为埋点数据),同时希望平衡性能和可靠性(如电商实时销量统计,重复数据可通过订单 ID 去重)。
- 必须选 Exactly-Once:若业务 "既不能丢也不能重",且下游无法去重(如金融交易、实时计费),即使付出性能和延迟代价也必须保证准确性。
- 补充:Flink 中的优化技巧(降低高语义的开销)
- 对 Exactly-Once:调大 Checkpoint 间隔(如从 100ms 改为 500ms),减少快照频率(代价是故障恢复时重复数据增多);
- 使用增量 Checkpoint(仅保存与上一次快照的差异数据),降低 I/O 和内存开销;
- 选择高性能状态后端(如 RocksDB,支持状态磁盘溢出,减少内存压力)。
- 对 At-Least-Once:异步更新 Offset(Flink 默认配置),避免同步写入阻塞;
- 若下游支持幂等,可直接用 At-Least-Once + 幂等写入,替代 Exactly-Once(性能提升 30%+)。
通过以上优化,可在保证业务语义的前提下,大幅缩小不同语义间的性能和延迟差距。
2.2 时间语义:解决数据乱序
- 事件时间(Event Time)
数据产生的实际时间(如日志中的create_time),最符合业务逻辑。几乎都用事件时间 ,需指定 "时间戳提取器" 和 "水印(Watermark)" 来处理乱序数据。
- Processing Time(处理时间)
- 定义:指 Flink 作业算子处理数据元素时机器的系统时间。简单来说,就是数据进入 Flink 算子那一瞬间,机器记录的时间戳。
- 区别于 Ingestion Time:它是基于每个算子本地系统时钟的时间,不同算子处理数据时间可能不同。如果数据在算子间传输有延迟,那每个算子记录的 Processing Time 会有差异。
- 适用场景:当你不太关心数据真实产生时间,只关注系统实时处理速度,对数据准确性要求不是极高时使用。比如监控系统实时流量,只要大概知道当前系统每秒处理了多少数据,不要求精确到数据产生的那一刻。
- Ingestion Time(摄入时间)
- 定义:是数据进入 Flink 作业的第一个算子(比如 Kafka Source)时,Flink 给数据记录的时间戳。这个时间一旦确定,在后续算子处理过程中基本保持不变。
- 区别于 Processing Time:Ingestion Time 在数据进入作业源头就确定了,之后不管数据在作业内如何流转,这个时间不变;而 Processing Time 在每个算子处理时重新记录。所以 Ingestion Time 更能反映数据进入作业的整体时间顺序,而 Processing Time 受算子处理速度影响更大。
- 适用场景:适用于对数据进入系统的时间顺序比较敏感,且能接受一定程度数据乱序情况。例如记录用户行为数据进入系统的时间,即使数据在后续处理中有些许乱序,依然可以根据摄入时间进行分析。
2.3 窗口(Window**)**
- 滚动窗口**(Tumbling** Window)
- 描述:窗口大小固定,且相互之间不重叠,像切香肠一样均匀分段。如设置滚动窗口大小为 5 分钟,那么数据就会被按照每 5 分钟进行划分,9:00-9:05 的订单为一批,9:05-9:10 的订单为下一批。
- 图表示意:假设时间轴为水平方向,从左向右推进,每一个滚动窗口就是一个固定长度的矩形块,依次排列,块与块之间没有间隙和重叠。

- 适用场景:适用于统计每小时销售额、每天用户活跃数等,对固定时间间隔内的数据进行独立的聚合计算。
- 滑动窗口**(Sliding** Window)
- 描述:窗口有固定大小和固定滑动间隔,窗口之间会有重叠。比如设置窗口大小为 10 分钟,滑动间隔为 5 分钟,就会每隔 5 分钟得到一个包含过去 10 分钟数据的窗口,如 9:00-9:10、9:05-9:15。
- 图表示意:同样以水平时间轴为例,窗口是一个个长度为 10 分钟的矩形,但相邻矩形之间有 5 分钟的重叠部分,看起来像窗口在时间轴上滑动。

-
-
- 窗口大小**(window size)**:上图窗口大小为4
- 滑动步长(window slide):代表窗口计算的频率,上图步长为2
-
当滑动步长小于窗口大小时,滑动窗口就会出现重叠,这时数据也可能会被同时分配到多个窗口中。而具体的个数就由窗口大小和滑动步长的比值(size/slide)来决定。
- 适用场景:实时监控系统负载,比如每隔 1 分钟统计过去 5 分钟的 CPU 使用率,能避免某段时间数据量突然增大导致结果波动,适用于需要更频繁计算和细粒度分析的场景。
- 会话窗口**(Session** Window)
- 描述:根据数据之间的间隔动态划分窗口,没有固定的开始或结束时间。如果顾客点单间隔超过 15 分钟,就视为 "新会话",如顾客 9:00 下单,9:05 加菜,9:20 再次下单,则分为两个窗口(9:00-9:05 和 9:20 - 未定)。
- 图表示意:在时间轴上,窗口的长度和位置不固定,取决于数据事件的间隔,表现为不连续、长度不一的区间。

- 适用场景:分析用户行为会话,如网页浏览、游戏在线时长等。
- 全局窗口(Global Window)
- 描述:全局窗口会将拥有相同 key 的所有数据都分发到一个窗口中,包含流中的所有数据。
- 图表示意:在时间轴上可以看作是一个无限长的窗口,涵盖了所有数据。
- 适用场景:适用于需要对整个流进行一次性计算的情况。
2.4 **触发条件(Trigger****)**
- 基于时间的触发条件
- Processing Time(处理时间)
*- 要点:依据算子处理数据时机器的系统时间触发。简单直接,反映数据在系统内的即时处理情况,但受系统负载影响,不同算子处理时间可能不同。仅负责根据事件时间触发窗口计算。
-
-
- 场景举例:在实时监控系统的网络流量场景中,假设要每 5 秒统计一次当前 5 秒内的网络流量数据。由于我们更关注当前系统实时处理的数据量,对流量数据的精确时间戳要求不高,此时可使用 Processing TimeTrigger。系统时钟每到 5 秒的时间间隔,就触发窗口计算,统计这 5 秒内流经系统的网络流量总和。
-
- Event Time(事件时间)
*- 要点:根据数据自身携带的时间戳触发。能精准体现数据实际发生的先后顺序,不受数据传输和处理延迟干扰,但需合理设置水印应对乱序数据。当事件时间达到窗口结束时间(结合水印机制处理乱序数据后),它会触发窗口内数据的计算操作,但默认情况下,并不会自动清除窗口内的数据。
-
-
- 场景举例:以电商平台的订单支付场景为例,要统计每天的订单支付金额。订单数据带有支付时间戳,这就是事件时间。我们以一天为窗口,当事件时间到达每天的结束时刻(如 23:59:59),触发窗口计算,统计当天所有订单的支付金额总和。通过这种方式,无论订单数据何时到达系统,都能按实际支付时间准确统计。但订单数据仍保留在窗口状态中,以便后续可能的重新计算(比如修正了水印等情况)或其他操作。
-
- 基于数据量的触发条件
- Count(计数 )
*- **要点:**窗口内数据元素数量达到设定的阈值即触发。不依赖时间,仅与数据量相关,可灵活根据数据规模控制计算频率。
-
-
- 场景举例:在日志分析系统中,假设要对用户操作日志进行实时分析。由于日志数据量巨大,为了避免数据积压和提高处理效率,设定每收集到 1000 条日志记录,就触发一次窗口计算。例如,统计这 1000 条日志中不同操作类型的出现次数,从而及时了解用户行为分布情况。
-
- 组合及特殊****触发条件
- PurgingTrigger 组合
*- 要点:
- 通常与其他触发器搭配使用。在其他触发器触发窗口计算后,PurgingTrigger 负责清除窗口内的数据,以释放系统资源,防止状态数据过度膨胀。
- 主要用于清除窗口内用于计算聚合结果的原始数据以及相关的中间状态数据。
- 原始输入数据状态:进入窗口的原始数据在 Flink 处理过程中会以某种形式保存在状态里。
- 要点:
例子:以统计每小时活跃用户数为例,每一个用户登录事件作为原始输入数据,会被存储在与该窗口相关的状态中,以便进行活跃用户数的计算。PurgingTrigger 触发时,这些关于用户登录事件的状态数据会被清除。
-
-
-
-
- 中间计算结果状态:在窗口计算过程中产生的中间结果也存储于状态。
-
-
-
例子:计算滑动窗口内的平均温度,随着窗口滑动,每处理一个新的温度数据,会更新窗口内温度总和及数据个数等中间结果,这些中间结果作为状态的一部分保存。当 PurgingTrigger 起作用,这些中间计算结果状态同样会被清除。
-
-
-
- 通常情况下,如果想要在窗口计算后清除窗口内的数据,需要将 PurgingTrigger 与其他触发器(如 Processing TimeTrigger、Event TimeTrigger、Count)组合使用。PurgingTrigger 自身不能单独作为触发条件,它是在其他触发器触发窗口计算后起作用。
- 场景举例:在处理海量物联网设备产生的传感器数据时,数据持续不断地流入。假设使用 Event TimeTrigger 按每小时统计设备数据指标,同时搭配 PurgingTrigger。每小时窗口结束触发计算后,PurgingTrigger 立即清除窗口内已处理的传感器数据,这样在长期运行过程中,能有效控制状态数据量,确保系统稳定运行,避免因数据累积导致内存溢出等问题。
-
-
-
-
- 清除窗口内数据对后续计算影响:
-
**(1)**正常计算逻辑已完成
-
-
-
-
- 解释:当PurgingTrigger触发时,意味着与之关联的触发器(如EventTimeTrigger、ProcessingTimeTrigger或CountTrigger)已经触发了窗口计算,并且窗口内数据的计算任务已经完成。此时,窗口内的数据对于当前窗口的计算目标来说已不再有直接用途。
- 举例:在统计每天的订单总金额场景中,使用EventTimeTrigger按天触发窗口计算。每天结束时,计算出当天订单总金额,此时PurgingTrigger清除窗口内的订单数据,不会影响当天订单总金额这一计算结果,因为计算已经完成。
-
-
-
**(2)**后续计算不依赖已清除数据
-
-
-
-
- 解释:后续的计算任务如果是基于新的数据窗口或者独立的计算逻辑,并不依赖被清除的窗口内数据。例如,计算每天的活跃用户数,每天的数据窗口是独立的,前一天窗口内的用户登录数据被清除后,不会影响第二天活跃用户数的计算,因为第二天会基于新的用户登录数据进行统计。
- 举例:在电商平台实时统计每小时的商品销量场景中,每小时的窗口计算完成后,PurgingTrigger清除该窗口内的销量数据。下一小时的销量统计是基于新一小时内产生的销售数据,与上一小时已清除的数据无关。
-
-
-
**(3)**数据有其他持久化存储
-
-
-
-
- 解释:在一些情况下,虽然PurgingTrigger清除了窗口内的状态数据,但原始数据可能已经被持久化存储到外部系统(如 Kafka、HDFS、数据库等)。如果后续确实需要重新分析历史数据,可以从这些持久化存储中获取。
- 举例:在日志分析场景中,实时窗口计算完成后清除了窗口内的日志数据,但日志数据本身可能已被存储在 HDFS 上。若后续要对历史日志进行深度分析,可从 HDFS 重新读取数据。
-
-
-
**(4)**可能影响的特殊情况及应对
-
-
-
-
- 特殊情况:如果后续计算需要基于历史窗口数据进行对比分析等复杂操作,清除窗口内数据可能会产生影响。例如,要计算本周每天的活跃用户数与上周同一天的活跃用户数对比,若上周数据被PurgingTrigger清除且没有其他存储,就无法完成对比。
- 应对措施:针对这种情况,可以将关键的历史数据存储到外部系统(如 Redis、数据库),以便后续计算使用。或者调整状态管理策略,不使用PurgingTrigger清除特定关键数据,而是采用其他方式控制状态数据量,如设置状态的 TTL(Time - To - Live),仅清除过期较久的数据。
-
-
-
2.5 状态(State**)**
- 状态****类型
- 键控状态(Keyed State)
- 关联方式 :与数据流中的键(Key)紧密关联**,**通过对数据流按特定键进行分区,每个键对应独立的状态。
例如:在按用户统计行为次数时,以用户ID为键,每个用户ID都有其独立的行为次数状态。(相当于存储了键的计算结果)
-
- 作用 范围 :每个键值对都有其独立的状态实例,不同健的状态相互隔离。
例如,在统计每个用户的登录次数场景中,以用户 ID 作为健,每个用户 ID 都有自己独立的登录次数状态。不同用户的登录次数状态互不干扰。
-
- 使用 场景 :常用于需要对不同键值进行独立统计、分析的场景。
比如按用户 ID 统计用户行为次数、按商品 ID 统计商品销量等。
-
- 状态种类 :包含多种类型。以下状态即可用于键控状态也可以用于算子状态。
- ValueState:存储单个值,如用户的最后登录时间
- ListState:存储列表,如用户每次的登录IP地址列表
- ReducingState:通过聚合函数合并值,如计算用户一段时间内的总消费金额
- AggregatingState:更通用的聚合状态,可自定义聚合逻辑
- 状态种类 :包含多种类型。以下状态即可用于键控状态也可以用于算子状态。
- 算子状态(Operator State)
- 关联方式 :与算子实例关联,算子的所有并行实例共享该状态。
例如:Kafka消费者算子中,用于记录每个分区消费偏移量(offset)的状态就是算子状态,它确保在故障恢复时能从正确位置继续消费数据。
-
- 作用 范围 :整个算子实例级别共享,主要用于保存算子在处理数据过程的一些全局信息,这些信息与健无关,但对整个算子的处理逻辑很重要。
- 使用 场景 :常用于需要在算子层面维护一些全局状态的场景,比如数据源的偏移量管理、分布式缓存等场景。这些场景中,状态是整个算子共享的,不依赖于具体的键值。
- 状态****后端
- 作用
- 负责管理状态的存储和访问。不同的状态后端适用于不同的应用场景,影响状态的存储性能、可扩展性以及故障恢复能力。
- 类型
- MemoryStateBackend :将状态存储状态在JVM堆内存中,读写速度快,适用于开发调试阶段或状态数据较少的场景。但由于受JVM堆内存限制,不适合大规模状态存储。
- FsStateBackend :将状态的快照存储在文件系统(如HDFS)中,支持大规模的状态存储,适合生产环境。在发生故障时,可以从文件系统重恢复状态。
- RocksDBStateBackend :基于RocksDB存储状态,RocksDB是一种高性能的嵌入式键值对存储。它能处理超大规模的状态数据,并且在Flink的分布式环境中表现良好,常用于需要处理海量状态数据的场景。
- 状态****一致性
- 重要性
- 在分布式流处理中,确保状态的一致性对于保证计算结果的准确性至关重要。由于可能存在故障、数据乱序等情况,状态一致性尤为突出。
- 实现方式
- Flink通过检查点(Checkpoint)机制来保证状态一致性。检查点定期将作业的状态(包括键控状态和算子状态)持久化到可靠存储(如文件系统)中。当作业发生故障时,可以从最近的检查点恢复状态,重新开始处理数据,从而保证计算结果的一致性。根据应用需求,Flink支持不同级别的一致性语义,如At-Most-One、At-Least-Once、Exactly-Once。
- 使用****方式
- 键控状态 的 使用
- 定义与获取:在实现RichFunction(如RichMapFunction、RichFlatMapFunction等)时,可以定义和获取键控状态。首先,需要定义一个状态描述符(StateDescriptor)来描述状态的名称、类型等信息。
例如:定义一个ValueState来存储用户的登录次数
import org.apache.flink.api.common.state.ValueState;`
`import org.apache.flink.api.common.state.ValueStateDescriptor;`
`import org.apache.flink.configuration.Configuration;`
`import org.apache.flink.streaming.api.functions.KeyedProcessFunction;`
`import org.apache.flink.util.Collector;`
`public class LoginCountFunction extends KeyedProcessFunction<String, LoginEvent, String> {`
`//` `状态定义:transient` `关键字标识该变量不会自动序列号,因为Flink会自行管理状态的持久化。`
` private transient ValueState<Integer> loginCountState;`
`//` `状态的初始化:在算子实例初始化时调用一次,当Flink开始部署作业,创建该` `KeyedProcessFunction` `的实例时,open` `方法会被执行。`
` @Override`
` public void open(Configuration parameters) throws Exception {`
`//` `通过` `ValueStateDescriptor` `定义状态的元数据,包括名称、类型和初始值`
` ValueStateDescriptor<Integer> descriptor = new ValueStateDescriptor<>(`
` "login - count",` `//` `名称`
` Integer.class,` `//` `类型`
` 0` `//` `初始值`
` );`
` loginCountState = getRuntimeContext().getState(descriptor);`
` }`
`//` `登录次数统计:每当有新的数据元素到达算子时,该函数会被触发。`
` @Override`
` public void processElement(LoginEvent event, Context context, Collector<String> collector) throws Exception {`
` Integer count = loginCountState.value();`
` count++;`
` loginCountState.update(count);`
`//` `通过` `collector` `输出包含用户ID和更新后登录次数的信息。`
` collector.collect(event.userId + " has logged in " + count + " times.");`
` }`
`}`
`
-
- 状态更新与使用
在处理数据元素的方法(如 processElement )中,可以根据业务逻辑更新和使用状态。如上述代码中,每次接收到用户登录事件,从状态中获取当前登录次数,加1后更新状态,并输出结果。
-
- 结合 checkpoin 实现故障重启
- 检查点如何保存状态
*- 检查点保存状态 :当检查点触发时,Flink会为作用中的每个算子创建状态快照。对于 LoginCountFunction 中的 loginCountState,Flink 会将其当前值(即每个用户的登录次数)作为状态快照的一部分进行保存。
- 保存到持久化存储 :这些状态快照会被保存到配置的持久化存储中,如文件系统(如HDFS)。状态后端(FsStateBackend或RocksDBStateBackend)负责管理状态的存储和恢复。
- 故障发生时的恢复过程
*- 检测故障 :当 Flink 作业发生故障时,Flink 的容错机制会检测到故障。
- 从检查点恢复 :Flink 会从最近一次成功的检查点中恢复作业的状态。对于 LoginCountFunction,它会从检查点中读取 loginCountState 的状态值,恢复到故障发生前的状态。
例如,如果在用户 A 登录次数统计到 10 次时发生故障,而最近一次检查点记录的用户 A 登录次数为 8 次,那么作业恢复后,loginCountState会恢复为 8 次,然后继续处理后续的登录事件,从第 9 次登录开始统计。
由于检查点中的偏移量是 8 次时对应的偏移量,有 2 次登录事件会重新处理。
- 算子状态的使用
- 定义与获取:实现 CheckpointedFunction 接口来使用算子状态。例如,自定义一个算子状态来记录处理数据的总数。
import` `org.apache.flink.api.common.state.ListState;`
`import` `org.apache.flink.api.common.state.ListStateDescriptor;`
`import` `org.apache.flink.runtime.state.CheckpointedFunction;`
`import` `org.apache.flink.streaming.api.functions.source.RichParallelSourceFunction;`
`import` `java.util.ArrayList;`
`import` `java.util.List;`
`// 作为一个自定义数据源,生成递增的长整型数据,并记录处理的数据总数,同时具备通过检查点机制进行状态持久化和恢复的能力,以确保在作业发生故障时能够从故障点继续恢复处理。`
`public` `class` `CustomSourceFunction` `extends` `RichParallelSourceFunction<Long>` `implements` `CheckpointedFunction` `{`
`private` `transient` `ListState<Long> totalCountState;`
`private` `long totalCount =` `0;`
`// 作用:在算子实例初始化时被调用,用于初始化相关资源。`
`// 触发时机:当 Flink 作业启动并创建该数据源算子实例时触发,每个算子实例只会触发一次。例如,在一个分布式 Flink 作业中,如果该数据源算子有 5 个并行实例,那么每个实例都会执行一次 open 方法来初始化各自的状态。`
`@Override`
`public` `void` `open(Configuration parameters)` `throws` `Exception` `{`
`ListStateDescriptor<Long> descriptor =` `new` `ListStateDescriptor<>(`
`"total - count",`
`Long.class`
`);`
`// 获取 totalCountState 实例,从已有的状态中恢复之前记录的总数 totalCount,以便在作业恢复时能从上次中断的地方继续统计。`
` totalCountState =` `getRuntimeContext().getListState(descriptor);`
`for` `(Long count : totalCountState.get())` `{`
` totalCount += count;`
`}`
`}`
`// 作用:这是数据源生成数据的核心方法。在一个无限循环中,totalCount 变量不断递增,代表处理的数据总数,每次递增后通过 ctx.collect(totalCount) 将数据发送出去,并同时更新 totalCountState 状态。`
`// 触发时机:在 open 方法执行完毕后,该方法开始持续执行,不断生成数据。只要作业处于运行状态且未被取消,它就会一直循环执行,每秒生成并发送一个递增的数据,同时更新状态。run 函数只会在作业启动时被调用一次,然后会一直在 run 函数内循环中等待数据到来,而不是每次外部数据源(如kafka等)有新的行为日志输入时就会触发一次运行。`
`@Override`
`public` `void` `run(SourceContext<Long> ctx)` `throws` `Exception` `{`
`while` `(true)` `{`
` totalCount++;`
` ctx.collect(totalCount);`
` totalCountState.update(new` `ArrayList<>(List.of(totalCount)));`
`Thread.sleep(1000);`
`}`
`// 其他例子:如果每次有新数据到达时执行特定逻辑,可以在 run 函数的循环体内部,处理从 Kafka 获取到的数据的部分编写相应代码`
`/*`
` KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);`
` consumer.subscribe(Collections.singletonList("user - behavior - topic"));`
` while (true) {`
` ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));`
` for (ConsumerRecord<String, String> record : records) {`
` // 解析Kafka消息为UserBehavior对象`
` UserBehavior behavior = parseKafkaRecord(record);`
` totalCount++;`
` ctx.collect(behavior);`
` totalCountState.update(new ArrayList<>(List.of(totalCount)));`
` }`
` } `
` */`
`}`
`// 作用:当作业被取消时,Flink 会调用此方法。目前该方法为空,开发者可在此方法中添加清理资源等逻辑,例如关闭文件句柄、释放网络连接等,以确保作业取消时资源被正确释放。`
`// 触发时机:当用户手动取消作业,或者由于某些错误导致作业被终止时触发。`
`@Override`
`public` `void` `cancel()` `{}`
`// 作用:在检查点触发时被调用,负责将当前的算子状态保存到检查点。这里先清空 totalCountState,然后将当前的 totalCount 值添加进去,这样检查点就记录了当前处理的数据总数。`
`// 触发时机:根据 Flink 作业配置的检查点间隔时间,每当检查点触发时执行。例如,如果配置了每 5 秒进行一次检查点,那么每 5 秒就会调用一次 snapshotState 方法来保存状态。`
`@Override`
`public` `void` `snapshotState(FunctionSnapshotContext context)` `throws` `Exception` `{`
` totalCountState.clear();`
` totalCountState.add(totalCount);`
`}`
`// 作用:当作业从故障中恢复时,Flink 会调用此方法,从检查点中恢复算子状态。这里从 totalCountState 中获取之前保存的总数 totalCount,使作业能够从上次中断的地方继续处理数据。`
`// 触发时机:当作业发生故障并开始从最近的检查点恢复时触发。例如,作业在运行过程中某个节点崩溃,Flink 会重启作业并调用 restoreState 方法来恢复状态,确保作业能够继续准确地生成和统计数据。`
`@Override`
`public` `void` `restoreState(FunctionRestoreContext context)` `throws` `Exception` `{`
`for` `(Long count : totalCountState.get())` `{`
` totalCount = count;`
`}`
`}`
`}`
`
各个函数执行顺序
- 故障发生前 :open 函数在作业启动时已执行,用于初始化算子资源;之后 run 函数持续运行以处理数据;同时在故障发生前最近一次检查点出发时,snapshotState 函数执行,将当前状态保存到检查点。
- 执行顺序:open -> run -> snapshotState
- 故障及恢复过程 :
- 恢复阶段 :
- restoreState:Flink 检查到节点崩溃后,开始从最近一次成功的检查点恢复作业。此时,restoreState 函数首先被调用,用于从检查点加载之前保存的状态数据,恢复算子到故障前的状态。
- 恢复阶段 :
例如:它会从 totalCountState 中获取之前保存的处理数据总数 totalCount。
-
-
- open:在状态恢复后,open 函数再次被调用。这一步是为了重新初始化一些在故障恢复过程中可能需要重新设置的资源或进行额外的初始化操作。尽管状态已通过 restoreState 恢复,但 open 函数提供了一个通用的初始化入口,确保算子在新的运行环境中能正常运行。
- 重启后正常运行阶段 :
- run:open 函数执行后,run 函数开始执行。作业从故障点恢复后,run 函数继续处理数据,就像故障没有发生一样。
- 执行顺序:restoreState -> open -> run -> snapshotState
-
- 若作业取消 :
- 在作业恢复运行后,用户手动取消作业(或由于某些错误导致作业被取消),会调用 cancel 函数。cancel 函数用于执行清理资源等操作,例如关闭文件句柄、释放网络连接等,确保作业取消时资源被正确释放。
2.6 **检查点(Checkpoint****)**
检查点(Checkpoin)和保存点(Savepoint)都是用于状态管理和故障恢复的重要机制,但它们在功能、使用场景和实现方式上存在一些区别。
- 定义与作用
- 定义 :检查点是 Flink 自动定期执行的操作,它会对作业在某一时刻的状态进行快照,并将这些状态快照持久化到可靠存储中,如文件系统(如HDFS)。
- 作用 :用于作业故障恢复。当作业发生故障(如节点崩溃、网络故障等)时,Flink可以从最近一次成功的检查点恢复作业的状态,包括数据源的偏移量、算子中间状态等,使得作业能够从故障点继续处理数据,保证计算的一致性和连续性。
- 触发****机制
- 定时触发 :检查点安装用户在作业配置中设定的固定时间间隔触发。例如,通过 env.enableCheckpointing(5000) 设置每 5000 毫秒触发一次检查点。
- 状态 一致性 保证 :Flink 支持不同级别的一致性语义,如 At-Most-Once、At-Least-Once、Exactly-Once。通过配置检查点模式确保所需的一致性级别。如:env.getCheckpointConfig().setCheckpointMode(CheckpointingMode.EXACTLY_ONCE)
- 使用****场景
- 实时流 处理 :在实时处理不间断数据流的场景中,如实时监控用户行为,金融交易流水处理等,检查点能够确保在出现故障时,作业可以快速恢复并继续准确处理数据,避免数据丢失或重复处理对业务造成影响。
2.7 **保存点(Savepoint)**
- 定义与作用
- 定义 :保存点是用户手动触发的操作(也可以通过自动化脚本触发),它也是对作业状态的一种快照,通用会将作业的状态持久化到外部存储。
- 作用 :主要用于作业的升级、迁移或暂停 / 重启。保存点记录了作业的完整状态,包括所有算子的状态和数据流的位置等信息。通过保存点,可以将作业从一个集群迁移到另外一个集群,或者在不丢失状态的情况下对作业进行升级,如更改算子逻辑、调整并行度等。
- 触发****机制
- 手动 触发 :用户通过 Flink 命名行工具(如 flink savepoint : jobId [:targetDirectory])手动触发保存点操作。其中,:jobId 是要创建保存点的作业ID,:targetDirectory 是可选的保存点存储目录。
- 使用****场景
- 作业 迁移 :当需要对正在运行的作业进行代码升级,例如添加新的功能、优化算子逻辑时,可以创建保存点,然后停止作业,更新代码,再从保存点启动作业,确保作业在新的代码逻辑下能够从之前的状态继续运行。
- 集群 迁移 :当需要将作业从一个 Flink 集群迁移到另一个集群时,保存点可以记录作业的所有状态信息,使得作业在新集群上从保存点恢复后能够无缝继续运行,就像在原集群上从未停止过一样。
2.8 **Checkpoint与Savepoint**异同
- 触发****方式
- 检查 点 :自动按照设定的时间间隔触发。
- 保存 点 :手动触发,由用户或自动化脚本控制。
- 用途
- 检查 点 :侧重于故障恢复,确保作业在故障后能快速恢复并继续处理数据,保证数据一致性。
- 保存 点 :主要用于作业的生命周期管理,如升级、迁移或暂停 / 重启,更关注作业在不同环境或配置下状态的保留和恢复。
- 状态****清理
- 检查 点 :Flink 会自动管理检查点的清理,根据配置的策略(如保留最近的几个检查点)删除旧的检查点。
- 保存 点 :保存点不会被 Flink 自动清理,需要用户手动删除,这使得保存点可以长期保留,方便后续多次使用。
- 数据读取位置
- 当作业因故障崩溃并从检查点恢复时,作业会从检查点 / 保存点记录的数据源偏移量位置继续读取数据,Flink 的状态管理机制会确保算子状态(窗口计算中间状态、聚合状态等)也恢复到检查点的状态,窗口内已处理的数据以及计算的中间结果都会恢复到检查点的状态,然后继续处理未处理完的数据,重新计算窗口的最终结果。
- Kafka数据保存策略
- 时间维度 :Kafka 默认会根据时间来保留数据,通过 log.retention.hours 配置参数来配置数据保留时长,超过这个时间的数据会被自动删除。
- 空间维度 :Kafka 也支持基于空间的保留策略,通过 log.retention.bytes 配置分区日志大小,分区日志超过这个字节数时,旧的数据会被删除。
- 默认策略 :如果没有显式设置空间相关的保留策略,Kafka 会使用基于时间的保留策略作为默认设置,默认保留 7 天(灯塔 cdmq 设置默认保留 3 小时)。