4. Flink核心概念与运行架构
好,咱们进入第四章。这一章可以说是 Flink 的“内功心法”。你写 Flink 作业,说白了就是在跟这四个东西打交道:Checkpoint、State、Time、Window。再加上 JobManager 和 TaskManager 这两个“大管家”和“干活的”,以及任务是怎么提交上去的——搞懂这些,你才算真正入了 Flink 的门。
我记得我刚接触 Flink 那会儿,上来就写 DataStream API,觉得挺简单。结果一上线,数据对不上,状态丢了,窗口乱跳……嗯,那会儿才意识到,底层概念不扎实,迟早要还债。
4.1 Flink 四大基石
这四个东西不是孤立的。它们互相配合,构成了 Flink 实时处理的核心能力。我习惯把它们分成两组来理解:“可靠性与状态管理”(Checkpoint + State)和 “时间与窗口语义”(Time + Window)。
4.1.1 Checkpoint:容错的“快照”机制
Checkpoint 是什么?说白了,就是 Flink 定期给整个作业的当前状态拍一张“合影”。这张合影里包含了每个算子的状态,以及数据流中的偏移量(比如 Kafka 的 offset)。
一旦作业挂了,Flink 可以从最近一次成功的 Checkpoint 恢复。数据不会丢,状态也能回到那一刻。这就是 Flink 的“精确一次”(Exactly-Once)语义的基石。
核心要点:
- Barrier(对齐栅栏):Flink 在数据流中插入一种特殊记录,叫 Barrier。当所有输入流的 Barrier 都到达某个算子时,该算子就开始做快照。这保证了数据的一致性。
- 异步与增量:从 Flink 1.11 开始,默认使用异步 + 增量 Checkpoint。只记录变化的部分,效率高很多。
- 配置建议:我一般把 Checkpoint 间隔设在 30 秒到 1 分钟之间。太频繁了,对 HDFS 压力大;太久了,恢复时间会变长。
我的经验: 曾经有个项目,Checkpoint 总是超时失败。查了半天,发现是 Kafka 分区数太多,Barrier 对齐时间过长。后来我调整了 setMinPauseBetweenCheckpoints,让两次 Checkpoint 之间至少间隔 500ms,问题就解决了。
4.1.2 State:算子的“记忆”
State 就是 Flink 算子的“记忆”。没有状态,Flink 就是一个纯粹的 ETL 工具。有了状态,它才能做聚合、去重、模式匹配这些复杂逻辑。
Flink 的状态分为两大类:
| 类型 | 说明 | 常见场景 |
|---|---|---|
| Keyed State | 基于 Key 划分的状态。每个 Key 都有自己的状态副本。 | 按用户 ID 统计点击次数、按设备 ID 做去重。 |
| Operator State | 每个算子实例共享的状态。不按 Key 划分。 | Kafka Source 记录的 offset、自定义的负载均衡逻辑。 |
Keyed State 又细分为几种:ValueState、ListState、MapState、ReducingState、AggregatingState。我个人最常用的是 ValueState 和 MapState。前者存单个值,后者存键值对,适合做缓存。
避坑指南: 我曾经犯过一个错误——在 RichFlatMapFunction 里声明了 ValueState,但忘了在 open() 方法里初始化。结果运行时一直报空指针。记住:所有状态描述符(StateDescriptor)必须在 open() 中创建,不能在构造函数里搞。
4.1.3 Time:时间的三种语义
Flink 里时间有三种,你想想看,它们有什么区别?
- Event Time(事件时间):数据本身携带的时间戳。比如 IoT 设备上报数据时的时间。这是最真实的时间,但可能乱序。
- Ingestion Time(摄入时间):数据进入 Flink 系统的时间。介于 Event Time 和 Processing Time 之间。
- Processing Time(处理时间):数据被算子处理时的机器时间。最简单,但结果不稳定。
我建议:只要业务允许,优先用 Event Time。为什么?因为 Processing Time 受机器负载影响,同一批数据重跑结果可能不一样。Event Time 加上 Watermark,才能保证结果的可复现性。
Watermark 的作用: 它就像一个“闹钟”,告诉 Flink:“时间戳小于等于 X 的数据我已经收齐了,你可以触发窗口计算了。” 如果 Watermark 设置得太激进,容易丢数据;太保守,延迟又高。我一般根据业务容忍度来调,比如允许 10 秒的乱序,就把 Watermark 延迟设为 10 秒。
4.1.4 Window:无界流变有界计算
数据是无限的,但计算需要边界。Window 就是那个“边界”。
Flink 提供了丰富的窗口类型:
| 窗口类型 | 说明 | 示例 |
|---|---|---|
| Tumbling Window | 固定大小,不重叠。比如每 5 分钟一个窗口。 | 统计每 5 分钟的页面 PV。 |
| Sliding Window | 固定大小,但可以重叠。比如窗口大小 10 分钟,滑动步长 5 分钟。 | 每 5 分钟统计过去 10 分钟的 UV。 |
| Session Window | 基于活动间隔。一段时间没有新数据,窗口就关闭。 | 用户会话分析、电商购物车。 |
| Global Window | 所有数据在一个窗口里。需要自定义触发器。 | 自定义的批量处理逻辑。 |
这里有个细节:窗口的触发时机。默认情况下,Event Time 窗口在 Watermark 超过窗口结束时间时触发。但如果你用了 allowedLateness,窗口会再等一段时间,允许迟到的数据更新结果。
我的习惯: 做实时大屏时,我一般用 Tumbling Window + Event Time。窗口大小设 1 分钟,Watermark 延迟 5 秒,allowedLateness 设 30 秒。这样既保证了实时性,又给了迟到数据一点“缓冲期”。
4.2 JobManager 与 TaskManager 角色
Flink 集群的运行时架构,说白了就是“一主多从”。
- JobManager(JM):集群的“大脑”。负责接收作业、调度任务、协调 Checkpoint、故障恢复。它里面有个
ResourceManager,负责分配资源。 - TaskManager(TM):集群的“肌肉”。负责执行具体的任务。每个 TM 有多个 Task Slot,每个 Slot 可以运行一个线程。
你可能会问:一个 Task Slot 能跑几个算子?答案是:一个 Slot 可以跑一个完整的算子链(Operator Chain)。Flink 会把能链在一起的算子(比如 map + filter)合并到一个线程里执行,减少线程切换和数据序列化的开销。
关键配置:
jobmanager.memory.process.size:JM 的总内存。我一般给 2-4 GB。taskmanager.memory.process.size:TM 的总内存。根据任务复杂度来,通常 4-8 GB。taskmanager.numberOfTaskSlots:每个 TM 的 Slot 数。建议等于 CPU 核数。
4.3 任务提交流程
一个 Flink 作业从提交到运行,经历了哪些步骤?我画了一张图,你一看就明白。
具体步骤是这样的:
- 客户端提交:你用
flink run命令或者通过 REST API 提交作业。客户端会解析StreamGraph,生成JobGraph,然后发给 JobManager。 - JobManager 接收:JM 收到 JobGraph 后,会把它转换成
ExecutionGraph。这是一个可并行执行的 DAG(有向无环图)。 - 资源申请:JM 的 ResourceManager 向 TaskManager 请求 Slot。如果资源不够,作业会排队等待。
- 任务分发:Dispatcher 将 ExecutionGraph 中的任务分发给对应的 TaskManager。每个 TaskManager 在自己的 Slot 里启动线程执行任务。
- 开始运行:TaskManager 开始处理数据,同时定期向 JobManager 汇报心跳和状态。JobManager 负责监控和 Checkpoint 协调。
注意: 如果作业提交后一直卡在“RUNNING”状态,但就是不处理数据,多半是资源不够。我曾经遇到过 YARN 集群上 Slot 被其他作业占满的情况。解决办法是:要么调大集群资源,要么给作业设置更高的优先级。
4.4 本章小结
这一章的内容有点多,但都是硬核知识。Checkpoint 保证了可靠性,State 提供了记忆能力,Time 和 Window 让无界流变得可计算。JobManager 和 TaskManager 的分工,以及任务提交流程,是你理解 Flink 运行时的基础。
我个人觉得,学 Flink 就像搭积木。四大基石是积木块,运行架构是搭积木的图纸。图纸看懂了,积木块摸透了,后面写代码就是水到渠成的事。