Flink面试题002-状态 + Checkpoint + 窗口

0 阅读9分钟

状态 + Checkpoint = 完整的流式计算“记忆”
类比:状态是大脑的短期记忆,Checkpoint是写日记存档,Flink是有健忘症的天才

组件主要功能局限性解决方案
状态存储计算中间结果(如UV统计的用户ID集合)内存存储,存在丢失风险(任务失败/重启)配合Checkpoint机制定期持久化
Checkpoint将状态快照定期保存至持久化存储无法识别已保存状态及最新状态值从状态读取并进行序列化处理

一、两者如何配合工作?

  1. 场景设定
    统计商品页面(page_001)最近5分钟的独立访客数(UV),滑动步长1分钟。

    • 数据流(时间顺序):
Time 10:00:00 - user_001 访问 page_001
Time 10:00:10 - user_002 访问 page_001  
Time 10:00:20 - user_001 再次访问 page_001
Time 10:00:30 - user_003 访问 page_001
Time 10:00:40 - user_004 访问 page_001
Time 10:01:00 - user_005 访问 page_001
...
  • 窗口定义:

滑动窗口 :5分钟长度,1分钟滑动步长
当前窗口:[10:00:00, 10:05:00)
数据到达时,Flink会自动判断属于哪个窗口

  1. 完整处理流程(含状态变化)
  • 初始状态(10:00:00)
状态:page_001 → HashSet{} (空)
Checkpoint:无
  • 处理第一条数据(10:00:00)
输入:user_001 访问 page_001

操作:
1. 读取状态:HashSet{}
2. 添加user_001 → HashSet{user_001}
3. 更新状态:page_001 → HashSet{user_001}
4. 窗口[10:00:00, 10:05:00)的UV=1

当前状态:page_001 → HashSet{user_001}
  • 处理第二条数据(10:00:10)
    输入:user_002 访问 page_001
操作:
1. 读取状态:HashSet{user_001}
2. 添加user_002 → HashSet{user_001, user_002}
3. 更新状态:page_001 → HashSet{user_001, user_002}
4. 窗口[10:00:00, 10:05:00)的UV=2

当前状态:page_001 → HashSet{user_001, user_002}
  • 处理第三条数据(10:00:20)
输入:user_001 再次访问 page_001

操作:
1. 读取状态:HashSet{user_001, user_002}
2. 添加user_001 → HashSet{user_001, user_002} (已存在,无变化)
3. 更新状态:page_001 → HashSet{user_001, user_002}
4. 窗口[10:00:00, 10:05:00)的UV=2 (保持不变)

当前状态:page_001 → HashSet{user_001, user_002}
  • Checkpoint触发(10:00:30)
    这是关键时刻!
Checkpoint过程:
1. 停止新数据流入(暂停处理)
2. 序列化当前状态:
   - Kafka消费位置:Offset=3 (已处理3条消息)
   - page_001的状态:HashSet{user_001, user_002} → 序列化为字节数组
3. 写入持久化存储(HDFS):
   /checkpoints/chk-001/
     ├── metadata (Offset=3, checkpoint时间=10:00:30)
     └── states/operator_state (HashSet{user_001, user_002})
4. Checkpoint完成,恢复数据流入

当前状态:page_001 → HashSet{user_001, user_002}
Checkpoint保存:chk-001
  • 继续处理数据(10:00:30-10:00:40)
输入:user_003 访问 page_001

操作:
1. 读取状态:HashSet{user_001, user_002}
2. 添加user_003 → HashSet{user_001, user_002, user_003}
3. 更新状态:page_001 → HashSet{user_001, user_002, user_003}
4. 窗口[10:00:00, 10:05:00)的UV=3

当前状态:page_001 → HashSet{user_001, user_002, user_003}
输入:user_004 访问 page_001

操作:
1. 读取状态:HashSet{user_001, user_002, user_003}
2. 添加user_004 → HashSet{user_001, user_002, user_003, user_004}
3. 更新状态:page_001 → HashSet{user_001, user_002, user_003, user_004}
4. 窗口[10:00:00, 10:05:00)的UV=4

当前状态:page_001 → HashSet{user_001, user_002, user_003, user_004}
  • 任务失败(10:00:50)
突然情况:TaskManager节点宕机
内存状态丢失:HashSet{user_001, user_002, user_003, user_004} 消失!
  1. 恢复过程(状态+Checkpoint协作)
  • 任务重启(10:01:00)
重启操作:
1. JobManager检测到任务失败
2. 查找最近的Checkpoint:chk-001 (10:00:30)
3. 通知所有TaskManager从Checkpoint恢复
  • 状态恢复
恢复过程:
1. 从HDFS加载chk-001
2. 反序列化状态:
   - Kafka消费位置:Offset=3
   - page_001状态:HashSet{user_001, user_002}
3. 重新设置状态:page_001 → HashSet{user_001, user_002}
  • 重新消费数据
关键点:KafkaSource从Offset=3的下一个位置开始消费
         即从第4条消息(Offset=3)开始重新处理

消费队列:
Offset=3 → user_003 访问 page_001 (10:00:30)
Offset=4 → user_004 访问 page_001 (10:00:40)
Offset=5 → user_005 访问 page_001 (10:01:00)
  • 重新处理(保证Exactly-Once)
重新处理Offset=3 (user_003):
1. 读取恢复状态:HashSet{user_001, user_002}
2. 添加user_003 → HashSet{user_001, user_002, user_003}
3. 更新状态:page_001 → HashSet{user_001, user_002, user_003}
4. UV=3

