Flink 实时开发:关键知识点

2.1“语义保证”(Delivery Guarantees)

语义保证是衡量系统可靠性的核心指标。在数据恢复中起着关键作用,但并不是只影响故障恢复。

  • At-Most-Once语义:任务故障运行可能出现数据丢失在数据处理过程中,由于网络抖动等原因,部分数据在算子传递到输入日志文件过程中丢失,但系统不会进行重处理,这就导致数据丢失。
  • At-Least-Once语义:即使任务故障运行可能出现数据重复处理情况数据网络传输算子处理过程可能因为一些异步操作缓冲区问题导致部分数据多次处理
  1. At-Most-Once(最多一次)
  • 定义:数据可能被处理 0 次或 1 次,但绝不会被重复处理

即:如果数据在传输或处理过程中丢失,系统不会重试,直接跳过。

  • 实现原理

不做任何容错机制,数据从源端发出后,不记录偏移量(Offset)、不保存状态快照(Checkpoint),处理完成后也不确认 “已处理”。一旦发生故障,未处理的数据会丢失,且不会重新处理

  • 适用场景:

对数据准确性要求极低,更看重处理速度和资源开销的场景,例如:

    • 实时监控非关键指标(如网站瞬时访问量,丢失少量数据不影响整体趋势);
    • 日志采集的 “尽力而为” 模式(如非核心业务的调试日志)。
  • 优缺点
    • 优点:性能最好(无容错开销)、延迟最低。
    • 缺点:数据可能丢失,准确性无法保证。
  1. At-Least-Once(至少一次)
  • 定义:数据至少被处理 1 次,但可能被重复处理

即:确保数据不会丢失,但可能因重试导致重复。

  • 实现原理
    • 记录数据处理的进度(如 Kafka 的 Offset),但 “标记已处理” 的操作可能滞后于实际处理
    • 发生故障时,从最近的进度记录(如未更新的 Offset)重新处理数据,导致部分数据被重复处理。

例如:处理完数据后,先输出结果,再更新 Offset。若更新 Offset 前发生故障,重启后会从旧 Offset 重新处理,导致结果重复。

  • 适用场景

允许数据重复,但绝对不能丢失的场景,例如:

    • 计费相关的初步数据采集(重复可后续通过幂等性处理修正);
    • 实时报警(重复报警总比漏报好)。

实时ETL保证不丢失允许重复可控

  • 优缺点
    • 优点:保证数据不丢失,容错机制简单(仅需记录进度)。
    • 缺点:可能产生重复数据,需下游系统做去重处理(如依赖业务主键去重)
  1. Exactly-Once(精确一次)
  • 定义:数据恰好被处理 1 次,既不丢失也不重复,是最高级别的语义保证。
  • 实现原理

结合了 “状态快照” 和 “分布式事务”:

    • Checkpoint 机制:定期将处理状态(如计算中间结果、Offset)保存为快照
    • 两阶段提交(2PC)确保 “处理数据” 和 “更新进度 / 输出结果” 的原子性(要么都成功,要么都失败)。

