流批一体架构核心设计 - 第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 快照。流程大概是:

  1. JobManager 往所有 source 注入 barrier
  2. barrier 随数据流向下游传播
  3. 每个算子收到 barrier 后,把当前状态快照到状态后端
  4. 所有算子确认后,checkpoint 完成

这里有个关键设计:barrier 对齐。如果某个算子收到多个输入的 barrier,它必须等所有输入都对齐了才能做快照。这保证了 exactly-once 语义,但也引入了额外的延迟。

避坑指南:我曾经在生产环境遇到 checkpoint 超时,查了半天发现是某个算子的状态太大(超过 100GB),导致每次快照都要写 30 秒。后来把 checkpoint 间隔从 1 分钟改成 5 分钟,同时启用了增量 checkpoint,问题就解决了。

Flink Runtime 核心架构图

下面这张图是我自己总结的 Flink Runtime 核心架构,包含了 JobManager、TaskManager、Slot、数据交换和状态后端的关系:

Flink Runtime 核心架构 JobManager Dispatcher JobMaster ResourceManager 调度 · 协调 · 容错 TaskManager Slot 1 Slot 2 ... subtask 实例 执行 · 状态 · 数据交换 RPC / 资源请求 数据交换策略 Pipelined:低延迟 · 实时推送 适合流作业,反压敏感 Batch:高吞吐 · 批次传输 适合批作业,shuffle 优化 状态后端与容错 MemoryStateBackend FsStateBackend RocksDBStateBackend Checkpoint · Savepoint · 增量快照 · 异步 barrier

总结一下

Flink Runtime 的核心设计,说白了就是三件事:谁调度(JobManager)、谁干活(TaskManager)、数据怎么传(Pipelined/Batch)。再加上状态后端和 checkpoint 机制,构成了整个流批一体的基石。

我个人觉得,理解这些概念最好的方式不是背文档,而是去生产环境里踩坑。比如你亲手调过一次 checkpoint 超时,或者被反压搞到 OOM 一次,这些知识就再也忘不掉了。