流批一体架构核心设计 - 第3节
Flink Runtime 核心:JobManager 与 TaskManager 架构
聊到 Flink 的运行时,很多人第一反应就是「JobManager 和 TaskManager」。说白了,这就是 Flink 的 Master-Worker 架构。我刚开始接触 Flink 时,总觉得这俩角色跟 Hadoop 的 NameNode/DataNode 差不多,后来踩了几个坑才发现——完全不是一回事。
JobManager:集群的「大脑」
JobManager 负责三件事:接收作业、调度任务、协调容错。它内部又分了三个组件:
- Dispatcher:接收提交的作业,启动 JobMaster
- JobMaster:每个作业一个,负责调度和 checkpoint 协调
- ResourceManager:管理 TaskManager 的 slot 资源
我在项目中遇到过一个问题:作业提交后一直卡在「RUNNING」状态,查了半天才发现是 Dispatcher 和 ResourceManager 之间的 RPC 超时设置太短。嗯,这种细节在文档里往往一笔带过,但生产环境里特别容易翻车。
TaskManager:真正的「苦力」
TaskManager 是真正干活的地方。它启动多个 Task Slot,每个 Slot 运行一个线程,执行 subtask。这里有个关键点:Slot 是资源隔离的单位,但不是线程安全的边界。
核心结论:一个 Slot 可以运行多个 subtask,但这些 subtask 共享同一个 JVM 的堆内存和线程资源。所以,别把 Slot 数量等同于并行度——这是新手最容易搞混的地方。
Task Slot 与并行度:别再搞混了
我见过不少同学把「并行度」和「Slot 数量」划等号。其实它们的关系是这样的:
| 概念 | 定义 | 控制方 |
|---|---|---|
| Task Slot | TaskManager 上固定的资源槽位 | 配置文件(taskmanager.numberOfTaskSlots) |
| 并行度 | 算子被拆分的 subtask 数量 | 代码 / 提交参数 / 算子级别 |
| 最大并行度 | 状态后端可以支持的最大并行度上限 | 代码设置(setMaxParallelism) |
举个例子:你有 2 台 TaskManager,每台 4 个 Slot,总共 8 个 Slot。你提交一个并行度为 10 的作业——会发生什么?
答案是:作业会失败,因为 Slot 不够。但如果你把并行度设为 6,那 6 个 subtask 会分配到 8 个 Slot 中的 6 个,剩下 2 个 Slot 空闲。
我的习惯:生产环境里,我通常把 Slot 数设为 CPU 核数,并行度设为 Slot 总数的 1.5~2 倍。这样既能充分利用资源,又不会因为数据倾斜导致某些 Slot 过载。
数据交换策略:Pipelined vs Batch
Flink 最让我佩服的一点,就是它把流和批的数据交换策略统一了。你想想看,流处理需要低延迟,批处理需要高吞吐——这俩需求本质上是冲突的。Flink 怎么解决的?
Pipelined(流水线)模式
上游一产生数据,立刻推给下游。延迟低,但需要上下游同时在线。适合流作业。
- 优点:延迟毫秒级,内存占用低
- 缺点:反压时容易 OOM
- 典型场景:实时 ETL、监控告警
Batch(批式)模式
上游先把数据写满一个 buffer,或者等整个批次结束,再一次性发给下游。吞吐高,但延迟大。
- 优点:吞吐量高,适合大规模 shuffle
- 缺点:延迟秒级甚至分钟级
- 典型场景:离线报表、历史数据回溯
我曾经踩过的坑:在流作业里用了 Batch 模式做 join,结果数据延迟从 100ms 飙升到 5 分钟。后来才发现,Batch 模式默认会等整个 watermark 周期结束才触发计算——流作业里千万别这么干。
Flink 在 Runtime 层通过 ExchangeMode 来控制这两种策略。源码里其实就两个枚举值:PIPELINED 和 BATCH。但底层实现差别很大——Pipelined 用的是有界或无界 buffer,Batch 用的是 sort-shuffle 或 hash-shuffle。
状态后端与容错机制
状态后端,说白了就是「状态存哪儿」。Flink 提供了三种选择:
| 状态后端 | 存储位置 | 适用场景 |
|---|---|---|
| MemoryStateBackend | TaskManager 内存 + JobManager 内存 | 开发测试,小状态 |
| FsStateBackend | TaskManager 内存 + 外部文件系统 | 生产环境,中等状态 |
| RocksDBStateBackend | TaskManager 本地磁盘 + 外部文件系统 | 超大状态(TB 级) |
我个人习惯:生产环境无脑选 RocksDB。为什么?因为 Memory 和 Fs 都把状态放在堆内存里,一旦状态超过几十 GB,GC 就能把你搞死。RocksDB 用磁盘+内存的 LSM-Tree 结构,虽然读写慢一点,但胜在稳定。
Checkpoint 与 Savepoint
容错的核心就是 checkpoint。Flink 用 Chandy-Lamport 算法的变种——异步 barrier 快照。流程大概是:
- JobManager 往所有 source 注入 barrier
- barrier 随数据流向下游传播
- 每个算子收到 barrier 后,把当前状态快照到状态后端
- 所有算子确认后,checkpoint 完成
这里有个关键设计:barrier 对齐。如果某个算子收到多个输入的 barrier,它必须等所有输入都对齐了才能做快照。这保证了 exactly-once 语义,但也引入了额外的延迟。
避坑指南:我曾经在生产环境遇到 checkpoint 超时,查了半天发现是某个算子的状态太大(超过 100GB),导致每次快照都要写 30 秒。后来把 checkpoint 间隔从 1 分钟改成 5 分钟,同时启用了增量 checkpoint,问题就解决了。
Flink Runtime 核心架构图
下面这张图是我自己总结的 Flink Runtime 核心架构,包含了 JobManager、TaskManager、Slot、数据交换和状态后端的关系:
总结一下
Flink Runtime 的核心设计,说白了就是三件事:谁调度(JobManager)、谁干活(TaskManager)、数据怎么传(Pipelined/Batch)。再加上状态后端和 checkpoint 机制,构成了整个流批一体的基石。
我个人觉得,理解这些概念最好的方式不是背文档,而是去生产环境里踩坑。比如你亲手调过一次 checkpoint 超时,或者被反压搞到 OOM 一次,这些知识就再也忘不掉了。