例如:Flink 的 Exactly-Once 通过 Checkpoint + 与外部系统(如 Kafka、MySQL)的事务集成实现,故障恢复时从 Checkpoint 恢复状态,确保数据不会重复处理。

  • 适用场景
    • 数据准确性要求极高的核心业务,例如:金融交易(如实时对账、转账金额统计);
    • 关键指标计量(如用户付费转化、订单量统计
  • 优缺点
    • 优点:数据零丢失、零重复,准确性最高。
    • 缺点:实现复杂,会引入一定的性能开销(Checkpoint、事务处理),延迟略高
  1. 三者对比与选择建议

语义

数据丢失

数据重复

性能开销

适用场景

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。

  1. 性能开销差异:核心源于容错机制的复杂度

性能开销主要体现在 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

关键影响因素(为什么差异不固定?)

  1. 状态大小:Exactly-Once 的 Checkpoint 开销与状态大小强相关。若业务状态很小(如仅统计计数),Checkpoint 耗时短,开销接近 At-Least-Once;若状态极大(如保存百万用户的会话数据),Checkpoint 序列化 / 传输耗时剧增,开销可能达到 At-Most-Once 的 3 倍以上。
  2. Checkpoint 频率:Flink 默认 Checkpoint 间隔为 100ms-1s,间隔越短,Exactly-Once 的 I/O 开销越大(频繁写快照),但故障恢复数据丢失越少;间隔越长,开销降低,但恢复时重复处理的数据越多。
  3. 外部系统兼容性:若 Sink(如 MySQL)支持事务(如 Flink 的 JDBC Sink 用事务写入),Exactly-Once 无需额外开销;若不支持,需通过 “幂等写入 + 状态记录” 间接实现,CPU / 内存开销会进一步增加。
  1. 数据延迟差异:核心源于 “同步 / 异步容错操作”

数据延迟指 “数据从 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 以上。
  1. 总结:如何根据业务选择?
  1. 优先选 At-Most-Once:若业务对 “数据丢失” 不敏感,只追求极致性能和低延迟(如非核心业务的实时监控、日志预览)。
  2. 优先选 At-Least-Once:若业务 “绝对不能丢数据”,但能接受重复(且下游可通过主键去重,如用户行为埋点数据),同时希望平衡性能和可靠性(如电商实时销量统计,重复数据可通过订单 ID 去重)。
  3. 必须选 Exactly-Once:若业务 “既不能丢也不能重”,且下游无法去重(如金融交易、实时计费),即使付出性能和延迟代价也必须保证准确性。
  1. 补充:Flink 中的优化技巧(降低高语义的开销)
  • 对 Exactly-Once:调大 Checkpoint 间隔(如从 100ms 改为 500ms),减少快照频率(代价是故障恢复时重复数据增多);
    • 使用增量 Checkpoint(仅保存与上一次快照的差异数据),降低 I/O 和内存开销;
    • 选择高性能状态后端(如 RocksDB,支持状态磁盘溢出,减少内存压力)。
  • 对 At-Least-Once:异步更新 Offset(Flink 默认配置),避免同步写入阻塞;
    • 若下游支持幂等,可直接用 At-Least-Once + 幂等写入,替代 Exactly-Once(性能提升 30%+)。

通过以上优化,可在保证业务语义的前提下,大幅缩小不同语义间的性能和延迟差距。

2.2 时间语义:解决数据乱序

  1. 事件时间(Event Time)

数据产生的实际时间(如日志中的create_time),最符合业务逻辑。几乎都用事件时间,需指定 “时间戳提取器” 和 “水印(Watermark)” 来处理乱序数据。

  1. Processing Time(处理时间)
  • 定义:指 Flink 作业算子处理数据元素时机器的系统时间。简单来说,就是数据进入 Flink 算子那一瞬间,机器记录的时间戳
  • 区别于 Ingestion Time:它是基于每个算子本地系统时钟的时间,不同算子处理数据时间可能不同。如果数据在算子间传输有延迟,那每个算子记录的 Processing Time 会有差异
  • 适用场景:当你不太关心数据真实产生时间,只关注系统实时处理速度,对数据准确性要求不是极高时使用。比如监控系统实时流量,只要大概知道当前系统每秒处理了多少数据,不要求精确到数据产生的那一刻
  1. Ingestion Time(摄入时间)
  • 定义:是数据进入 Flink 作业的第一个算子(比如 Kafka Source)时,Flink 给数据记录的时间戳。这个时间一旦确定,在后续算子处理过程中基本保持不变
  • 区别于 Processing TimeIngestion Time 在数据进入作业源头就确定了,之后不管数据在作业内如何流转,这个时间不变;而 Processing Time 在每个算子处理时重新记录所以 Ingestion Time 更能反映数据进入作业的整体时间顺序,而 Processing Time 受算子处理速度影响更大。
  • 适用场景:适用于对数据进入系统的时间顺序比较敏感,且能接受一定程度数据乱序情况。例如记录用户行为数据进入系统的时间,即使数据在后续处理中有些许乱序,依然可以根据摄入时间进行分析

2.3 窗口Window

  1. 窗口Tumbling Window
  • 描述窗口大小固定,且相互之间不重叠,像切香肠一样均匀分段。如设置滚动窗口大小为 5 分钟,那么数据就会被按照每 5 分钟进行划分,9:00-9:05 的订单为一批,9:05-9:10 的订单为下一批。
  • 图表示意:假设时间轴为水平方向,从左向右推进,每一个滚动窗口就是一个固定长度的矩形块,依次排列,块与块之间没有间隙和重叠。

  • 适用场景:适用于统计每小时销售额、每天用户活跃数等,对固定时间间隔内的数据进行独立的聚合计算。
  1. 滑动窗口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 使用率,能避免某段时间数据量突然增大导致结果波动,适用于需要更频繁计算和细粒度分析的场景。
  1. 会话窗口Session Window
  • 描述:根据数据之间的间隔动态划分窗口,没有固定的开始或结束时间。如果顾客点单间隔超过 15 分钟,就视为 “新会话”,如顾客 9:00 下单,9:05 加菜,9:20 再次下单,则分为两个窗口(9:00-9:05 和 9:20 - 未定)。
  • 图表示意:在时间轴上,窗口的长度和位置不固定,取决于数据事件的间隔,表现为不连续、长度不一的区间

  • 适用场景分析用户行为会话,如网页浏览、游戏在线时长等。
  1. 全局窗口(Global Window)
  • 描述:全局窗口会将拥有相同 key 的所有数据都分发到一个窗口中,包含流中的所有数据
  • 图表示意:在时间轴上可以看作是一个无限长的窗口,涵盖了所有数据
  • 适用场景适用于需要对整个流进行一次性计算的情况

2.4 触发条件Trigger

  1. 基于时间触发条件
  • Processing Time(处理时间)
      • 要点:依据算子处理数据时机器的系统时间触发。简单直接,反映数据在系统内的即时处理情况,但受系统负载影响,不同算子处理时间可能不同。仅负责根据事件时间触发窗口计算。
      • 场景举例:在实时监控系统的网络流量场景中,假设要每 5 秒统计一次当前 5 秒内的网络流量数据。由于我们更关注当前系统实时处理的数据量,对流量数据的精确时间戳要求不高,此时可使用 Processing TimeTrigger。系统时钟每到 5 秒的时间间隔,就触发窗口计算,统计这 5 秒内流经系统的网络流量总和。
  • Event Time(事件时间)
      • 要点:根据数据自身携带的时间戳触发。能精准体现数据实际发生的先后顺序,不受数据传输和处理延迟干扰,但需合理设置水印应对乱序数据。当事件时间达到窗口结束时间(结合水印机制处理乱序数据后),它会触发窗口内数据的计算操作,但默认情况下,并不会自动清除窗口内的数据
      • 场景举例:以电商平台的订单支付场景为例,要统计每天的订单支付金额。订单数据带有支付时间戳,这就是事件时间。我们以一天为窗口,当事件时间到达每天的结束时刻(如 23:59:59),触发窗口计算,统计当天所有订单的支付金额总和。通过这种方式,无论订单数据何时到达系统,都能按实际支付时间准确统计。订单数据仍保留在窗口状态中,以便后续可能的重新计算(比如修正了水印等情况)或其他操作。
  1. 基于数据量的触发条件
  • Count(计数
      • 要点:窗口内数据元素数量达到设定的阈值即触发。不依赖时间,仅与数据量相关,可灵活根据数据规模控制计算频率。
      • 场景举例:在日志分析系统中,假设要对用户操作日志进行实时分析。由于日志数据量巨大,为了避免数据积压和提高处理效率,设定每收集到 1000 条日志记录,就触发一次窗口计算。例如,统计这 1000 条日志中不同操作类型的出现次数,从而及时了解用户行为分布情况。
  1. 组合特殊触发条件
  • PurgingTrigger 组合
      • 要点
        • 通常与其他触发器搭配使用。在其他触发器触发窗口计算后,PurgingTrigger 负责清除窗口内的数据,以释放系统资源,防止状态数据过度膨胀。
        • 主要用于清除窗口内用于计算聚合结果的原始数据以及相关的中间状态数据
          • 原始输入数据状态进入窗口的原始数据在 Flink 处理过程中会以某种形式保存在状态里。

例子以统计每小时活跃用户数为例,每一个用户登录事件作为原始输入数据,会被存储在与该窗口相关的状态中,以便进行活跃用户数的计算。PurgingTrigger 触发时,这些关于用户登录事件的状态数据会被清除。

          • 中间计算结果状态在窗口计算过程中产生的中间结果也存储于状态。

例子计算滑动窗口内的平均温度,随着窗口滑动,每处理一个新的温度数据,会更新窗口内温度总和及数据个数等中间结果,这些中间结果作为状态的一部分保存。当 PurgingTrigger 起作用,这些中间计算结果状态同样会被清除。

        • 通常情况下,如果想要在窗口计算后清除窗口内的数据,需要将 PurgingTrigger 与其他触发器(如 Processing TimeTriggerEvent TimeTriggerCount)组合使用。PurgingTrigger 自身不能单独作为触发条件,它是在其他触发器触发窗口计算后起作用。
      • 场景举例:在处理海量物联网设备产生的传感器数据时,数据持续不断地流入。假设使用 Event TimeTrigger 按每小时统计设备数据指标,同时搭配 PurgingTrigger。每小时窗口结束触发计算后,PurgingTrigger 立即清除窗口内已处理的传感器数据,这样在长期运行过程中,能有效控制状态数据量,确保系统稳定运行,避免因数据累积导致内存溢出等问题。
      • 清除窗口数据后续计算影响

1正常计算逻辑已完成

          • 解释:当PurgingTrigger触发时,意味着与之关联的触发器(如EventTimeTriggerProcessingTimeTriggerCountTrigger)已经触发了窗口计算,并且窗口内数据的计算任务已经完成。此时,窗口内的数据对于当前窗口的计算目标来说已不再有直接用途
          • 举例:在统计每天的订单总金额场景中,使用EventTimeTrigger按天触发窗口计算。每天结束时,计算出当天订单总金额,此时PurgingTrigger清除窗口内的订单数据,不会影响当天订单总金额这一计算结果,因为计算已经完成。

2后续计算不依赖已清除数据

          • 解释:后续的计算任务如果是基于新的数据窗口或者独立的计算逻辑,并不依赖被清除的窗口内数据。例如,计算每天的活跃用户数,每天的数据窗口是独立的,前一天窗口内的用户登录数据被清除后,不会影响第二天活跃用户数的计算,因为第二天会基于新的用户登录数据进行统计。
          • 举例:在电商平台实时统计每小时的商品销量场景中,每小时的窗口计算完成后,PurgingTrigger清除该窗口内的销量数据。下一小时的销量统计是基于新一小时内产生的销售数据,与上一小时已清除的数据无关

3数据有其他持久化存储

          • 解释:在一些情况下,虽然PurgingTrigger清除了窗口内的状态数据,但原始数据可能已经被持久化存储到外部系统(如 Kafka、HDFS、数据库等)。如果后续确实需要重新分析历史数据,可以从这些持久化存储中获取。
          • 举例:在日志分析场景中,实时窗口计算完成后清除了窗口内的日志数据,但日志数据本身可能已被存储在 HDFS 上。若后续要对历史日志进行深度分析,可从 HDFS 重新读取数据。

4可能影响的特殊情况及应对

          • 特殊情况:如果后续计算需要基于历史窗口数据进行对比分析等复杂操作,清除窗口内数据可能会产生影响。例如,要计算本周每天的活跃用户数与上周同一天的活跃用户数对比,若上周数据被PurgingTrigger清除且没有其他存储,就无法完成对比。
          • 应对措施:针对这种情况,可以将关键的历史数据存储到外部系统(如 Redis、数据库),以便后续计算使。或者调整状态管理策略,不使用PurgingTrigger清除特定关键数据,而是采用其他方式控制状态数据量,如设置状态的 TTL(Time - To - Live),仅清除过期较久的数据

2.5 状态State

  1. 状态类型
  • 键控状态(Keyed State)
    • 关联方式与数据流中的键(Key)紧密关联通过对数据流按特定键进行分区,每个键对应独立的状态

例如用户统计行为次数用户ID每个用户ID都有其独立的行为次数状态相当于存储了键的计算结果

    • 作用范围每个键值对都有其独立的状态实例,不同健的状态相互隔离

例如统计每个用户登录次数场景用户 ID 作为每个用户 ID 都有自己独立登录次数状态不同用户登录次数状态互不干扰

    • 使用场景常用于需要不同键值进行独立统计分析场景

比如用户 ID 统计用户行为次数商品 ID 统计商品销量

    • 状态种类包含多种类型以下状态即可用于键控状态也可以用于算子状态
      • ValueState:存储单个值用户的最后登录时间
      • ListState:存储列表用户每次的登录IP地址列表
      • ReducingState:通过聚合函数合并值计算用户一段时间内的总消费金额
      • AggregatingState:更通用的聚合状态,可自定义聚合逻辑
  • 算子状态(Operator State)
    • 关联方式算子实例关联算子所有并行实例共享状态

例如:Kafka消费算子用于记录每个分区消费偏移量(offset)的状态就是算子状态,它确保在故障恢复时能从正确位置继续消费数据

    • 作用范围整个算子实例级别共享主要用于保存算子在处理数据过程的一些全局信息这些信息无关整个算子处理逻辑重要
    • 使用场景常用于需要在算子层面维护一些全局状态场景,比如数据源的偏移量管理、分布式缓存等场景这些场景状态整个算子共享依赖具体键值
  1. 状态后端
  • 作用
    • 负责管理状态的存储和访问不同状态后端适用于不同应用场景影响状态存储性能扩展以及故障恢复能力
  • 类型
    • MemoryStateBackend将状态存储状态在JVM堆内存中,读写速度快,适用于开发调试阶段或状态数据较少的场景由于JVM堆内存限制适合规模状态存储
    • FsStateBackend将状态的快照存储在文件系统(如HDFS)中,支持大规模的状态存储,适合生产环境发生故障可以文件系统恢复状态
    • RocksDBStateBackend基于RocksDB存储状态RocksDB一种高性能嵌入式键值对存储处理超大规模状态数据并且Flink分布式环境表现良好常用于需要处理海量状态数据的场景
  1. 状态一致性
  • 重要性
    • 分布式流处理确保状态一致性对于保证计算结果准确性至关重要由于可能存在故障数据乱序情况状态一致性尤为突出
  • 实现方式
    • Flink通过检查点Checkpoint机制保证状态一致性检查点定期将作业的状态(包括键控状态和算子状态)持久化到可靠存储(如文件系统)中。当作业发生故障时,可以从最近的检查点恢复状态,重新开始处理数据,从而保证计算结果的一致性。根据应用需求Flink支持不同级别一致性语义At-Most-OneAt-Least-OnceExactly-Once
  1. 使用方式
  • 键控状态使用
    • 定义获取实现RichFunctionRichMapFunctionRichFlatMapFunction可以定义获取键控状态首先需要定义一个状态描述符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 loginCountStateFlink 当前每个用户登录次数作为状态快照一部分进行保存
        • 保存到持久化存储这些状态快照保存配置持久化存储文件系统HDFS状态后端FsStateBackendRocksDBStateBackend负责管理状态存储恢复
    • 故障发生时的恢复过程
        • 检测故障 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
  • 故障及恢复过程
    • 恢复阶段
      • restoreStateFlink 检查节点崩溃开始最近一次成功检查点恢复作业此时restoreState 函数首先调用用于检查点加载之前保存状态数据恢复算子故障状态

例如 totalCountState 获取之前保存处理数据总数 totalCount

      • open状态恢复open 函数再次调用这一步重新初始化一些在故障恢复过程可能需要重新设置资源进行额外初始化操作尽管状态通过 restoreState 恢复 open 函数提供一个通用初始化入口确保算子运行环境正常运行
    • 重启后正常运行阶段
      • runopen 函数执行run 函数开始执行作业故障点恢复run 函数继续处理数据就像故障没有发生一样
    • 执行顺序restoreState -> open -> run -> snapshotState
  • 若作业取消
    • 作业恢复运行用户手动取消作业由于某些错误导致作业取消调用 cancel 函数cancel 函数用于执行清理资源操作例如关闭文件句柄释放网络连接确保作业取消资源正确释放

2.6 检查Checkpoint

检查点Checkpoin)保存Savepoint)都是用于状态管理故障恢复重要机制它们功能使用场景实现方式存在一些区别

  1. 定义作用
  • 定义:检查点是 Flink 自动定期执行的操作,它会对作业在某一时刻的状态进行快照,并将这些状态快照持久化到可靠存储中,如文件系统(如HDFS)。
  • 作用用于作业故障恢复作业发生故障节点崩溃网络故障Flink可以最近一次成功检查点恢复作业状态包括数据源偏移量算子中间状态使得作业能够故障继续处理数据保证计算一致性连续性
  1. 触发机制
  • 定时触发检查安装用户作业配置设定固定时间间隔触发例如通过 env.enableCheckpointing(5000) 设置 5000 毫秒触发一次检查
  • 状态一致性保证Flink 支持不同级别一致性语义 At-Most-OnceAt-Least-OnceExactly-Once通过配置检查点模式确保所需一致性级别env.getCheckpointConfig().setCheckpointMode(CheckpointingMode.EXACTLY_ONCE)
  1. 使用场景
  • 实时流处理在实时处理不间断数据流场景实时监控用户行为金融交易流水处理检查能够确保出现故障作业可以快速恢复继续准确处理数据避免数据丢失或重复处理业务造成影响

