Flink 作业能从 Kafka 读到数据,并不意味着它已经具备生产恢复能力。任务运行在什么进程、状态存在哪里、谁负责申请资源,以及故障后从哪个位置继续处理,需要在部署时分别确定。

本文以 Apache Flink 2.3 文档为基线,讨论有状态流作业的生产拓扑。图中的连接区分任务调度、记录传输与状态持久化;对象存储、Kafka 和 Kubernetes 都是外部系统,不属于 TaskManager 内部组件。批处理的调度与 shuffle 策略另有配置,不能把下面的持续流水线直接套用到所有有界作业。

集群内外的职责

JobManager 负责协调执行,TaskManager 执行 source、转换和 sink。业务记录通常在 TaskManager 及外部系统之间流动,不需要经过 JobManager 转发。客户端负责提交;作业以 detached 方式启动后,客户端退出并不等于作业结束。

图 1:Flink 控制连接、业务数据与快照存储的边界

flowchart TB
  C["客户端"] -.->|"提交"| JM["JobManager"]
  JM -.->|"调度与协调"| TM["TaskManagers"]
  S["外部数据源"] -->|"业务记录"| TM
  TM -->|"业务记录"| D["外部接收系统"]
  TM -->|"状态快照"| FS["持久化存储"]
  JM -.->|"恢复元数据"| FS

图 1 的虚线表示控制关系。增大 JobManager 的 CPU 配额不能直接解决 sink 写入变慢;增加 TaskManager 也不能弥补不可访问的快照目录。故障定位要沿着发生问题的连接检查。

JobManager 内的三个角色

Dispatcher 接收提交并为 job 启动 JobMaster;每个 JobMaster 管理一个 JobGraph 的执行。Flink ResourceManager 管理 slot,并与资源提供方配合获取 TaskManager。这里的 ResourceManager 是 Flink 内部组件,和 YARN 的同名服务不是同一个进程。组件职责见官方架构说明。

图 2:JobManager 内的提交、单作业协调与资源管理

flowchart TB
  subgraph JM["JobManager 进程"]
    D["Dispatcher"]
    J1["JobMaster:job A"]
    J2["JobMaster:job B"]
    R["Flink ResourceManager"]
    D --> J1
    D --> J2
    J1 -->|"申请 slot"| R
    J2 -->|"申请 slot"| R
  end
  R -->|"管理资源"| T["TaskManagers"]
  J1 -.->|"部署与恢复任务"| T
  J2 -.->|"部署与恢复任务"| T

图 2 表示逻辑职责,不要求把三个角色分别部署成三个 Pod。JobMaster 与 job 一一对应,也不意味着一个 Application 只能生成一个 job。

Session 和 Application 的部署边界

Session 集群先启动,再接收不同应用提交的 job。它适合需要复用既有执行资源的任务,但共享 TaskManager 的作业也共享进程故障影响范围:一个进程退出,该进程上多个 job 的任务都可能需要恢复。

图 3:Session 集群复用执行资源及其故障影响范围

flowchart TB
  A["应用 A 客户端"] --> JM["长期运行的 JobManager"]
  B["应用 B 客户端"] --> JM
  JM --> T1
  JM --> T2
  subgraph T1["TaskManager 1:共享 JVM"]
    A1["job A 的任务"]
    B1["job B 的任务"]
  end
  subgraph T2["TaskManager 2"]
    A2["job A 的其他任务"]
  end
  F["TM 1 退出"] -.-> A1
  F -.-> B1

Application 模式把应用的 main() 放到 JobManager 侧运行,集群生命周期与该应用绑定。一次应用执行可以生成多个 job。对于需要独立发布、资源预算和故障范围的长期作业,按应用划分集群更容易管理;同一 Kubernetes 节点上的不同应用仍可能争用磁盘与网络。

图 4:Application 集群按应用隔离并承载一个或多个 job

flowchart TB
  P["提交应用包"] --> A
  subgraph A["Application A 专属集群"]
    M["JobManager 执行 main"]
    J1["job A1"]
    J2["job A2"]
    T["A 的 TaskManagers"]
    M --> J1
    M --> J2
    J1 --> T
    J2 --> T
  end
  B["Application B 专属集群"]