重新处理Offset=4 (user_004):
1. 读取状态:HashSet{user_001, user_002, user_003}
2. 添加user_004 → HashSet{user_001, user_002, user_001, user_004}
3. 更新状态:page_001 → HashSet{user_001, user_002, user_003, user_004}
4. UV=4

最终状态:page_001 → HashSet{user_001, user_002, user_003, user_004}
与失败前完全一致!

在这里插入图片描述

  • 结论
  • 状态是"实时记忆":处理过程中持续更新,存储计算中间结果。
  • Checkpoint是"定期存档":固定时间点冻结并保存状态快照。
  • 协同工作模式:
    • 正常处理:状态实时更新 → Checkpoint定期保存
    • 失败恢复:Checkpoint加载存档 → 重新处理Checkpoint后的数据
  • Exactly-Once保证:
    • 状态恢复确保计算进度正确
    • Kafka Offset恢复确保数据不丢不重
  • 最终效果:即使任务在10:00:50失败,也能通过10:00:30的Checkpoint恢复到完全一致的计算结果,实现了端到端的精确一次处理。

二、窗口到底是什么

Flink窗口机制:流式计算的"时间切片器"

1、核心本质

流是 无限 的,但聚合计算必须在有限数据集上做。窗口就是把无限流切成一个个"有限数据块"的机制。

在这里插入图片描述

没有窗口,你就只能做"从作业启动开始的无限累加,这在真实业务中几乎没用。

2、窗口 类 型

  1. 按时间类型分
类型时间基准适用场景代码示例
处理时间窗口Flink系统时钟实时监控、延迟敏感.window(TumblingProcessingTimeWindows.of(Time.minutes(5)))
事件时间窗口数据自带时间戳准确统计、金融交易.assignTimestampsAndWatermarks(…).window(TumblingEventTimeWindows.of(…))
摄入时间窗口Source摄入时间简单场景较少使用
  1. 按窗口行为分
类型窗口划分特点可视化示例
滚动窗口不重叠简单,数据只属于一个窗口[1,2,3,4,5] [6,7,8,9,10]
滑动窗口部分重叠计算更平滑[1,2,3,4,5] [2,3,4,5,6]
会话窗口活动间隔按用户活动自动划分[1,2] 间隔 [3,4,5]
  1. 按数据驱动分
    类型 驱动方式 窗口大小 示例
    时间窗口 时间驱动 固定时间长度 5分钟窗口
    计数窗口 数据量驱动 固定数据条数 每1000条一个窗口
    全局窗口 自定义触发 无限 需要自定义触发器

3、通俗易懂

窗口(Window) 是将无限的数据流切割成有限的数据块,以便进行聚合计算的机制。它就像时间切片器,把连续的时间流切成一段段可处理的时间段。

类比:

无限流:一条永不停歇的河流
窗口:水桶,定期从河流中舀取一段水进行分析
计算结果:分析每桶水的质量(如UV、PV、平均温度等)

4、窗口的完整生命周期

  1. 窗口创建与分配
// 数据到达时,Flink判断它属于哪些窗口
Collection<W> windows = windowAssigner.assignWindows(
    element, 
    elementTimestamp, 
    context
);
// 例如:5分钟滑动窗口,1分钟步长,数据可能属于5个不同窗口
  1. 状态存储与更新
// 为每个窗口维护独立状态
for (W window : windows) {
    // 获取或创建该窗口的状态
    S state = windowState.get(window);
    // 更新状态(如添加用户ID到HashSet)
    state.update(element);
    // 保存状态
    windowState.put(window, state);
}
  1. 窗口触发计算
// 当触发条件满足时,计算窗口结果
if (trigger.onElement(element, timestamp, window, ctx)) {
    // 触发窗口计算
    Iterable<IN> windowData = windowState.get(window);
    OUT result = windowFunction.apply(window, windowData);
    output.collect(result);
}
  1. 窗口清理
// 窗口过期后,清理其状态
if (window.maxTimestamp() + allowedLateness < currentWatermark) {
    windowState.remove(window);  // 清理状态,释放内存
}

5、窗口的三大核心组件

  1. 窗口分配器(Window Assigner)
    作用:决定数据属于哪个/哪些窗口
// 内置分配器
TumblingEventTimeWindows.of(Time.minutes(5))     // 5分钟滚动窗口
SlidingProcessingTimeWindows.of(Time.minutes(5), Time.minutes(1))  // 5分钟滑动,1分钟步长
EventTimeSessionWindows.withGap(Time.minutes(30))  // 30分钟会话窗口
  1. 触发器(Trigger)
    作用:决定何时触发窗口计算
// 内置触发器
ProcessingTimeTrigger.create()  // 处理时间触发
EventTimeTrigger.create()       // 事件时间触发
CountTrigger.of(100)            // 每100条触发
PurgingTrigger.of(ProcessingTimeTrigger.create())  // 触发后清理状态
  1. 驱逐器(Evictor)
    作用:窗口计算前/后移除部分数据
CountEvictor.of(1000)   // 保留最近1000条
TimeEvictor.of(Time.seconds(10))  // 保留最近10秒
DeltaEvictor.of(threshold, deltaFunction)  // 基于阈值

窗口在Flink架构中的位置

在这里插入图片描述
窗口是Flink流批一体的核心,它将无限流转化为有限批处理,同时保持了 流处理 的实时性。正确理解和使用窗口,是掌握Flink流式计算的关键。

6、窗口的使用

窗口类型的三个维度
Flink窗口实际上是三个独立维度的交叉组合:

窗口 = 时间类型(处理/事件/摄入) × 窗口行为(滚动/滑动/会话) × 驱动方式(时间/计数/全局)