2.7 保存Savepoint

  1. 定义作用
  • 定义保存用户手动触发操作可以通过自动化脚本触发也是作业状态一种快照通用作业状态持久化外部存储
  • 作用主要用于作业升级迁移暂停 / 重启保存记录作业完整状态包括所有算子状态数据流位置等信息通过保存可以作业一个集群迁移另外一个集群或者丢失状态情况作业进行升级更改算子逻辑调整并行度
  1. 触发机制
  • 手动触发用户通过 Flink 命名工具 flink savepoint : jobId [:targetDirectory]手动触发保存操作其中:jobId 创建保存作业ID:targetDirectory 保存存储目录
  1. 使用场景
  • 作业迁移需要正在运行作业进行代码升级例如添加功能优化算子逻辑可以创建保存然后停止作业更新代码保存启动作业确保作业代码逻辑能够之前状态继续运行
  • 集群迁移需要作业一个 Flink 集群迁移另一个集群保存可以记录作业所有状态信息使得作业集群保存恢复能够无缝继续运行就像集群停止一样

2.8 CheckpointSavepoint异同

  1. 触发方式
  • 检查自动按照设定时间间隔触发
  • 保存手动触发用户自动化脚本控制
  1. 用途
  • 检查侧重于故障恢复确保作业故障快速恢复继续处理数据保证数据一致性
  • 保存用于作业生命周期管理升级迁移暂停 / 重启关注作业不同环境配置状态保留恢复
  1. 状态清理
  • 检查Flink 自动管理检查清理根据配置策略保留最近几个检查删除检查
  • 保存保存不会 Flink 自动清理需要用户手动删除使得保存可以长期保留方便后续多次使用
  1. 数据读取位置
  • 作业故障崩溃检查恢复作业检查 / 保存记录数据源偏移量位置继续读取数据Flink 状态管理机制确保算子状态窗口计算中间状态聚合状态恢复检查状态窗口处理数据以及计算中间结果都会恢复检查状态然后继续处理处理数据重新计算窗口最终结果
  • Kafka数据保存策略
    • 时间维度Kafka 默认根据时间保留数据通过 log.retention.hours 配置参数配置数据保留时长超过时间数据自动删除
    • 空间维度Kafka 支持基于空间保留策略通过 log.retention.bytes 配置分区日志大小分区日志超过这个字节数数据删除
    • 默认策略如果没有显式设置空间相关保留策略Kafka 使用基于时间保留策略作为默认设置默认保留 7 灯塔 cdmq 设置默认保留 3 小时

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

J-JunLiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值