图 4 的隔离单位是应用。官方 2.3 部署概览以 Application 和 Session 为主要模式;旧资料中的 Per-Job Cluster 已属于弃用路线,不应与当前推荐模式并列给出无版本限制的部署建议。部署模式及生命周期。

资源提供方决定谁启动进程

Session/Application 回答集群供谁使用;Standalone、YARN、Native Kubernetes 回答进程由谁提供。Standalone 可以运行在裸机、容器或 Kubernetes 上,因此“运行在 Kubernetes”本身不能证明启用了 native 集成。

图 5:部署模式与资源提供方是两项独立选择

flowchart TB
  P["部署设计"] --> M["应用与生命周期"]
  P --> R["进程与资源供给"]
  M --> S["Session:共享集群"]
  M --> A["Application:专属集群"]
  R --> ST["Standalone:外部启动 TM"]
  R --> Y["YARN:申请容器"]
  R --> K["Native Kubernetes:申请 Pod"]

Standalone 的 Flink ResourceManager 只能分配已注册 TaskManager 的 slot,不会自行启动新的 TaskManager。图 5 因而也决定了扩容操作的归属:Standalone 需要外部管理进程数量,native 集成可以按资源需求申请或释放 TaskManager。

图 6:Native Kubernetes 中 TaskManager Pod 的申请与注册

sequenceDiagram
  participant J as JobMaster
  participant R as Flink ResourceManager
  participant K as Kubernetes API
  participant T as TaskManager Pod
  J->>R: 请求执行资源
  R->>K: 创建 TaskManager Pod
  Note over K,T: Kubernetes 调度并启动 Pod
  T->>R: 注册可用 slot
  R-->>J: 提供资源
  J->>T: 部署任务

Flink Kubernetes Operator 是额外的生命周期管理组件,负责管理 Flink 部署资源;它不参与逐条记录处理,也不替代 JobMaster 的任务调度。Native Kubernetes 集成本身可以不经 Operator 使用。图 6 只画 Flink 运行时申请资源的路径。Native Kubernetes 与 Operator 的关系。

并行度、slot 与 TaskManager

TaskManager 是 JVM 进程,slot 是资源调度单位。默认 slot sharing 允许同一 job 中不同算子的子任务共享一个 slot。单一共享组、资源充足的常见流水线中,所需 slot 数可按最高算子并行度理解;分开共享组或设置更细的资源要求后,不能继续套用这个简化估算。

图 7:两个 slot 承载并行度为二的算子流水线

flowchart TB
  subgraph TM["TaskManager JVM"]
    subgraph S0["slot 0"]
      A0["Source 0"] --> B0["Map 0"]
    end
    subgraph S1["slot 1"]
      A1["Source 1"] --> B1["Map 1"]
    end
    CPU["共享进程 CPU 资源"]
    CPU -.-> S0
    CPU -.-> S1
  end

图 7 省略了网络连接,强调 slot 与执行资源的关系。slot 不提供 CPU 隔离;增加 slot 数也不会产生更多 CPU 核。多个 slot 共用一个 JVM 时,进程退出和部分内存问题会影响其中所有任务。强隔离还需要进程、容器和资源配额配合。

算子链与网络交换

可链接的算子可以组成一个 task,减少线程交接和序列化开销。是否形成 chain 还受并行度、分区方式和 chaining 设置约束;不能把“相邻算子”直接等同于“一条 chain”。keyBy 需要按 key 重分区,通常在链之间引入网络交换。

图 8:算子链内部调用与 keyBy 后的跨任务交换

flowchart TB
  subgraph C0["Task 0:同线程算子链"]
    S0["Source 0"] --> M0["Map 0"]
  end
  subgraph C1["Task 1:同线程算子链"]
    S1["Source 1"] --> M1["Map 1"]
  end
  M0 -->|"keyBy"| A0["聚合子任务 0"]
  M0 -->|"keyBy"| A1["聚合子任务 1"]
  M1 -->|"keyBy"| A0
  M1 -->|"keyBy"| A1

图 8 中的两个聚合子任务按 key 接收记录,同一个 key 的状态归属同一个并行实例。算子链与 slot sharing 是两种机制:多个 task 可以共享 slot,但仍保有各自的任务线程。调度与链接规则以 Flink Architecture 为准。

内存预算覆盖整个进程

容器的内存上限需要覆盖 JVM heap、managed memory、network memory、其他堆外内存,以及 JVM 自身的 metaspace 和 overhead。只把 heap 设置成容器上限,会挤掉堆外部分的空间。

图 9:TaskManager 进程内存的预算层次

flowchart TB
  P["Total Process Memory"] --> F["Total Flink Memory"]
  P --> M["JVM Metaspace"]
  P --> O["JVM Overhead"]
  F --> H["Framework / Task Heap"]
  F --> G["Managed Memory"]
  F --> N["Network Memory"]
  F --> X["Framework / Task Off-heap"]

图 9 展示包含关系,不代表这些区域的固定比例。RocksDB 的部分 native 内存可纳入 managed memory 管理;用户代码的额外堆外分配也需要预算。配置时可以从总进程内存出发,再根据实际状态量和网络压力调整,避免同时给多组互相约束的总量与分量赋值。TaskManager 内存配置。

运行时状态与恢复副本

State backend 决定工作状态的组织和访问方式;checkpoint storage 决定恢复快照存在哪里。HashMapStateBackend 把工作状态放在 Java heap,EmbeddedRocksDBStateBackend 使用 TaskManager 本地 RocksDB。后者仍消耗内存、磁盘与序列化资源,本地文件不能独立提供节点丢失后的恢复保证。

图 10:运行时状态与持久化 checkpoint 的两条存储路径

flowchart TB
  U["算子更新状态"] --> B{"选择 state backend"}
  B --> H["HashMap:Java heap"]
  B --> R["EmbeddedRocksDB:本地数据库"]
  H -->|"生成快照"| C["Checkpoint storage"]
  R -->|"生成快照"| C
  C --> D["共享持久化文件系统或对象存储"]
  D -.->|"失败后恢复"| N["新 TaskManager"]

图 10 只列两种常见 backend。Flink 2.3 还提供面向远端状态的 ForSt,但该版本文档仍将它标为实验性,并列出 savepoint 格式等限制,不能当作本地 RocksDB 的无差别替换。生产方案应同时选择 backend、快照存储、访问权限和保留策略。State Backends,Checkpoints。

Checkpoint 完成的含义

Checkpoint 保存相互匹配的输入位置与算子状态。aligned checkpoint 中,先收到 barrier 的输入需要等待其他输入的同编号 barrier,随后形成一致状态切面;状态持久化可以异步完成。checkpoint 被触发不等于已经可用于恢复,监控需要查看最近一次成功完成的 checkpoint。

图 11:双输入算子的 aligned checkpoint 与完成确认

sequenceDiagram
  participant A as 输入 A
  participant B as 输入 B
  participant O as 双输入算子
  participant S as 快照存储
  participant J as Checkpoint 协调器
  A->>O: barrier n
  Note over A,O: 暂停消费 A 在 barrier 后的记录
  B->>O: barrier 前的剩余记录
  B->>O: barrier n
  O->>O: 建立状态切面并向下游传播 barrier
  O->>S: 异步持久化快照
  Note over A,O: 可恢复消费后续记录
  S-->>O: 持久化成功
  O-->>J: ACK n
  Note over J: 收齐所需 ACK 后完成 checkpoint

图 11 省略了 source 触发和下游算子的独立快照过程。unaligned checkpoint 会把相关在途数据也纳入快照,以减少背压下的对齐等待,代价包括额外 I/O 和快照体积。

Flink 的 exactly-once 状态一致性也不意味着外部副作用天然去重。恢复会重放输入,source 必须支持相应位置的重读,sink 需要匹配的事务提交或幂等语义。向外部 HTTP 服务发出的任意写请求,不会仅因启用 checkpoint 就自动获得端到端 exactly-once。状态恢复机制。

HA、升级与恢复

TaskManager 故障需要重新提供执行资源并恢复状态;JobManager 故障还涉及协调者选举和恢复元数据。Kubernetes 重新创建 Pod 只解决进程存在的问题,不能凭空重建已经丢失的输入位置和状态。

图 12:Kubernetes HA 与作业恢复所需的外部状态

flowchart TB
  F["原 JobManager 失效"] --> J["恢复的 JobManager"]
  K["Kubernetes HA:选举与指针"] --> J
  H["持久化 HA 元数据"] --> J
  J -->|"重新部署任务"| T["可用 TaskManagers"]
  C["已完成 checkpoint"] -->|"恢复算子状态"| T
  S["可重放的数据源"] -->|"从匹配位置继续"| T

Kubernetes HA 依赖 ConfigMap 等 Kubernetes 资源及相应权限;JobManager 恢复所需元数据保存在 high-availability.storageDir,Kubernetes 中保存相关指针。HA 存储和 checkpoint 存储的用途不同,两者都应在新进程所在位置可访问。图 12 也解释了为什么“Pod 已恢复 Running”不足以证明业务恢复。Kubernetes HA Services。

Savepoint 与升级路径

Checkpoint 主要服务运行期故障恢复,savepoint 常用于受控停机、升级和迁移。两者都依赖状态文件,但生命周期、格式和可迁移条件存在区别;保留的 checkpoint 也可以作为恢复入口,不能把“人工恢复”视作 savepoint 独有能力。

图 13:从旧版本状态恢复到新版本的验证路径

flowchart TB
  O["旧版本作业"] -->|"生成并确认成功"| S["可用 savepoint"]
  S --> C["检查 operator UID、序列化与版本兼容"]
  C --> T["隔离环境恢复新版本"]
  T --> V{"状态和输出符合预期"}
  V -->|"是"| P["执行受控生产切换"]
  V -->|"否"| R["修复兼容问题并保留旧版本恢复路径"]
  R --> C

图 13 的隔离恢复应使用独立输出位置,避免测试作业与生产作业重复写入。稳定的 operator UID 能帮助状态映射,但不能保证任意类型变更或 Flink 跨版本升级都兼容。保留旧应用包、配置和可读取的状态文件,是准备回退路径的一部分;外部 sink 已提交的数据还需要单独处理。Savepoints,Checkpoints vs. Savepoints。

从背压定位吞吐瓶颈

业务记录向下游流动,背压沿反方向传播。Source 显示高背压,可能是下游 sink 写入慢,也可能是中间算子计算或状态访问慢。只增加 source 并行度,可能继续增加下游积压。

图 14:业务数据与背压的相反传播方向

flowchart TB
  S["Source"] -->|"记录"| A["聚合算子"]
  A -->|"记录"| K["Sink"]
  K -->|"写入"| D["外部存储"]
  D -.->|"写入变慢"| K
  K -.->|"输出受阻"| A
  A -.->|"输出受阻"| S
  A --> M["观察 busy、idle、back pressured"]
  K --> M

图 14 的定位顺序从下游受阻位置向前检查。Web UI 的聚合指标可能掩盖子任务间差异,出现数据倾斜时需要查看各 subtask 的忙碌程度、输入量和状态大小。checkpoint 持续失败还会让最近成功恢复点越来越旧,即使作业仍显示 RUNNING。背压指标及其定义。

上线验证至少应覆盖 TaskManager 丢失、JobManager 重建、状态存储短暂不可用,以及 sink 变慢后的追赶。每种演练记录恢复耗时、重放范围和输出重复情况;生产 REST/Web UI 入口应放在受控网络和身份边界内。Flink 可以运行提交者的代码,不能把可提交作业的接口当成普通只读监控页暴露。Production Readiness Checklist。

Flink 专题导航

本篇是 Flink 部署专题入口。机制与选型可结合已有系列阅读:

阅读位置 内容
本篇 生产拓扑、组件职责、资源与恢复边界
事件时间、水位线与状态恢复 分布式系统系列中的时间与一致性机制
Spark 与 Flink:两种计算引擎的设计选择 Spark 系列中的引擎比较

参考资料

以上链接固定在 Apache Flink 2.3 文档分支;部署其他版本时需要核对配置项、connector 版本和恢复兼容性。图 1–14 是依据这些机制绘制的职责示意,不表示已经执行生产故障演练。