从入门到源码,全面掌握流批一体计算引擎
前言
在实时数据处理需求爆发式增长的今天,Apache Flink 已成为流计算领域的事实标准。无论是双 11 的实时交易大屏、金融风控系统的毫秒级响应,还是实时数仓的秒级数据新鲜度,背后都离不开 Flink 的身影。
与传统批处理引擎不同,Flink 从设计之初就以流处理 为核心理念------它认为"批是流的特例"。这一颠覆性的思路使得 Flink 能够同时高效处理实时流数据和离线批数据,真正实现了流批一体的架构愿景。
本文将从背景场景、架构设计、核心概念、SQL 支持、实战用法、性能调优、源码分析、CDC 实时数仓等多个维度,对 Flink 进行全面深度的解析。无论你是初次接触 Flink 的开发者,还是希望深入理解其内部机制的架构师,都能从中找到有价值的内容。
一、项目背景和应用场景
1.1 诞生背景
Apache Flink 是一个开源的分布式流处理框架,最初诞生于德国柏林的几所大学的联合研究项目 Stratosphere。它的名字来源于德语中"敏捷"(flink)一词,恰如其分地体现了它对低延迟、高吞吐实时数据处理的追求。下面我们通过时间线来回顾 Flink 的发展历程。
历史时间线
2010-2011 │ 柏林工业大学、柏林洪堡大学等启动 Stratosphere 研究项目
│ → 目标:构建下一代大数据分析平台
│
2014 │ Stratosphere 核心成员创建 Flink,捐献给 Apache
│ → 项目正式更名为 Apache Flink(德语"敏捷"之意)
│
2014.12 │ Flink 成为 Apache 顶级项目
│ → 社区快速发展
│
2016 │ 阿里巴巴开始大规模使用 Flink(内部版 Blink)
│ → 双 11 实时大屏、风控、推荐
│
2019 │ 阿里巴巴收购 Flink 商业公司 Ververica
│ → Blink 核心功能回馈社区,Flink 1.9 发布
│
2020 │ Flink 1.11 发布,CDC 生态起步
│ → 实时数仓概念兴起
│
2022 │ Flink 1.16 发布,流批一体进入成熟期
│ → 统一 API、统一执行引擎
│
2023-2024 │ Flink 1.18/1.19 发布
│ → Generalized Incremental Checkpoint、Watermark 对齐
│
2025 │ Flink 2.0 发布
│ → 全面拥抱流批一体、存算分离、Disaggregated State
技术演进驱动力
在 Flink 出现之前,业界的流处理方案面临诸多痛点:Storm 虽然延迟低但吞吐量有限且没有状态管理能力;Spark Streaming 通过微批(Mini-batch)模拟流处理,虽然吞吐量高但延迟始终停留在秒级。Flink 诞生的核心原因就是要解决传统流处理和批处理架构的这些局限性:
| 问题 | Storm 表现 | Spark Streaming 表现 | Flink 解决方案 |
|---|---|---|---|
| 延迟 | 毫秒级,但吞吐低 | 秒级(微批),延迟高 | 毫秒级 + 高吞吐 |
| 语义保证 | At-Least-Once | Exactly-Once(微批内) | 原生 Exactly-Once |
| 状态管理 | 无内置状态 | 有限状态支持 | 强大的有状态计算 |
| 事件时间 | 不支持 | 支持但受限于微批 | 原生事件时间 + Watermark |
| 流批一体 | 仅流处理 | 批为主,流为辅 | 流批一体,流为核心 |
| 窗口机制 | 基础窗口 | 基于微批的窗口 | 灵活窗口(会话窗口等) |
性能对比
Storm vs Spark Streaming vs Flink 核心指标对比
| 指标 | Storm | Spark Streaming | Flink |
|---|---|---|---|
| 处理模型 | 逐条处理 | 微批(Mini-batch) | 逐条处理(可微批优化) |
| 延迟 | ~10ms | ~500ms-2s | ~10ms |
| 吞吐量 | 低(10 万条/秒) | 高(100 万条/秒) | 高(100 万条/秒) |
| Exactly-Once | 不支持 | 微批内支持 | 端到端支持 |
| 状态大小 | 无状态 | 受限于微批 | TB 级状态 |
| API 丰富度 | 低 | 高 | 非常高 |
测试环境 :10 节点集群,每节点 16 核 64GB 内存,10Gbps 网络
数据来源:Yahoo Streaming Benchmark 变体
核心设计理念
Flink 之所以能在众多流处理框架中脱颖而出,与它坚持的五大核心设计理念密不可分。这些理念贯穿了 Flink 的整体架构和 API 设计:
-
流处理优先(Stream-First)
- 一切数据皆为流:批处理是有界流的特例。这意味着 Flink 不是在批处理引擎上"模拟"流处理,而是用真正的流引擎来"优化"批处理
- 真正的逐事件处理,非微批模拟
- 事件驱动架构,低延迟响应
-
有状态计算(Stateful Computation)
- 内置分布式状态管理。所谓"有状态计算",是指算子可以记住之前处理过的数据信息(比如累计计数、用户画像),而不是把每条数据当作独立事件处理
- 支持 TB 级状态(基于 RocksDB),状态数据可以远超内存容量
- 状态一致性保证(Exactly-Once),即使发生故障也不会丢失或重复
-
事件时间处理(Event Time Processing)
- 基于事件实际发生的时间(而非系统处理时间)进行计算,这对于处理网络延迟、数据乱序等现实场景至关重要
- Watermark 机制处理乱序数据------Watermark 是一种"水位线"标记,告诉系统"在这个时间点之前的数据应该都到齐了"
- 灵活的迟到数据处理策略
-
流批一体(Unified Stream & Batch)
- 同一套 API 处理流和批,开发者不需要为实时和离线分别编写两套代码
- 同一套执行引擎,减少了技术栈的复杂度
- 降低开发和维护成本,消除传统 Lambda 架构中"两套逻辑导致结果不一致"的问题
-
容错与恢复
- 基于 Chandy-Lamport 分布式快照算法的 Checkpoint 机制,能够在不停止数据处理的情况下,周期性地保存全局一致的状态快照
- Savepoint 支持版本升级和迁移------Savepoint 是手动触发的完整状态快照,可用于作业升级、集群迁移等运维场景
- 毫秒级故障恢复
1.2 应用场景详解
Flink 的应用场景非常广泛,从金融风控到电商推荐,从实时数仓到物联网监控,几乎涵盖了所有需要实时数据处理的领域。下面我们按照四大核心场景分别介绍。
1.2.1 实时数据处理(Real-time Processing)
实时数据处理是 Flink 最基础也最广泛的应用场景。它的核心诉求是:数据从产生到被处理和响应,延迟控制在毫秒到秒级别。典型的实时数据处理包括风控系统(在交易发生的瞬间判断是否存在欺诈行为)、监控告警(服务器指标异常时立即触发告警)、以及实时 ETL(数据产生后立即清洗转换写入下游系统)。
适用场景:实时风控、监控告警、实时 ETL
实时数据处理流程:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 数据源 │ → │ Flink │ → │ 实时 │ → │ 下游 │
│ Kafka │ │ 流处理 │ │ 计算 │ │ 输出 │
│ 日志 │ │ 清洗过滤 │ │ 聚合告警 │ │ ES/Redis │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
│ │ │ │
▼ ▼ ▼ ▼
事件产生 逐条处理 状态计算 实时响应
(毫秒级) (毫秒级) (毫秒级) (毫秒级)
典型案例:
| 行业 | 场景 | 延迟要求 | 吞吐量 | 状态规模 |
|---|---|---|---|---|
| 金融 | 实时反欺诈 | < 100ms | 50 万条/秒 | 100GB+ |
| 电商 | 实时风控 | < 200ms | 100 万条/秒 | 500GB+ |
| 运维 | 异常检测告警 | < 1s | 200 万条/秒 | 50GB |
| 物联网 | 设备监控 | < 500ms | 300 万条/秒 | 1TB+ |
| 安全 | 入侵检测 | < 100ms | 100 万条/秒 | 200GB |
代码示例:
java
/**
* 实时交易风控 - 检测短时间内异常高频交易
*/
public class FraudDetectionJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 从 Kafka 读取交易数据
KafkaSource<Transaction> source = KafkaSource.<Transaction>builder()
.setBootstrapServers("kafka:9092")
.setTopics("transactions")
.setGroupId("fraud-detection")
.setValueOnlyDeserializer(new TransactionDeserializer())
.build();
DataStream<Transaction> transactions = env
.fromSource(source, WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(5)),
"Kafka Source");
// 按用户分组,检测 5 分钟内交易次数超过阈值的用户
DataStream<Alert> alerts = transactions
.keyBy(Transaction::getUserId)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.aggregate(new TransactionCountAgg(), new AlertWindowFunction())
.filter(alert -> alert.getTxCount() > 10 || alert.getTotalAmount() > 100000);
// 输出告警
alerts.sinkTo(new AlertSink());
env.execute("Fraud Detection Job");
}
}
1.2.2 事件驱动应用(Event-Driven Application)
事件驱动应用是一种以事件为中心的架构模式:系统不主动轮询数据,而是被动响应外部事件并更新内部状态。Flink 天然适合这种模式,因为它的有状态流处理引擎可以在内存中维护复杂的业务状态(如用户画像、订单簿、在线会话),并在每个事件到达时实时更新状态和触发动作。与传统的请求-响应架构相比,事件驱动应用能够提供更低的延迟和更高的吞吐。
适用场景:推荐系统实时更新、交易撮合、动态定价
事件驱动架构:
┌────────────┐ ┌────────────────────────────────┐ ┌────────────┐
│ 事件源 │ → │ Flink 有状态处理 │ → │ 输出 │
│ 用户行为 │ │ ┌──────────┐ ┌──────────┐ │ │ 推荐结果 │
│ 订单事件 │ │ │ 状态存储 │ │ 业务逻辑 │ │ │ 价格更新 │
│ 传感器 │ │ │ (State) │ │ (Process) │ │ │ 触发动作 │
└────────────┘ │ └──────────┘ └──────────┘ │ └────────────┘
└────────────────────────────────┘
典型案例:
| 行业 | 场景 | 状态复杂度 | 实时性要求 |
|---|---|---|---|
| 电商 | 实时推荐更新 | 高(用户画像状态) | < 200ms |
| 金融 | 订单撮合引擎 | 非常高(订单簿状态) | < 10ms |
| 出行 | 动态定价 | 中(供需状态) | < 1s |
| 游戏 | 实时排行榜 | 中(分数状态) | < 500ms |
| 社交 | 实时消息路由 | 高(在线状态) | < 100ms |
代码示例:
java
/**
* 事件驱动 - 实时用户画像更新
*/
public class UserProfileUpdateJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<UserEvent> events = env.fromSource(/* Kafka Source */);
// 使用 KeyedProcessFunction 维护用户状态
DataStream<UserProfile> profiles = events
.keyBy(UserEvent::getUserId)
.process(new KeyedProcessFunction<String, UserEvent, UserProfile>() {
// 用户画像状态
private ValueState<UserProfile> profileState;
@Override
public void open(Configuration parameters) {
ValueStateDescriptor<UserProfile> descriptor =
new ValueStateDescriptor<>("user-profile", UserProfile.class);
profileState = getRuntimeContext().getState(descriptor);
}
@Override
public void processElement(UserEvent event, Context ctx,
Collector<UserProfile> out) throws Exception {
UserProfile profile = profileState.value();
if (profile == null) {
profile = new UserProfile(event.getUserId());
}
// 根据事件更新画像
profile.updateWith(event);
profileState.update(profile);
// 输出更新后的画像
out.collect(profile);
}
});
profiles.sinkTo(/* Redis/HBase Sink */);
env.execute("User Profile Update");
}
}
1.2.3 数据分析与 ETL(Analytics & ETL)
ETL(Extract-Transform-Load)是数据仓库建设的核心环节,传统 ETL 通常以 T+1 的频率运行(即每天凌晨跑批处理昨天的数据)。而 Flink 可以实现实时 ETL:数据在源系统产生变更的瞬间,就被 Flink 实时捕获(通过 CDC 技术)、清洗转换、关联维度表,最终写入数据仓库或数据湖。这将数据的新鲜度从"天级"提升到"秒级",极大地加速了业务决策。
适用场景:实时数仓、实时报表、CDC 数据同步
实时 ETL 架构:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 业务 DB │ → │ Flink CDC │ → │ Flink SQL │ → │ 数据湖 │
│ MySQL │ │ Binlog │ │ 实时 ETL │ │ Iceberg │
│ PG │ │ 捕获 │ │ 清洗关联 │ │ Hudi │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
│ │ │ │
▼ ▼ ▼ ▼
数据变更 实时捕获 流式加工 持久化存储
(Insert/ (秒级) (SQL 处理) (分钟级可查)
Update/Delete)
典型案例:
| 行业 | 场景 | 数据规模 | 处理频率 |
|---|---|---|---|
| 电商 | 实时销售看板 | 100 万条/分钟 | 秒级 |
| 金融 | 实时对账 | 50 万条/分钟 | 秒级 |
| 物流 | 实时轨迹汇总 | 200 万条/分钟 | 秒级 |
| 广告 | 实时投放效果 | 500 万条/分钟 | 秒级 |
| 零售 | 库存实时同步 | 10 万条/分钟 | 秒级 |
代码示例:
sql
-- Flink SQL 实时 ETL:MySQL CDC → 实时宽表 → Kafka
-- 创建 CDC 源表
CREATE TABLE orders (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
amount DECIMAL(10, 2),
order_time TIMESTAMP(3),
status STRING,
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'mysql-host',
'port' = '3306',
'username' = 'flink',
'password' = '***',
'database-name' = 'ecommerce',
'table-name' = 'orders'
);
-- 创建用户维表
CREATE TABLE users (
user_id BIGINT,
user_name STRING,
user_level STRING,
city STRING,
PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://mysql-host:3306/ecommerce',
'table-name' = 'users'
);
-- 实时关联生成宽表
INSERT INTO dwd_order_detail
SELECT
o.order_id,
o.user_id,
u.user_name,
u.user_level,
u.city,
o.product_id,
o.amount,
o.order_time,
o.status
FROM orders o
LEFT JOIN users FOR SYSTEM_TIME AS OF o.order_time AS u
ON o.user_id = u.user_id;
1.2.4 流批一体(Unified Stream & Batch)
在 Flink 出现之前,企业通常采用 Lambda 架构来同时满足实时和离线的数据需求:用 Storm 处理实时流,用 Spark/Hive 处理离线批,再将两者的结果合并。这种架构最大的痛点是需要维护两套完全不同的代码和技术栈,而且两套系统的计算结果经常对不上。
Flink 提出的流批一体理念,催生了更优雅的 Kappa 架构 :用一套 Flink 代码同时处理实时和历史数据,通过 RuntimeExecutionMode 切换流/批模式,从根本上消除了双系统的一致性问题。
适用场景:Lambda 架构向 Kappa 架构演进、统一数据处理
架构演进:
Lambda 架构(传统):
┌────────────┐ ┌────────────────┐ ┌────────────┐
│ 数据源 │ → │ 批处理层 │ → │ │
│ │ │ (Spark) │ │ 服务层 │
│ │ → │ 流处理层 │ → │ (合并结果) │
│ │ │ (Storm) │ │ │
└────────────┘ └────────────────┘ └────────────┘
问题:两套代码、两套逻辑、结果不一致
▼
Kappa 架构(Flink):
┌────────────┐ ┌────────────────┐ ┌────────────┐
│ 数据源 │ → │ Flink │ → │ 服务层 │
│ │ │ 流批一体处理 │ │ (统一结果) │
└────────────┘ └────────────────┘ └────────────┘
优势:一套代码、一套逻辑、结果一致
代码示例:
java
/**
* 流批一体示例 - 同一份代码处理流和批
*/
public class UnifiedJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<Order> orders;
if (args[0].equals("streaming")) {
// 流模式:从 Kafka 读取
env.setRuntimeMode(RuntimeExecutionMode.STREAMING);
orders = env.fromSource(kafkaSource, watermarkStrategy, "Kafka");
} else {
// 批模式:从文件读取
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
orders = env.fromSource(fileSource, watermarkStrategy, "File");
}
// 同一份业务逻辑
DataStream<SalesReport> report = orders
.keyBy(Order::getCategory)
.window(TumblingEventTimeWindows.of(Time.hours(1)))
.aggregate(new SalesAggregator());
report.sinkTo(/* Sink */);
env.execute("Unified Stream & Batch Job");
}
}
1.3 生态系统
Flink 不仅仅是一个计算引擎,它已经发展成为一个完整的实时计算生态系统。围绕 Flink Core(DataStream API)这个核心,社区和商业公司构建了丰富的上下游组件:Flink SQL 降低了使用门槛;Flink CDC 让实时数据接入变得简单;Flink ML 提供了流式机器学习能力;CEP 库支持复杂事件模式匹配;PyFlink 让 Python 开发者也能参与流计算;而数十种 Connectors 则打通了与 Kafka、Elasticsearch、JDBC、数据湖等外部系统的连接。
┌───────────────────────────────────────────────────────────────────────┐
│ Flink 生态系统 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ ┌───────────┐ │
│ │ Flink Core │ │ Flink SQL │ │ Flink CDC │ │ Flink ML │ │
│ │ (DataStream)│ │ (Table API) │ │ (变更数据捕获) │ │ (机器学习) │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ └───────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │
│ │ Flink CEP │ │ Stateful │ │ PyFlink │ │ Flink on │ │
│ │ (复杂事件) │ │ Functions │ │ (Python) │ │ Kubernetes │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └───────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Connectors(连接器) │ │
│ │ Kafka │ Pulsar │ JDBC │ Elasticsearch │ HBase │ Redis │ Iceberg│ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ 部署方式 │ │
│ │ Standalone │ YARN │ Kubernetes │ Docker │ Cloud Native │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
二、架构设计
Flink 采用经典的 Master-Worker 架构(主从架构)。Master 节点(JobManager)负责作业的调度、协调和容错,Worker 节点(TaskManager)负责实际的数据处理和状态存储。这种架构设计使 Flink 能够高效地管理分布式资源、协调分布式快照、并在节点故障时快速恢复。
2.1 整体架构图
下图展示了 Flink 集群的三层结构:Client 负责编译用户程序并提交作业;JobManager 是集群的"大脑",负责作业调度和资源协调;TaskManager 是集群的"双手",负责实际执行计算任务。
┌─────────────────────────────────────────────────────────────────────────┐
│ Client Stage │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Flink Client │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 用户程序 │ │ JobGraph │ │ 提交作业 │ │ │
│ │ │ main() │ │ 生成 │ │ to Cluster │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ JobManager Stage │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Dispatcher │ │ JobMaster │ │ ResourceManager│ │
│ │ (接收作业) │ │ (作业调度) │ │ (资源管理) │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
│ │ │ │
│ ┌──────┴──────┐ │ │
│ │ Checkpoint │ │ │
│ │ Coordinator │ │ │
│ └─────────────┘ │ │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ TaskManager Stage │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ TaskManager 1 │ │ TaskManager 2 │ │ TaskManager N │ │
│ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │
│ │ │ Task Slot │ │ │ │ Task Slot │ │ │ │ Task Slot │ │ │
│ │ │ ┌───────┐ │ │ │ │ ┌───────┐ │ │ │ │ ┌───────┐ │ │ │
│ │ │ │ Task │ │ │ │ │ │ Task │ │ │ │ │ │ Task │ │ │ │
│ │ │ └───────┘ │ │ │ │ └───────┘ │ │ │ │ └───────┘ │ │ │
│ │ │ ┌───────┐ │ │ │ │ ┌───────┐ │ │ │ │ ┌───────┐ │ │ │
│ │ │ │ State │ │ │ │ │ │ State │ │ │ │ │ │ State │ │ │ │
│ │ │ └───────┘ │ │ │ │ └───────┘ │ │ │ │ └───────┘ │ │ │
│ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
2.2 主要角色
Flink 集群中的每个组件都有明确的职责分工。理解这些角色是掌握 Flink 架构的基础:
| 角色 | 职责 | 说明 |
|---|---|---|
| Client | 作业构建与提交 | 将用户程序编译为 JobGraph,提交给集群 |
| Dispatcher | 作业接收与分发 | 接收 JobGraph,为每个作业启动 JobMaster |
| JobMaster | 单个作业的调度 | 将 JobGraph 转为 ExecutionGraph,调度 Task |
| ResourceManager | 资源管理 | 管理 TaskManager 的 Slot 资源分配 |
| TaskManager | 任务执行进程 | 提供 Slot 执行 Task,管理本地状态 |
| Task Slot | 资源隔离单元 | TaskManager 中的固定资源子集(内存隔离) |
| Checkpoint Coordinator | 容错协调 | 触发和协调分布式 Checkpoint |
2.3 作业提交与执行流程
当我们编写好 Flink 程序并提交到集群时,它会经历一系列精心设计的步骤:从 Client 端编译程序生成 JobGraph,到 Dispatcher 接收并分发作业,再到 JobMaster 向 ResourceManager 申请资源,最后将 Task 部署到 TaskManager 的 Slot 中执行。整个流程如下图所示:
┌─────────────────────────────────────────────────────────────────────────┐
│ Flink 作业提交流程 │
│ │
│ 1. 构建 JobGraph │
│ ┌─────────────┐ │
│ │ Client │ ← 用户编写 Flink 程序 │
│ │ main() │ ← 构建 DataStream/SQL 逻辑 │
│ │ │ ← 编译为 JobGraph │
│ └─────────────┘ │
│ │ │
│ 2. 提交作业 │
│ ▼ │
│ ┌─────────────┐ │
│ │ Dispatcher │ ← 接收 JobGraph │
│ │ │ ← 启动 JobMaster │
│ └─────────────┘ │
│ │ │
│ 3. 资源申请 │
│ ▼ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ JobMaster │ ──────→ │ ResourceManager │ │
│ │ │ │ 申请 Slot 资源 │ │
│ └─────────────┘ └──────────────────┘ │
│ │ │ │
│ 4. 分配 Slot ▼ │
│ │ TaskManager 注册 Slot │
│ │ ResourceManager 分配 Slot 给 JobMaster │
│ │ │
│ 5. 部署 Task │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ JobMaster │ │
│ │ ↓ JobGraph → ExecutionGraph │ │
│ │ ↓ 将 Task 部署到对应的 TaskManager Slot │ │
│ │ ↓ 建立 Task 之间的数据通道 │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ 6. 开始处理 │
│ TaskManager ──→ 执行 Task ──→ 数据流转 ──→ 输出结果 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.4 核心组件交互:图的转换
Flink 的一大设计亮点是它的多层图转换机制。用户编写的程序不会直接执行,而是经过四次图的转换,每一步都进行特定的优化。这类似于编译器将高级语言逐步翻译为机器码的过程------StreamGraph 是"源代码",JobGraph 经过算子链化等优化,ExecutionGraph 按并行度展开为可调度的执行单元,最终 Physical Graph 是实际运行在集群中的物理实例。
┌─────────────────────────────────────────────────────────────────────────┐
│ Flink 作业图转换过程 │
│ │
│ 1. StreamGraph(逻辑图) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 用户代码中的算子关系 │ │
│ │ Source → Map → KeyBy → Window → Sink │ │
│ │ 保留所有算子细节,未做优化 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ Client 端 │
│ ▼ │
│ 2. JobGraph(优化图) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 算子链化(Operator Chaining) │ │
│ │ Source → [Map] → KeyBy → [Window → Sink] │ │
│ │ 可链化的算子合并为一个 JobVertex │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ 提交给 JobManager │
│ ▼ │
│ 3. ExecutionGraph(执行图) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 按并行度展开 │ │
│ │ Source[0] → Map[0] KeyBy → Window[0] → Sink[0] │ │
│ │ Source[1] → Map[1] KeyBy → Window[1] → Sink[1] │ │
│ │ Source[2] → Map[2] KeyBy → Window[2] → Sink[2] │ │
│ │ 每个节点是一个 ExecutionVertex │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ 调度到 TaskManager │
│ ▼ │
│ 4. Physical Graph(物理图) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 实际运行在 TaskManager Slot 中的 Task 实例 │ │
│ │ 包含 Network Buffer、State Backend、数据通道 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
三、关键概念
本章将深入介绍 Flink 最核心的几个概念。这些概念是理解和使用 Flink 的基础------DataStream API 定义了编程模型,时间语义决定了计算的正确性,窗口机制将无界流切分为有界计算,状态管理让算子拥有"记忆",Checkpoint 保证故障恢复后的一致性,连接器则打通了与外部系统的桥梁。
3.1 DataStream API
DataStream API 是 Flink 的核心编程接口,用于处理无界和有界数据流。一个 Flink 程序由三部分组成:Source (数据源)、Transformation (转换)和 Sink(输出)。
3.1.1 程序结构
java
/**
* Flink DataStream 程序基本结构
*/
public class BasicStructure {
public static void main(String[] args) throws Exception {
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 读取数据源(Source)
DataStream<String> source = env.fromSource(
KafkaSource.<String>builder()
.setBootstrapServers("kafka:9092")
.setTopics("input-topic")
.setGroupId("my-group")
.setValueOnlyDeserializer(new SimpleStringSchema())
.build(),
WatermarkStrategy.noWatermarks(),
"Kafka Source"
);
// 3. 数据转换(Transformation)
DataStream<WordCount> counts = source
.flatMap(new Tokenizer())
.keyBy(WordCount::getWord)
.sum("count");
// 4. 数据输出(Sink)
counts.sinkTo(/* Kafka/JDBC/ES Sink */);
// 5. 触发执行
env.execute("My Flink Job");
}
}
3.1.2 Transformation 算子完整列表
Transformation(转换算子)是 Flink 数据处理的核心。每个算子接收一个或多个 DataStream 作为输入,经过处理后输出新的 DataStream。下表列出了 Flink 提供的所有常用算子,其中"是否触发 Shuffle"这一列很重要------Shuffle 意味着数据需要跨网络重新分区,是性能优化时的关键关注点。
| 类别 | 算子名称 | 方法签名 | 作用说明 | 是否触发 Shuffle |
|---|---|---|---|---|
| 基础转换 | map | map(MapFunction) |
一对一转换,每个元素产生一个输出 | 否 |
| flatMap | flatMap(FlatMapFunction) |
一对多转换,每个元素可产生 0 到多个输出 | 否 | |
| filter | filter(FilterFunction) |
过滤满足条件的元素 | 否 | |
| 分区操作 | keyBy | keyBy(KeySelector) |
按指定 Key 逻辑分区(类似 Spark 的 groupBy) | 是(Hash) |
| partitionCustom | partitionCustom(Partitioner, KeySelector) |
自定义分区策略 | 是 | |
| shuffle | shuffle() |
随机均匀分区 | 是 | |
| rebalance | rebalance() |
轮询方式均匀分区 | 是 | |
| rescale | rescale() |
局部轮询(上下游对应) | 是(局部) | |
| broadcast | broadcast() |
广播到所有下游分区 | 是 | |
| 聚合操作 | reduce | reduce(ReduceFunction) |
滚动聚合,将当前元素与上一次结果合并 | 否(KeyedStream) |
| sum/min/max | sum("field") |
按指定字段滚动求和/最小/最大 | 否(KeyedStream) | |
| minBy/maxBy | minBy("field") |
返回指定字段最小/最大值的整条记录 | 否(KeyedStream) | |
| 窗口操作 | window | window(WindowAssigner) |
定义窗口类型(滚动/滑动/会话) | 否 |
| windowAll | windowAll(WindowAssigner) |
全局窗口(非 KeyedStream) | 否 | |
| 多流操作 | union | union(DataStream...) |
合并多条同类型的流 | 否 |
| connect | connect(DataStream) |
连接两条不同类型的流 | 否 | |
| coMap/coFlatMap | map1()/map2() |
对 ConnectedStream 的两条流分别处理 | 否 | |
| join | join(DataStream) |
窗口内双流 Join(Inner Join) | 是 | |
| coGroup | coGroup(DataStream) |
窗口内双流分组(更灵活的 Join) | 是 | |
| 侧输出 | process | process(ProcessFunction) |
最灵活的算子,支持侧输出、定时器、状态 | 否 |
| getSideOutput | getSideOutput(OutputTag) |
获取侧输出流 | 否 | |
| 其他 | assignTimestampsAndWatermarks | assignTimestampsAndWatermarks(WatermarkStrategy) |
分配时间戳和 Watermark | 否 |
| iterate | iterate() |
创建迭代数据流 | 否 | |
| name | name("name") |
设置算子名称(用于 Web UI) | 否 | |
| setParallelism | setParallelism(n) |
设置算子并行度 | 否 |
3.1.3 算子链化(Operator Chaining)
Flink 会将满足条件的连续算子合并为一个 Operator Chain,在同一个线程中执行,减少序列化和网络开销。
┌─────────────────────────────────────────────────────────────────────────┐
│ 算子链化示意图 │
│ │
│ 链化前: │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Source │→ │ Map │→ │ Filter │→ │ KeyBy │→ │ Sink │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │
│ 线程1 线程2 线程3 线程4 线程5 │
│ │
│ 链化后: │
│ ┌──────────────────────────┐ ┌──────────────────────┐ │
│ │ Source → Map → Filter │→ │ KeyBy → Sink │ │
│ │ (一个 Task) │ │ (一个 Task) │ │
│ └──────────────────────────┘ └──────────────────────┘ │
│ 线程1 线程2 │
│ │
│ 链化条件: │
│ • 上下游并行度相同 │
│ • 上下游是 Forward 连接(非 Shuffle) │
│ • 算子未禁用链化 │
│ • 在同一个 Slot Sharing Group │
└─────────────────────────────────────────────────────────────────────────┘
java
// 控制链化行为
stream
.map(new MyMapper()).name("mapper")
.filter(new MyFilter()).disableChaining() // 禁止与上游链化
.keyBy(...)
.process(new MyProcess()).startNewChain(); // 从此处开始新链
3.2 时间语义
在流处理中,"时间"是一个比想象中更复杂的概念。同一条数据至少涉及三个不同的时间点:它在业务系统中产生的时间(事件时间)、它被 Flink Source 读取的时间(摄入时间)、以及它被某个算子处理的时间(处理时间)。选择哪种时间语义直接决定了计算结果的正确性和确定性。在生产环境中,强烈推荐使用 Event Time,因为只有它能保证在数据乱序、延迟到达、甚至作业重启重放的情况下,都能得到一致且正确的计算结果。
时间是 Flink 流处理的核心概念。Flink 支持三种时间语义:
┌─────────────────────────────────────────────────────────────────────────┐
│ 三种时间语义 │
│ │
│ ① Event Time(事件时间) │
│ 事件实际发生的时间,嵌入在数据中 │
│ ┌─────┐ │
│ │ 事件 │ → timestamp: 10:00:01 │
│ └─────┘ (数据自带的时间戳) │
│ │
│ ② Processing Time(处理时间) │
│ Flink 算子处理该事件时的系统时钟时间 │
│ ┌─────┐ │
│ │ 事件 │ → 到达算子时:10:00:05 │
│ └─────┘ (机器墙上时钟) │
│ │
│ ③ Ingestion Time(摄入时间) │
│ 事件进入 Flink Source 的时间 │
│ ┌─────┐ │
│ │ 事件 │ → 进入 Source 时:10:00:03 │
│ └─────┘ (Source 算子分配) │
│ │
│ 推荐:优先使用 Event Time,结果不受处理延迟影响,可重放 │
└─────────────────────────────────────────────────────────────────────────┘
| 时间语义 | 确定性 | 延迟容忍 | 乱序处理 | 适用场景 |
|---|---|---|---|---|
| Event Time | 确定 | 高 | 支持(Watermark) | 需要精确结果的业务分析 |
| Processing Time | 不确定 | 低 | 不支持 | 对延迟敏感、容忍不精确 |
| Ingestion Time | 部分确定 | 中 | 不支持 | 折中方案(已不推荐) |
3.2.1 Watermark 机制详解
Watermark(水位线)是 Flink 处理乱序数据的核心机制。在现实世界中,数据几乎不可能完全按照事件时间顺序到达------网络延迟、分区不均、重试等都会导致数据乱序。Watermark 本质上是一个时间戳标记,它随数据流一起流动,表示"小于该时间戳的数据大概率已全部到达"。当某个窗口收到的 Watermark 超过了窗口的结束时间,就意味着这个窗口的数据已经收集完毕,可以触发计算了。
┌─────────────────────────────────────────────────────────────────────────┐
│ Watermark 工作原理 │
│ │
│ 数据流(按到达顺序): │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │t=1│ │t=3│ │t=2│ │t=5│ │t=4│ │t=7│ │t=6│ │t=8│ → 时间轴 │
│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │
│ │
│ Watermark 生成(允许 2 秒乱序): │
│ max_timestamp = 当前最大事件时间 │
│ watermark = max_timestamp - 允许延迟(2s) │
│ │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │t=1│ │t=3│ │t=2│ │t=5│ │t=4│ │t=7│ │t=6│ │t=8│ │
│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │
│ W=-1 W=1 W=1 W=3 W=3 W=5 W=5 W=6 │
│ │
│ 当 Watermark 到达窗口结束时间,窗口触发计算: │
│ 窗口 [0, 5) 在 W=5 时触发 → 包含 t=1,2,3,4 的数据 │
│ 窗口 [5, 10) 在 W=10 时触发 → 包含 t=5,6,7,8 的数据 │
└─────────────────────────────────────────────────────────────────────────┘
java
/**
* Watermark 策略配置
*/
// 方式 1:有界乱序(最常用)
WatermarkStrategy<Event> strategy = WatermarkStrategy
.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5)) // 允许 5 秒乱序
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
// 方式 2:单调递增(数据严格有序)
WatermarkStrategy<Event> monotonous = WatermarkStrategy
.<Event>forMonotonousTimestamps()
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
// 方式 3:自定义 Watermark 生成器
WatermarkStrategy<Event> custom = WatermarkStrategy
.<Event>forGenerator(ctx -> new WatermarkGenerator<Event>() {
private long maxTimestamp = Long.MIN_VALUE;
@Override
public void onEvent(Event event, long eventTimestamp, WatermarkOutput output) {
maxTimestamp = Math.max(maxTimestamp, event.getTimestamp());
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 周期性发射 Watermark,允许 5 秒延迟
output.emitWatermark(new Watermark(maxTimestamp - 5000));
}
})
.withTimestampAssigner((event, timestamp) -> event.getTimestamp());
// 应用 Watermark 策略
DataStream<Event> stream = env
.fromSource(source, strategy, "Kafka Source");
3.2.2 迟到数据处理
即使有了 Watermark 机制,仍然可能有些数据到达得太晚------晚于 Watermark 允许的延迟范围。对于这些"迟到数据",Flink 提供了三种处理策略:直接丢弃(默认行为)、允许窗口在触发后继续保留一段时间等待迟到数据(allowedLateness)、或者将迟到数据输出到侧流进行单独处理(Side Output)。在生产环境中,推荐组合使用第二和第三种策略,既保证主流结果的实时性,又不丢失任何数据。
┌─────────────────────────────────────────────────────────────────────────┐
│ 迟到数据处理策略 │
│ │
│ 策略 1:丢弃(默认) │
│ Watermark 已过窗口结束时间 → 迟到数据直接丢弃 │
│ │
│ 策略 2:允许延迟(allowedLateness) │
│ 窗口触发后不立即销毁,保留一段时间等待迟到数据 │
│ 迟到数据到达后,窗口重新触发计算 │
│ │
│ 策略 3:侧输出(Side Output) │
│ 将迟到数据输出到侧流,单独处理 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 主流(正常数据) → 窗口计算 → 正常输出 │ │
│ │ │ │ │
│ │ ↓ (迟到数据) │ │
│ │ 侧输出流 → 单独处理 → 修正/补偿 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
java
/**
* 迟到数据处理 - 三种策略
*/
// 定义迟到数据侧输出标签
final OutputTag<Event> lateTag = new OutputTag<Event>("late-data") {};
DataStream<Result> result = stream
.keyBy(Event::getKey)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
// 策略 2:允许额外 1 分钟延迟
.allowedLateness(Time.minutes(1))
// 策略 3:超过延迟的数据输出到侧流
.sideOutputLateData(lateTag)
.aggregate(new MyAggregateFunction());
// 获取迟到数据侧流
DataStream<Event> lateStream = result.getSideOutput(lateTag);
// 对迟到数据做补偿处理
lateStream.addSink(new LateDateCompensationSink());
3.3 窗口机制
在流处理中,数据是无界的------它永远不会"结束"。但很多计算(如"每 5 分钟的订单总额")需要对一段有限的数据进行聚合。**窗口(Window)**就是解决这个问题的核心机制:它将无界的数据流按照时间或数量切分为一个个有界的数据块(窗口),然后对每个窗口内的数据进行计算。Flink 提供了四种内置窗口类型,覆盖了绝大多数业务场景。
窗口是流处理中将无界数据切分为有界数据块的核心机制。
┌─────────────────────────────────────────────────────────────────────────┐
│ 四种窗口类型 │
│ │
│ ① 滚动窗口(Tumbling Window) │
│ 固定大小,窗口之间无重叠 │
│ ┌─────────┐┌─────────┐┌─────────┐┌─────────┐ │
│ │ Window 1││ Window 2││ Window 3││ Window 4│ │
│ │ [0, 5) ││ [5, 10) ││[10, 15) ││[15, 20) │ │
│ └─────────┘└─────────┘└─────────┘└─────────┘ │
│ ────────────────────────────────────────────→ 时间 │
│ │
│ ② 滑动窗口(Sliding Window) │
│ 固定大小,窗口之间有重叠 │
│ ┌─────────────┐ │
│ │ Window 1 │ │
│ │ [0, 10) │ │
│ └───┬─────────┘ │
│ │┌─────────────┐ │
│ ││ Window 2 │ │
│ ││ [5, 15) │ │
│ │└───┬─────────┘ │
│ │ │┌─────────────┐ │
│ │ ││ Window 3 │ │
│ │ ││ [10, 20) │ │
│ ────────────────────────────────────────────→ 时间 │
│ │
│ ③ 会话窗口(Session Window) │
│ 按活动间隔动态划分,无固定大小 │
│ ┌───────┐ ┌─────────────┐ ┌────┐ │
│ │ Ses 1 │ │ Ses 2 │ │Ses3│ │
│ │ │ │ │ │ │ │
│ └───────┘ └─────────────┘ └────┘ │
│ ──●●●───gap───●●●●●●●───gap───●●───────→ 时间 │
│ (超过gap │
│ 开新窗口) │
│ │
│ ④ 全局窗口(Global Window) │
│ 所有数据归到一个窗口,需自定义触发器 │
│ ┌──────────────────────────────────────────┐ │
│ │ Global Window │ │
│ │ (需要自定义 Trigger 触发计算) │ │
│ └──────────────────────────────────────────┘ │
│ ────────────────────────────────────────────→ 时间 │
└─────────────────────────────────────────────────────────────────────────┘
java
/**
* 窗口类型使用示例
*/
KeyedStream<Event, String> keyed = stream.keyBy(Event::getKey);
// 1. 滚动窗口:每 5 分钟统计一次
keyed.window(TumblingEventTimeWindows.of(Time.minutes(5)));
// 2. 滑动窗口:窗口大小 10 分钟,每 5 分钟滑动一次
keyed.window(SlidingEventTimeWindows.of(Time.minutes(10), Time.minutes(5)));
// 3. 会话窗口:超过 30 分钟无活动则关闭窗口
keyed.window(EventTimeSessionWindows.withGap(Time.minutes(30)));
// 4. 全局窗口 + 自定义触发器
keyed.window(GlobalWindows.create())
.trigger(CountTrigger.of(1000)); // 每 1000 条触发一次
Window Function 类型
窗口函数定义了当窗口触发时,如何对窗口内的数据进行计算。Flink 提供了三种基本的 Window Function,以及一种组合模式。其中最重要的性能差异在于:ReduceFunction 和 AggregateFunction 是增量计算 的------每来一条数据就更新一次中间结果,内存占用极小;而 ProcessWindowFunction 需要缓存整个窗口的数据 ,在窗口触发时一次性处理,内存开销大。生产中推荐使用 AggregateFunction + ProcessWindowFunction 的组合模式,兼顾性能和灵活性。
| 函数类型 | 说明 | 增量计算 | 访问窗口信息 | 适用场景 |
|---|---|---|---|---|
| ReduceFunction | 两两聚合,输入输出类型相同 | ✅ | ❌ | 简单聚合(求和、最大值) |
| AggregateFunction | 自定义聚合,输入输出类型可不同 | ✅ | ❌ | 复杂聚合(平均值、TopN) |
| ProcessWindowFunction | 全量遍历窗口数据 | ❌ | ✅ | 需要窗口元信息或全量排序 |
| Aggregate + Process | 增量聚合 + 窗口信息 | ✅ | ✅ | 最佳实践:兼顾性能和灵活性 |
java
/**
* 最佳实践:AggregateFunction + ProcessWindowFunction
* 增量聚合(节省内存) + 获取窗口信息(灵活性)
*/
keyed.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.aggregate(
// 增量聚合:每来一条数据更新累加器
new AggregateFunction<Event, Tuple2<Long, Long>, Double>() {
@Override
public Tuple2<Long, Long> createAccumulator() {
return Tuple2.of(0L, 0L); // (sum, count)
}
@Override
public Tuple2<Long, Long> add(Event event, Tuple2<Long, Long> acc) {
return Tuple2.of(acc.f0 + event.getValue(), acc.f1 + 1);
}
@Override
public Double getResult(Tuple2<Long, Long> acc) {
return acc.f0.doubleValue() / acc.f1; // 平均值
}
@Override
public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) {
return Tuple2.of(a.f0 + b.f0, a.f1 + b.f1);
}
},
// 包装窗口信息
new ProcessWindowFunction<Double, WindowResult, String, TimeWindow>() {
@Override
public void process(String key, Context ctx,
Iterable<Double> values, Collector<WindowResult> out) {
Double avg = values.iterator().next();
TimeWindow window = ctx.window();
out.collect(new WindowResult(key, window.getStart(), window.getEnd(), avg));
}
}
);
3.4 状态管理
**状态(State)**是 Flink 区别于其他流处理引擎的核心能力,也是实现复杂业务逻辑的基础。简单来说,状态就是算子在处理数据过程中需要"记住"的信息。例如:一个计数器需要记住当前的累计值;一个去重算子需要记住已经出现过的 Key;一个窗口聚合需要记住窗口内的中间计算结果。
Flink 将状态分为两大类:Keyed State (按 Key 隔离,每个 Key 有独立的状态空间,是最常用的类型)和 Operator State(算子级别的状态,不按 Key 区分,主要用于 Source 和 Sink 等场景)。Flink 提供内置的分布式状态管理,支持 TB 级状态,并通过 Checkpoint 机制保证状态的一致性。
┌─────────────────────────────────────────────────────────────────────────┐
│ Flink 状态分类 │
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ Keyed State │ │ Operator State │ │
│ │ (按 Key 隔离的状态) │ │ (算子级别的状态) │ │
│ │ │ │ │ │
│ │ • ValueState<T> │ │ • ListState<T> │ │
│ │ 单个值 │ │ 列表(常用于 Source 偏移) │ │
│ │ │ │ │ │
│ │ • ListState<T> │ │ • BroadcastState<K, V> │ │
│ │ 列表 │ │ 广播状态 │ │
│ │ │ │ │ │
│ │ • MapState<K, V> │ │ • UnionListState<T> │ │
│ │ 键值对 │ │ 联合列表(恢复时合并) │ │
│ │ │ │ │ │
│ │ • ReducingState<T> │ └─────────────────────────────────┘ │
│ │ 自动归约 │ │
│ │ │ 使用场景: │
│ │ • AggregatingState<IN,OUT> │ • Keyed State:大多数业务场景 │
│ │ 自动聚合 │ • Operator State:Source/Sink 状态 │
│ │ │ • Broadcast State:规则/配置下发 │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
java
/**
* 状态使用示例 - 用户行为计数与去重
*/
public class UserBehaviorCounter extends KeyedProcessFunction<String, UserEvent, UserStats> {
// 各种状态类型
private ValueState<Long> countState; // 单值:访问次数
private ListState<String> visitedPages; // 列表:访问过的页面
private MapState<String, Long> pageCountMap; // Map:每页访问次数
@Override
public void open(Configuration parameters) {
// 注册状态
countState = getRuntimeContext().getState(
new ValueStateDescriptor<>("count", Long.class));
visitedPages = getRuntimeContext().getListState(
new ListStateDescriptor<>("pages", String.class));
pageCountMap = getRuntimeContext().getMapState(
new MapStateDescriptor<>("page-counts", String.class, Long.class));
}
@Override
public void processElement(UserEvent event, Context ctx,
Collector<UserStats> out) throws Exception {
// 更新计数
Long count = countState.value();
count = (count == null) ? 1L : count + 1;
countState.update(count);
// 记录访问页面
visitedPages.add(event.getPage());
// 更新页面计数
Long pageCount = pageCountMap.get(event.getPage());
pageCountMap.put(event.getPage(), pageCount == null ? 1L : pageCount + 1);
// 输出统计结果
out.collect(new UserStats(event.getUserId(), count));
}
}
状态后端对比
状态后端(State Backend)决定了状态数据的存储方式和位置。Flink 提供两种状态后端:HashMapStateBackend 将状态存储在 JVM 堆内存中,读写速度极快但容量受限于可用堆内存;EmbeddedRocksDBStateBackend 将状态存储在 RocksDB 中(本地磁盘 + 内存缓存),容量可达 TB 级,但每次读写都需要序列化/反序列化。对于生产环境,推荐使用 RocksDB 状态后端配合增量 Checkpoint,这是经过大规模验证的最佳实践。
| 特性 | HashMapStateBackend | EmbeddedRocksDBStateBackend |
|---|---|---|
| 存储位置 | JVM 堆内存 | RocksDB(本地磁盘 + 内存缓存) |
| 状态大小 | 受限于堆内存(GB 级) | 仅受限于磁盘(TB 级) |
| 读写速度 | 非常快(纳秒级) | 较慢(微秒级,需序列化) |
| 序列化 | 仅 Checkpoint 时 | 每次读写都需要 |
| Checkpoint | 全量快照 | 增量 Checkpoint(推荐) |
| 适用场景 | 状态小、追求极致性能 | 状态大、生产环境推荐 |
| GC 压力 | 大(状态在堆内) | 小(状态在堆外) |
java
/**
* 状态后端配置
*/
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 方式 1:HashMapStateBackend(开发/小状态)
env.setStateBackend(new HashMapStateBackend());
// 方式 2:EmbeddedRocksDBStateBackend(生产推荐)
env.setStateBackend(new EmbeddedRocksDBStateBackend(true)); // true = 增量 Checkpoint
// 也可在 flink-conf.yaml 中全局配置
// state.backend: rocksdb
// state.backend.rocksdb.localdir: /data/flink/rocksdb
// state.backend.incremental: true
3.5 Checkpoint 与容错
在分布式系统中,节点故障是不可避免的。Flink 的容错机制基于 Checkpoint ------一种周期性自动触发的分布式状态一致性快照。Checkpoint 的理论基础是 Chandy-Lamport 分布式快照算法 :通过在数据流中注入特殊的标记(称为 Barrier),将数据流切分为一个个"纪元",每个纪元结束时所有算子的状态组合在一起就构成了一个全局一致的快照。当故障发生时,Flink 可以从最近一次成功的 Checkpoint 恢复,将所有算子的状态回滚到快照时刻,然后从数据源重放快照之后的数据,从而实现 Exactly-Once 语义------即每条数据恰好被处理一次,既不丢失也不重复。
Checkpoint 是 Flink 实现 Exactly-Once 语义的核心机制,基于 Chandy-Lamport 分布式快照算法。
3.5.1 Checkpoint 原理
┌─────────────────────────────────────────────────────────────────────────┐
│ Checkpoint Barrier 对齐机制 │
│ │
│ Step 1:Checkpoint Coordinator 向所有 Source 注入 Barrier │
│ │
│ Source 1: ──●──●──║──●──●──●──→ ║ = Checkpoint Barrier │
│ Source 2: ──●──●──●──║──●──●──→ ● = 普通数据 │
│ │
│ Step 2:算子收到 Barrier 后,对齐 + 快照状态 │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Operator(双输入算子) │ │
│ │ │ │
│ │ Input 1: ──●──●──║........→ (Barrier 已到,缓冲后续数据)│ │
│ │ Input 2: ──●──●──●──●──║──→ (Barrier 未到,继续处理) │ │
│ │ │ │
│ │ 当两个输入的 Barrier 都到齐: │ │
│ │ 1. 保存当前算子状态快照 │ │
│ │ 2. 向下游发送 Barrier │ │
│ │ 3. 继续处理缓冲的数据 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Step 3:所有 Sink 确认 Barrier → Checkpoint 完成 │
│ │
│ Source → Operator → Sink │
│ ║ ║ ║ │
│ ↓ ↓ ↓ │
│ State① State② State③ → 持久化到分布式存储(HDFS/S3) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Exactly-Once vs At-Least-Once
Flink 支持两种语义保证级别。Exactly-Once (精确一次)通过 Barrier 对齐来保证每条数据恰好被处理一次,但在等待对齐期间可能增大延迟;At-Least-Once(至少一次)不需要对齐 Barrier,延迟更低,但在故障恢复时可能有少量数据被重复处理。选择哪种语义取决于业务场景:金融、计费等对数据准确性要求极高的场景选择 Exactly-Once;日志分析、监控等容忍少量重复的场景可以选择 At-Least-Once 以获得更低延迟。
| 特性 | Exactly-Once(对齐) | At-Least-Once(非对齐) |
|---|---|---|
| Barrier 处理 | 对齐:等所有输入 Barrier 到齐 | 不对齐:Barrier 到即快照 |
| 数据准确性 | 精确一次 | 至少一次(可能重复) |
| 延迟影响 | Barrier 对齐期间可能增大延迟 | 延迟更低 |
| 适用场景 | 金融、计费等精确性要求高的场景 | 容忍少量重复的场景 |
Checkpoint vs Savepoint
Checkpoint 和 Savepoint 都是 Flink 的状态快照机制,但它们的定位截然不同。Checkpoint 由 Flink 自动周期触发,专用于故障恢复,格式与状态后端绑定,支持增量快照(RocksDB 后端)。Savepoint 则是由用户手动触发的完整状态快照,采用标准化格式(跨状态后端兼容),主要用于计划性的运维操作:如作业版本升级、集群迁移、并行度调整(扩缩容)等。简单记忆:Checkpoint 是"自动保险",Savepoint 是"手动存档"。
| 特性 | Checkpoint | Savepoint |
|---|---|---|
| 触发方式 | 自动周期触发 | 手动触发 |
| 用途 | 故障恢复 | 版本升级、迁移、扩缩容 |
| 生命周期 | 作业取消后可自动清理 | 手动管理,不自动删除 |
| 格式 | 状态后端特定格式 | 标准化格式(跨后端兼容) |
| 增量 | 支持(RocksDB) | 始终全量 |
java
/**
* Checkpoint 配置
*/
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 启用 Checkpoint,间隔 1 分钟
env.enableCheckpointing(60000);
// Checkpoint 配置
CheckpointConfig config = env.getCheckpointConfig();
// Exactly-Once 语义
config.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// Checkpoint 之间最小间隔 30 秒
config.setMinPauseBetweenCheckpoints(30000);
// Checkpoint 超时时间 10 分钟
config.setCheckpointTimeout(600000);
// 同时进行的 Checkpoint 数量
config.setMaxConcurrentCheckpoints(1);
// 作业取消后保留 Checkpoint
config.setExternalizedCheckpointRetention(
CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
// Checkpoint 存储路径
config.setCheckpointStorage("hdfs://namenode:8020/flink/checkpoints");
// 容忍的 Checkpoint 失败次数
config.setTolerableCheckpointFailureNumber(3);
// 非对齐 Checkpoint(降低延迟)
config.enableUnalignedCheckpoints();
3.6 连接器(Connectors)
连接器(Connectors)是 Flink 与外部系统交互的桥梁。一个完整的 Flink 作业通常需要从某个数据源读取数据(Source Connector),经过处理后将结果写入另一个系统(Sink Connector)。Flink 社区和生态维护了丰富的连接器,覆盖了消息队列(Kafka、Pulsar)、关系型数据库(JDBC)、搜索引擎(Elasticsearch)、数据湖(Iceberg、Hudi)、缓存(Redis)等主流系统。表中的"Exactly-Once"列标注了各连接器作为 Sink 时是否支持精确一次语义------这对数据准确性至关重要。
Flink 提供丰富的连接器生态,支持主流数据系统。
| 连接器 | Source | Sink | Exactly-Once | 说明 |
|---|---|---|---|---|
| Kafka | ✅ | ✅ | ✅(两阶段提交) | 最常用,流处理标配 |
| Pulsar | ✅ | ✅ | ✅ | 云原生消息队列 |
| JDBC | ✅ | ✅ | ✅(幂等写入) | 关系型数据库 |
| Elasticsearch | ❌ | ✅ | ❌(At-Least-Once) | 搜索引擎 |
| HBase | ✅ | ✅ | ❌ | 列式存储 |
| Redis | ✅ | ✅ | ❌ | 缓存 |
| FileSystem | ✅ | ✅ | ✅(Exactly-Once) | HDFS/S3/本地文件 |
| Iceberg | ✅ | ✅ | ✅ | 数据湖 |
| Hudi | ✅ | ✅ | ✅ | 数据湖 |
| MySQL CDC | ✅ | - | ✅ | 变更数据捕获 |
| MongoDB CDC | ✅ | ✅ | ✅ | 文档数据库 CDC |
java
/**
* Kafka Source + Sink 完整示例
*/
// Kafka Source
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers("kafka:9092")
.setTopics("input-topic")
.setGroupId("flink-group")
.setStartingOffsets(OffsetsInitializer.committedOffsets(OffsetResetStrategy.LATEST))
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStream<String> stream = env.fromSource(
source,
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(5)),
"Kafka Source"
);
// Kafka Sink(Exactly-Once)
KafkaSink<String> sink = KafkaSink.<String>builder()
.setBootstrapServers("kafka:9092")
.setRecordSerializer(
KafkaRecordSerializationSchema.builder()
.setTopic("output-topic")
.setValueSerializationSchema(new SimpleStringSchema())
.build()
)
.setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
.setTransactionalIdPrefix("flink-tx")
.build();
stream.sinkTo(sink);
四、Flink SQL 与 Table API
对于很多数据分析师和 SQL 开发者来说,编写 Java/Scala 代码来处理流数据门槛较高。Flink SQL 和 Table API 正是为了解决这个问题而生的------它们提供了声明式的编程体验,让你只需要描述"想要什么结果",而不用关心"如何实现"。底层,Flink SQL 借助 Apache Calcite 进行 SQL 解析和查询优化,最终将 SQL 语句编译为高效的 DataStream 程序执行。更重要的是,Flink SQL 天然支持流处理,你可以用标准 SQL 语法处理实时数据流,这在传统数据库中是不可能的。
4.1 Table API 基础
Flink SQL / Table API 是 Flink 的高级声明式 API,提供了类似 SQL 的编程体验,底层由 Calcite(Apache 开源的 SQL 解析和优化框架)进行查询优化。Table API 使用链式方法调用的风格,而 Flink SQL 则直接使用标准 SQL 语法,两者可以混合使用,底层共享同一套优化和执行引擎。
┌─────────────────────────────────────────────────────────────────────────┐
│ Flink SQL 架构 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 用户 SQL / Table API │ │
│ └──────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Calcite 解析 & 优化 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ SQL 解析 │→ │ 逻辑计划 │→ │ 规则优化 │→ │ 物理执行计划 │ │ │
│ │ │ (Parser) │ │(Logical) │ │(Optimize)│ │ (Physical) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └──────────────────────────┬──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ DataStream API 执行 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
java
/**
* Table API 基础用法
*/
import org.apache.flink.table.api.*;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
// 方式 1:使用 Table API
Table orders = tableEnv.from("orders");
Table result = orders
.filter($("status").isEqual("COMPLETED"))
.groupBy($("user_id"))
.select(
$("user_id"),
$("amount").sum().as("total_amount"),
$("order_id").count().as("order_count")
);
// 方式 2:使用 SQL
Table sqlResult = tableEnv.sqlQuery(
"SELECT user_id, SUM(amount) AS total_amount, COUNT(*) AS order_count " +
"FROM orders " +
"WHERE status = 'COMPLETED' " +
"GROUP BY user_id"
);
// 转回 DataStream
DataStream<Row> stream = tableEnv.toDataStream(result);
4.2 Flink SQL 语法
Flink SQL 遵循 ANSI SQL 标准,并在此基础上扩展了流处理特有的语法(如 Watermark 定义、窗口 TVF、时态 Join 等)。主要分为 DDL(数据定义语言,用于创建/修改表)和 DML(数据操作语言,用于查询和写入)。以下通过一个完整的例子展示 Flink SQL 的核心能力。
DDL:表的创建与管理
sql
-- 创建 Kafka 源表
CREATE TABLE page_views (
user_id BIGINT,
page_url STRING,
view_time TIMESTAMP(3),
-- 定义 Watermark
WATERMARK FOR view_time AS view_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'page_views',
'properties.bootstrap.servers' = 'kafka:9092',
'properties.group.id' = 'flink-sql-group',
'format' = 'json',
'scan.startup.mode' = 'latest-offset'
);
-- 创建 JDBC 维表(用于 Lookup Join)
CREATE TABLE users (
user_id BIGINT,
user_name STRING,
user_level STRING,
PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
'connector' = 'jdbc',
'url' = 'jdbc:mysql://mysql:3306/app',
'table-name' = 'users',
'lookup.cache.max-rows' = '5000',
'lookup.cache.ttl' = '10min'
);
-- 创建 Elasticsearch Sink 表
CREATE TABLE page_view_stats (
user_level STRING,
window_start TIMESTAMP(3),
window_end TIMESTAMP(3),
pv BIGINT,
uv BIGINT,
PRIMARY KEY (user_level, window_start) NOT ENFORCED
) WITH (
'connector' = 'elasticsearch-7',
'hosts' = 'http://es:9200',
'index' = 'page_view_stats'
);
DML:数据查询与写入
sql
-- 窗口聚合 + 维表关联 + 写入
INSERT INTO page_view_stats
SELECT
u.user_level,
window_start,
window_end,
COUNT(*) AS pv,
COUNT(DISTINCT p.user_id) AS uv
FROM TABLE(
TUMBLE(TABLE page_views, DESCRIPTOR(view_time), INTERVAL '5' MINUTE)
) AS p
LEFT JOIN users FOR SYSTEM_TIME AS OF p.view_time AS u
ON p.user_id = u.user_id
GROUP BY u.user_level, window_start, window_end;
Join 类型
Flink SQL 支持丰富的 Join 类型。与传统数据库不同,流式 Join 的核心挑战在于:两条流的数据到达时间不同步,而且流是无界的,不能无限缓存历史数据。因此 Flink 设计了多种特殊的 Join 语义来应对不同场景:
| Join 类型 | 说明 | SQL 语法 | 适用场景 |
|---|---|---|---|
| Regular Join | 双流等值 Join | A JOIN B ON a.id = b.id |
两条流互相关联 |
| Interval Join | 时间区间 Join | A JOIN B ON ... AND a.ts BETWEEN b.ts - 5 AND b.ts + 5 |
限定时间范围的关联 |
| Temporal Join | 时态表 Join | A JOIN B FOR SYSTEM_TIME AS OF a.ts ON ... |
流关联维表(时间版本) |
| Lookup Join | 维表查找 | A JOIN B FOR SYSTEM_TIME AS OF A.proctime ON ... |
流关联外部维表 |
| Left/Right/Full Join | 外连接 | A LEFT JOIN B ON ... |
保留未匹配数据 |
4.3 动态表与持续查询
**动态表(Dynamic Table)**是 Flink SQL 中最核心的抽象概念。在传统数据库中,表是静态的------你执行一次查询就得到一个固定的结果集。但在流处理中,数据持续到达,查询结果也在不断变化。Flink 用"动态表"来建模这种场景:输入的数据流被看作一张持续追加的表(Append-only Table),SQL 查询的结果也是一张持续更新的表(可能包含 INSERT、UPDATE、DELETE)。最终这张结果表再被转换回数据流输出。这个 Stream ↔ Table 的双向转换是 Flink SQL 的理论基石。
Flink SQL 的核心概念是 动态表(Dynamic Table)。流数据被视为不断更新的表,SQL 查询被视为持续执行的查询。
┌─────────────────────────────────────────────────────────────────────────┐
│ Stream ↔ Dynamic Table 转换 │
│ │
│ ① 流 → 动态表(Input) │
│ │
│ 输入流(Append-only): │
│ ──[Alice, ./home]──[Bob, ./cart]──[Alice, ./cart]──→ │
│ │
│ 对应的动态表(持续追加): │
│ ┌───────┬────────┐ │
│ │ user │ url │ 时刻 1: Alice, ./home │
│ ├───────┼────────┤ 时刻 2: + Bob, ./cart │
│ │ Alice │ ./home │ 时刻 3: + Alice, ./cart │
│ │ Bob │ ./cart │ │
│ │ Alice │ ./cart │ │
│ └───────┴────────┘ │
│ │
│ ② 持续查询(Continuous Query) │
│ │
│ SELECT user, COUNT(url) AS cnt FROM clicks GROUP BY user │
│ │
│ 动态结果表(持续更新): │
│ ┌───────┬─────┐ ┌───────┬─────┐ ┌───────┬─────┐ │
│ │ user │ cnt │ │ user │ cnt │ │ user │ cnt │ │
│ ├───────┼─────┤ → ├───────┼─────┤ → ├───────┼─────┤ │
│ │ Alice │ 1 │ │ Alice │ 1 │ │ Alice │ 2 │ │
│ └───────┴─────┘ │ Bob │ 1 │ │ Bob │ 1 │ │
│ └───────┴─────┘ └───────┴─────┘ │
│ │
│ ③ 动态表 → 流(Output) │
│ │
│ Append 流:只有 INSERT(适用于窗口聚合) │
│ Retract 流:INSERT + DELETE(适用于非窗口聚合) │
│ Upsert 流:UPSERT + DELETE(适用于有主键的结果) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
4.4 Catalog 与元数据管理
Catalog 是 Flink SQL 的元数据管理中心,负责存储数据库、表、视图、函数等元数据信息。默认情况下,Flink 使用内存中的 Catalog(GenericInMemoryCatalog),表的定义会在会话结束后丢失。在生产环境中,通常会集成 Hive Catalog,将表元数据持久化到 Hive Metastore 中,这样不同的 Flink 作业可以共享表定义,而且表定义不会随会话结束而丢失。
java
/**
* Catalog 管理 - 使用 Hive Catalog 持久化表元数据
*/
// 配置 Hive Catalog
String catalogName = "my_hive";
String database = "default";
String hiveConfDir = "/opt/hive/conf";
HiveCatalog hiveCatalog = new HiveCatalog(catalogName, database, hiveConfDir);
tableEnv.registerCatalog(catalogName, hiveCatalog);
tableEnv.useCatalog(catalogName);
tableEnv.useDatabase(database);
// 之后创建的表会自动注册到 Hive Metastore
tableEnv.executeSql("CREATE TABLE IF NOT EXISTS ...");
4.5 实战示例:实时 Dashboard
下面通过一个完整的电商实时经营看板案例,展示 Flink SQL 在生产中的实际应用。这个案例用纯 SQL 实现了从 Kafka 读取订单数据、每分钟实时统计 GMV(Gross Merchandise Volume,商品交易总额)和买家数、分品类实时排行等功能------无需编写一行 Java 代码。
sql
-- 实战:电商实时经营看板(全 SQL 实现)
-- 1. 订单实时源表
CREATE TABLE orders_source (
order_id STRING,
user_id BIGINT,
product_id BIGINT,
category STRING,
amount DECIMAL(10, 2),
order_time TIMESTAMP(3),
WATERMARK FOR order_time AS order_time - INTERVAL '10' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'orders',
'properties.bootstrap.servers' = 'kafka:9092',
'format' = 'json',
'scan.startup.mode' = 'latest-offset'
);
-- 2. 每分钟实时 GMV 统计
CREATE VIEW realtime_gmv AS
SELECT
window_start,
window_end,
COUNT(DISTINCT order_id) AS order_count,
COUNT(DISTINCT user_id) AS buyer_count,
SUM(amount) AS gmv,
AVG(amount) AS avg_order_amount
FROM TABLE(
TUMBLE(TABLE orders_source, DESCRIPTOR(order_time), INTERVAL '1' MINUTE)
)
GROUP BY window_start, window_end;
-- 3. 分品类实时排行
CREATE VIEW category_ranking AS
SELECT
category,
window_start,
window_end,
SUM(amount) AS category_gmv,
COUNT(*) AS category_orders,
ROW_NUMBER() OVER (
PARTITION BY window_start ORDER BY SUM(amount) DESC
) AS ranking
FROM TABLE(
TUMBLE(TABLE orders_source, DESCRIPTOR(order_time), INTERVAL '5' MINUTE)
)
GROUP BY category, window_start, window_end;
-- 4. 写入结果表
INSERT INTO dashboard_sink
SELECT * FROM realtime_gmv;
INSERT INTO category_ranking_sink
SELECT * FROM category_ranking WHERE ranking <= 10;
五、基本用法
本章通过四个由浅入深的实战案例,展示 Flink 在不同场景下的编程方法。从经典的 WordCount 入门,到实时日志分析、用户行为漏斗统计,再到 CEP 复杂事件处理------每个案例都是生产中的真实场景简化版,帮助你建立从"知道概念"到"能写代码"的桥梁。
5.1 WordCount 经典示例
WordCount(词频统计)是大数据领域的"Hello World"------虽然简单,但它涵盖了 Flink 编程的核心流程:创建执行环境、读取数据、转换处理(flatMap 分词 + keyBy 分组 + sum 聚合)、输出结果、触发执行。下面分别用 Java DataStream API、PyFlink 和 Flink SQL 三种方式实现同一个逻辑,方便对比不同 API 的编程风格。
Java 版本
java
/**
* Flink WordCount - Java DataStream API
*/
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class WordCount {
public static void main(String[] args) throws Exception {
// 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 读取数据
DataStream<String> text = env.readTextFile("hdfs://data/input/*.txt");
// WordCount 逻辑
DataStream<Tuple2<String, Integer>> counts = text
// 分词
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String line, Collector<Tuple2<String, Integer>> out) {
for (String word : line.split("\\s+")) {
if (!word.isEmpty()) {
out.collect(Tuple2.of(word, 1));
}
}
}
})
// 按词分组
.keyBy(value -> value.f0)
// 求和
.sum(1);
// 输出
counts.print();
// 执行
env.execute("WordCount");
}
}
PyFlink 版本
python
"""
Flink WordCount - PyFlink DataStream API
"""
from pyflink.datastream import StreamExecutionEnvironment
from pyflink.common.typeinfo import Types
env = StreamExecutionEnvironment.get_execution_environment()
# 读取数据
text = env.read_text_file("hdfs://data/input/*.txt")
# WordCount 逻辑
counts = (text
.flat_map(lambda line: [(word, 1) for word in line.split() if word],
output_type=Types.TUPLE([Types.STRING(), Types.INT()]))
.key_by(lambda x: x[0])
.reduce(lambda a, b: (a[0], a[1] + b[1]))
)
# 输出
counts.print()
# 执行
env.execute("WordCount")
Flink SQL 版本
sql
-- Flink SQL WordCount
-- 创建输入表
CREATE TABLE text_source (
line STRING
) WITH (
'connector' = 'filesystem',
'path' = 'hdfs://data/input/',
'format' = 'raw'
);
-- 创建输出表
CREATE TABLE word_counts (
word STRING,
cnt BIGINT,
PRIMARY KEY (word) NOT ENFORCED
) WITH (
'connector' = 'print'
);
-- 执行 WordCount
INSERT INTO word_counts
SELECT word, COUNT(*) AS cnt
FROM text_source,
LATERAL TABLE(string_split(line, ' ')) AS T(word)
WHERE word <> ''
GROUP BY word;
5.2 实时日志分析
实时日志分析是 Flink 最常见的生产场景之一。下面的案例从 Kafka 消费 Nginx 访问日志,同时完成三个分析任务:每分钟的 HTTP 状态码分布、每 5 分钟的 Top 10 URL 排行、以及 5xx 错误的实时告警。这个案例展示了 Flink 的几个关键能力:多路输出(一条流同时产生多个分析结果)、侧输出(Side Output,将异常数据分流到单独的流处理)、以及窗口聚合。
java
/**
* 实时日志分析 - 统计各 HTTP 状态码分布、Top URL、异常告警
*/
public class RealtimeLogAnalysis {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 从 Kafka 读取 Nginx 日志
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers("kafka:9092")
.setTopics("nginx-logs")
.setGroupId("log-analysis")
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStream<String> logs = env.fromSource(
source, WatermarkStrategy.noWatermarks(), "Nginx Logs");
// 解析日志
DataStream<AccessLog> parsed = logs
.map(AccessLog::parse)
.filter(Objects::nonNull);
// 分析 1:每分钟状态码分布
DataStream<StatusCodeStats> statusStats = parsed
.keyBy(AccessLog::getStatusCode)
.window(TumblingProcessingTimeWindows.of(Time.minutes(1)))
.aggregate(new CountAgg(), new StatusWindowFunction());
// 分析 2:每分钟 Top 10 URL
DataStream<UrlRanking> topUrls = parsed
.keyBy(AccessLog::getUrl)
.window(SlidingProcessingTimeWindows.of(Time.minutes(5), Time.minutes(1)))
.aggregate(new CountAgg(), new UrlWindowFunction());
// 分析 3:5xx 错误实时告警
final OutputTag<AccessLog> errorTag = new OutputTag<AccessLog>("5xx-errors") {};
SingleOutputStreamOperator<AccessLog> mainStream = parsed
.process(new ProcessFunction<AccessLog, AccessLog>() {
@Override
public void processElement(AccessLog log, Context ctx,
Collector<AccessLog> out) {
out.collect(log);
if (log.getStatusCode() >= 500) {
ctx.output(errorTag, log);
}
}
});
// 获取错误流,按 URL 聚合后告警
DataStream<AccessLog> errors = mainStream.getSideOutput(errorTag);
DataStream<ErrorAlert> alerts = errors
.keyBy(AccessLog::getUrl)
.window(TumblingProcessingTimeWindows.of(Time.minutes(1)))
.aggregate(new ErrorCountAgg())
.filter(alert -> alert.getErrorCount() > 10); // 1 分钟超过 10 次
// 输出
statusStats.sinkTo(/* Elasticsearch Sink */);
topUrls.sinkTo(/* Redis Sink */);
alerts.sinkTo(/* Alert Sink: 钉钉/飞书/PagerDuty */);
env.execute("Realtime Log Analysis");
}
}
5.3 实时用户行为分析
实时漏斗分析是电商和互联网产品中的核心需求------追踪用户在"浏览→点击→加购→下单→支付"这条转化路径上的每个阶段的转化率。与离线漏斗分析不同,实时漏斗需要为每个用户维护当前所处的漏斗阶段状态,并在用户行为超时(如 30 分钟未完成支付)时输出中间结果。下面的案例充分展示了 Flink 有状态处理的威力:ValueState 记录用户漏斗进度,registerEventTimeTimer 注册超时定时器。
java
/**
* 实时用户行为分析 - 实时漏斗统计
*/
public class RealtimeFunnelAnalysis {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<UserEvent> events = env.fromSource(/* Kafka Source */);
// 漏斗:浏览 → 点击 → 加购 → 下单 → 支付
DataStream<FunnelResult> funnel = events
.keyBy(UserEvent::getUserId)
.process(new KeyedProcessFunction<String, UserEvent, FunnelResult>() {
// 状态:记录用户在漏斗中到达的最大阶段
private ValueState<Integer> maxStageState;
// 状态:记录进入漏斗的时间
private ValueState<Long> entryTimeState;
private static final String[] STAGES = {
"view", "click", "add_cart", "order", "pay"
};
@Override
public void open(Configuration parameters) {
maxStageState = getRuntimeContext().getState(
new ValueStateDescriptor<>("max-stage", Integer.class));
entryTimeState = getRuntimeContext().getState(
new ValueStateDescriptor<>("entry-time", Long.class));
}
@Override
public void processElement(UserEvent event, Context ctx,
Collector<FunnelResult> out) throws Exception {
int eventStage = getStageIndex(event.getEventType());
if (eventStage < 0) return;
Integer maxStage = maxStageState.value();
if (maxStage == null) maxStage = -1;
// 用户首次进入漏斗
if (eventStage == 0 && maxStage < 0) {
maxStageState.update(0);
entryTimeState.update(event.getTimestamp());
// 注册 30 分钟超时定时器
ctx.timerService().registerEventTimeTimer(
event.getTimestamp() + 30 * 60 * 1000);
}
// 用户推进到下一阶段
if (eventStage == maxStage + 1) {
maxStageState.update(eventStage);
}
// 用户完成支付
if (eventStage == STAGES.length - 1) {
Long entryTime = entryTimeState.value();
out.collect(new FunnelResult(
event.getUserId(), maxStageState.value(),
event.getTimestamp() - entryTime));
// 清空状态
maxStageState.clear();
entryTimeState.clear();
}
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx,
Collector<FunnelResult> out) throws Exception {
// 超时未完成,输出当前进度
Integer maxStage = maxStageState.value();
if (maxStage != null && maxStage >= 0) {
out.collect(new FunnelResult(
ctx.getCurrentKey(), maxStage, -1));
maxStageState.clear();
entryTimeState.clear();
}
}
private int getStageIndex(String eventType) {
for (int i = 0; i < STAGES.length; i++) {
if (STAGES[i].equals(eventType)) return i;
}
return -1;
}
});
funnel.sinkTo(/* Sink */);
env.execute("Realtime Funnel Analysis");
}
}
5.4 CEP 复杂事件处理
**CEP(Complex Event Processing,复杂事件处理)**是一种从事件流中检测符合特定模式(Pattern)的事件序列的技术。例如:检测"连续 3 次登录失败后紧接着 1 次登录成功"------这种模式可能暗示账号被盗。Flink 内置了 CEP 库(flink-cep),提供了丰富的模式定义 API:begin 定义起始事件、followedBy 定义后续事件、times(n) 指定重复次数、within 限定时间窗口等。CEP 广泛应用于金融风控、安全监控、物联网异常检测等场景。
Flink CEP(Complex Event Processing)用于从事件流中检测符合特定模式的事件序列。
java
/**
* CEP 示例 - 检测登录异常(连续 3 次失败后成功 = 可疑登录)
*/
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
public class LoginFailDetection {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<LoginEvent> loginEvents = env.fromSource(/* Source */);
// 定义 CEP 模式:连续 3 次失败 → 1 次成功(10 分钟内)
Pattern<LoginEvent, ?> pattern = Pattern.<LoginEvent>begin("failures")
.where(new SimpleCondition<LoginEvent>() {
@Override
public boolean filter(LoginEvent event) {
return "FAIL".equals(event.getStatus());
}
})
.times(3) // 连续 3 次
.consecutive() // 必须连续
.followedBy("success") // 之后跟随
.where(new SimpleCondition<LoginEvent>() {
@Override
public boolean filter(LoginEvent event) {
return "SUCCESS".equals(event.getStatus());
}
})
.within(Time.minutes(10)); // 10 分钟内
// 将模式应用到流上
PatternStream<LoginEvent> patternStream = CEP.pattern(
loginEvents.keyBy(LoginEvent::getUserId), pattern);
// 提取匹配结果
DataStream<SuspiciousLogin> alerts = patternStream.select(
(Map<String, List<LoginEvent>> match) -> {
List<LoginEvent> failures = match.get("failures");
LoginEvent success = match.get("success").get(0);
return new SuspiciousLogin(
success.getUserId(),
failures.get(0).getTimestamp(),
success.getTimestamp(),
failures.stream().map(LoginEvent::getIp).collect(Collectors.toList()),
success.getIp()
);
}
);
alerts.sinkTo(/* Alert Sink */);
env.execute("Login Fail Detection");
}
}
六、内存模型与资源管理
理解 Flink 的内存模型对于生产环境的调优至关重要。与 Spark 类似,Flink 的 TaskManager 也将内存划分为多个区域,每个区域有特定的用途和大小限制。配置不当会导致各种 OOM(Out Of Memory)错误------这是 Flink 生产运维中最常见的问题之一。本章将详细解析 TaskManager 的内存布局、各区域的配置参数、常见 OOM 场景的诊断与解决方案,以及网络缓冲区的工作机制。
6.1 TaskManager 内存布局
TaskManager 的内存分为以下几个主要区域:Framework Memory (Flink 框架自身使用)、Task Memory (用户代码使用)、Network Memory (Task 之间的数据传输缓冲区)、Managed Memory(Flink 托管的堆外内存,主要用于 RocksDB 状态后端和批处理排序)。此外还有 JVM Metaspace(类元数据)和 JVM Overhead(GC、线程栈等开销)。
┌─────────────────────────────────────────────────────────────────────────┐
│ Flink TaskManager 内存布局 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Total Process Memory │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ Total Flink Memory │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Framework Memory(框架内存) │ │ │ │
│ │ │ │ Framework Heap: 128MB (框架堆内存) │ │ │ │
│ │ │ │ Framework Off-Heap: 128MB (框架堆外内存) │ │ │ │
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Task Memory(任务内存) │ │ │ │
│ │ │ │ Task Heap: 用户代码堆内存 │ │ │ │
│ │ │ │ Task Off-Heap: 用户代码堆外内存 │ │ │ │
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Network Memory(网络缓冲区) │ │ │ │
│ │ │ │ 用于 Task 之间的数据交换(Shuffle) │ │ │ │
│ │ │ │ 默认占 Total Flink Memory 的 10% │ │ │ │
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Managed Memory(托管内存) │ │ │ │
│ │ │ │ • RocksDB 状态后端 │ │ │ │
│ │ │ │ • 批处理排序/哈希表 │ │ │ │
│ │ │ │ • Python 进程(PyFlink) │ │ │ │
│ │ │ │ 默认占 Total Flink Memory 的 40% │ │ │ │
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ JVM Metaspace: 256MB(类元数据) │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ JVM Overhead: 总内存 10%(GC、线程栈等) │ │ │
│ │ └───────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
6.2 内存配置参数
下表列出了 TaskManager 内存相关的核心配置参数。在配置时有两种入口:可以直接设置 taskmanager.memory.process.size(总进程内存,适合容器化部署),或者设置 taskmanager.memory.flink.size(Flink 可用内存,不含 JVM 开销),Flink 会自动按比例分配各区域的大小。
| 参数 | 默认值 | 说明 | 推荐设置 |
|---|---|---|---|
taskmanager.memory.process.size |
- | TM 总进程内存 | 容器场景使用 |
taskmanager.memory.flink.size |
- | Flink 总内存(不含 JVM 开销) | 非容器场景使用 |
taskmanager.memory.task.heap.size |
- | 任务堆内存 | 根据用户代码需求设置 |
taskmanager.memory.task.off-heap.size |
0 | 任务堆外内存 | 使用 RocksDB 时适当增大 |
taskmanager.memory.managed.fraction |
0.4 | 托管内存比例 | 使用 RocksDB 时 0.4-0.7 |
taskmanager.memory.network.fraction |
0.1 | 网络内存比例 | 高吞吐场景适当增大 |
taskmanager.memory.network.min |
64MB | 网络内存最小值 | - |
taskmanager.memory.network.max |
1GB | 网络内存最大值 | - |
taskmanager.memory.jvm-metaspace.size |
256MB | JVM 元空间 | 算子多时适当增大 |
taskmanager.memory.jvm-overhead.fraction |
0.1 | JVM 开销比例 | - |
┌─────────────────────────────────────────────────────────────────────────┐
│ 内存配置计算示例 │
│ │
│ 假设:taskmanager.memory.process.size = 4096MB │
│ │
│ 1. JVM Overhead = 4096 × 0.1 = 409.6MB │
│ 2. JVM Metaspace = 256MB │
│ 3. Total Flink Memory = 4096 - 409.6 - 256 = 3430.4MB │
│ 4. Framework Heap = 128MB │
│ 5. Framework Off-Heap = 128MB │
│ 6. Network Memory = 3430.4 × 0.1 = 343MB │
│ 7. Managed Memory = 3430.4 × 0.4 = 1372.2MB │
│ 8. Task Heap = 3430.4 - 128 - 128 - 343 - 1372.2 = 1459.2MB │
│ │
│ 总结:Task Heap ≈ 1.4GB 可供用户代码使用 │
└─────────────────────────────────────────────────────────────────────────┘
6.3 常见 OOM 场景与解决方案
OOM(Out Of Memory)是 Flink 生产环境中最常见的问题。不同区域的内存不足会产生不同的错误信息,理解这些差异是快速定位和解决问题的关键。
6.3.1 Direct Memory OOM
症状 :java.lang.OutOfMemoryError: Direct buffer memory
原因:
- Network Buffer 不足
- RocksDB 内存超出限制
- 用户代码使用了大量堆外内存
解决方案:
yaml
# 增大网络内存
taskmanager.memory.network.fraction: 0.15
taskmanager.memory.network.max: 2gb
# 增大任务堆外内存
taskmanager.memory.task.off-heap.size: 512mb
6.3.2 Metaspace OOM
症状 :java.lang.OutOfMemoryError: Metaspace
原因:作业中算子过多,加载了大量类
解决方案:
yaml
# 增大 Metaspace
taskmanager.memory.jvm-metaspace.size: 512mb
6.3.3 GC Overhead OOM
症状 :java.lang.OutOfMemoryError: GC overhead limit exceeded
原因:Task Heap 内存不足,频繁 Full GC
解决方案:
yaml
# 增大 Task Heap
taskmanager.memory.task.heap.size: 2gb
# 或减少其他内存占用
taskmanager.memory.managed.fraction: 0.3
6.4 网络缓冲区(Network Buffer)
Network Buffer 是 Flink Task 之间进行数据传输的内存缓冲区。当上游 Task 产生数据时,数据先写入 Output Buffer,然后通过网络(TCP)传输到下游 Task 的 Input Buffer。每个 Buffer 默认大小为 32KB。Network Buffer 的大小直接影响 Flink 的吞吐量和反压行为------当下游的 Input Buffer 满了,反压就会向上游传播,最终可能导致整条处理链路暂停。
┌─────────────────────────────────────────────────────────────────────────┐
│ Network Buffer 数据传输 │
│ │
│ Task A (上游) Task B (下游) │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Operator │ │ Operator │ │
│ │ ↓ │ │ ↑ │ │
│ │ ┌─────────┐ │ Network (TCP) │ ┌─────────┐ │ │
│ │ │ Output │ │ ─────────────────────→ │ │ Input │ │ │
│ │ │ Buffer │ │ │ │ Buffer │ │ │
│ │ │ (32KB) │ │ │ │ (32KB) │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ Buffer 参数配置: │
│ • taskmanager.memory.segment-size: 32KB (单个 Buffer 大小) │
│ • 总 Buffer 数 = Network Memory / Segment Size │
│ • 每个 Gate 所需 Buffer = 上下游并行度 │
│ │
│ 反压传导: │
│ Input Buffer 满 → 停止接收 → 上游 Output Buffer 满 │
│ → 上游算子暂停 → 反压向上传播 │
└─────────────────────────────────────────────────────────────────────────┘
七、性能调优实战
性能调优是 Flink 生产化过程中最重要也最具挑战性的环节。一个未经调优的 Flink 作业可能只发挥了 10% 的集群性能,而经过系统性调优后往往能获得数倍甚至数十倍的性能提升。本章将从并行度、状态后端、Checkpoint、反压、数据倾斜、资源配置六个维度,提供详尽的调优指南和生产检查清单。
7.1 并行度调优
**并行度(Parallelism)**决定了算子有多少个并行实例同时执行。合理的并行度设置是性能调优的基础------设置太低会浪费集群资源,设置太高可能导致过多的 Shuffle 开销或数据库连接数过多。核心原则是:Source 并行度与数据源分区数对齐,计算算子并行度充分利用可用 Slot,Sink 并行度适当降低以避免对外部系统产生过大压力。
并行度设置原则:
| 场景 | 推荐并行度 | 说明 |
|---|---|---|
| Source(Kafka) | = Kafka 分区数 | 一个并行度消费一个分区 |
| 计算算子 | = 可用 Slot 数 | 充分利用集群资源 |
| Sink(数据库) | 适当降低 | 避免数据库连接过多 |
| 窗口算子 | 视 Key 基数而定 | Key 少时降低避免空窗口 |
java
// 全局并行度
env.setParallelism(16);
// Source 并行度 = Kafka 分区数
source.setParallelism(32);
// 计算并行度
stream.keyBy(...)
.window(...)
.aggregate(...)
.setParallelism(64);
// Sink 并行度(降低)
stream.sinkTo(jdbcSink).setParallelism(8);
7.2 状态后端调优(RocksDB)
RocksDB 是 Flink 生产环境中最常用的状态后端,但其默认配置并不适合所有场景。RocksDB 本质上是一个嵌入式的 LSM-Tree(Log-Structured Merge-Tree)键值数据库,它的性能主要受磁盘类型(SSD vs HDD)、Block Cache 大小(读缓存)、Write Buffer 大小(写缓存)等因素影响。以下是核心调优参数和检查清单。
yaml
# RocksDB 核心参数调优
state.backend: rocksdb
state.backend.incremental: true
state.backend.rocksdb.localdir: /ssd/flink/rocksdb
# Block Cache(读缓存)
state.backend.rocksdb.block.cache-size: 256mb
# Write Buffer(写缓存)
state.backend.rocksdb.writebuffer.size: 128mb
state.backend.rocksdb.writebuffer.count: 4
# 线程数
state.backend.rocksdb.thread.num: 4
# 压缩
state.backend.rocksdb.compression: LZ4_COMPRESSION
┌─────────────────────────────────────────────────────────────────────────┐
│ RocksDB 调优检查清单 │
│ │
│ □ 使用 SSD 存储 RocksDB 数据目录 │
│ → 机械硬盘性能差 10 倍以上 │
│ │
│ □ 开启增量 Checkpoint │
│ → state.backend.incremental: true │
│ → 大状态场景 Checkpoint 时间减少 90% │
│ │
│ □ 合理设置 Block Cache │
│ → 一般为 Managed Memory 的 1/3 │
│ → 热点数据命中率 > 95% │
│ │
│ □ 调整 Write Buffer │
│ → 写入密集:增大 writebuffer.size │
│ → 多 Column Family:增大 writebuffer.count │
│ │
│ □ 启用布隆过滤器 │
│ → state.backend.rocksdb.use-bloom-filter: true │
│ → 加速 MapState 的 get 操作 │
│ │
│ □ 监控 RocksDB Metrics │
│ → 关注 compaction、stall、cache hit rate │
└─────────────────────────────────────────────────────────────────────────┘
7.3 Checkpoint 调优
Checkpoint 调优的核心目标是:在保证容错能力的前提下,尽量减少 Checkpoint 对正常数据处理的影响。Checkpoint 频率太高会增加系统负担,太低会导致故障恢复时需要重放更多数据。另外,当 Checkpoint 耗时过长甚至超时失败时,通常意味着状态过大、反压严重或存储写入性能不足------需要根据具体现象对症下药。
yaml
# Checkpoint 核心参数
execution.checkpointing.interval: 60s # Checkpoint 间隔
execution.checkpointing.timeout: 600s # 超时时间
execution.checkpointing.min-pause: 30s # 最小间隔
execution.checkpointing.max-concurrent-checkpoints: 1
execution.checkpointing.mode: EXACTLY_ONCE
# 存储配置
state.checkpoints.dir: hdfs://namenode:8020/flink/checkpoints
state.savepoints.dir: hdfs://namenode:8020/flink/savepoints
# 保留数量
state.checkpoints.num-retained: 3
# 非对齐 Checkpoint(降低反压场景下的 Checkpoint 耗时)
execution.checkpointing.unaligned.enabled: true
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Checkpoint 超时 | 状态过大 | 开启增量 Checkpoint |
| Checkpoint 超时 | Barrier 对齐慢 | 启用非对齐 Checkpoint |
| Checkpoint 超时 | HDFS 写入慢 | 优化 HDFS 或换 S3 |
| Checkpoint 频繁失败 | 反压严重 | 先解决反压问题 |
| Checkpoint 间隔内完不成 | 间隔太短 | 适当增大间隔 |
| 恢复时间过长 | 状态过大 | 增加并行度分散状态 |
7.4 反压(Backpressure)诊断与处理
**反压(Backpressure)**是流处理系统中一种自然的流量控制机制:当下游算子处理速度跟不上上游数据产生速度时,系统会自动减慢上游的处理速度,防止数据丢失或内存溢出。适度的反压是正常的,但持续的严重反压意味着系统存在瓶颈------可能是 Sink 写入慢、某个算子计算逻辑过重、数据倾斜导致个别 Task 过载、或者资源不足。
诊断反压的关键是找到瓶颈算子------它通常是"第一个显示 HIGH 反压的算子的下一个算子"(即反压的源头是那个"忙但不反压"的算子)。
┌─────────────────────────────────────────────────────────────────────────┐
│ 反压传播示意图 │
│ │
│ Source ──→ Map ──→ KeyBy/Window ──→ Aggregate ──→ Sink │
│ OK OK HIGH ⚠️ OK SLOW 🐢 │
│ ↑ │ │
│ └──────── 反压向上传播 ─────────┘ │
│ │
│ 根源定位:反压从 Sink 向上传播 │
│ • Sink 写入慢 → 整条链路被拖慢 │
│ • 需要找到"第一个 HIGH"的算子的下游 │
│ │
│ Flink Web UI 反压监控: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Operator │ Backpressure │ Busy │ Idle │ Status │ │
│ ├──────────────────┼──────────────┼────────┼────────┼──────────┤ │
│ │ Source: Kafka │ LOW │ 30% │ 70% │ OK │ │
│ │ Map │ LOW │ 40% │ 60% │ OK │ │
│ │ KeyBy/Window │ HIGH │ 99% │ 1% │ ⚠️ │ │
│ │ Aggregate │ LOW │ 50% │ 50% │ OK │ │
│ │ Sink: JDBC │ LOW │ 95% │ 5% │ 🐢 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 诊断结论:Sink 处理能力不足,导致 KeyBy/Window 反压 │
└─────────────────────────────────────────────────────────────────────────┘
反压解决方案:
| 原因 | 解决方案 |
|---|---|
| Sink 写入慢 | 增大 Sink 并行度、批量写入、使用异步 Sink |
| 算子计算慢 | 优化业务逻辑、增大并行度、避免阻塞调用 |
| 数据倾斜 | 加盐打散热点 Key、使用 rebalance |
| 状态访问慢 | 优化 RocksDB 配置、使用 SSD |
| 序列化开销大 | 使用 Flink 内置类型、避免 Kryo fallback |
| GC 频繁 | 增大堆内存、优化对象创建 |
7.5 数据倾斜处理
数据倾斜(Data Skew)是分布式计算中最常见的性能杀手之一。当某些 Key 的数据量远超其他 Key 时(例如电商场景中的"大卖家"),负责处理这些热点 Key 的 Task 会严重过载,而其他 Task 则处于空闲状态。解决数据倾斜最经典的方法是两阶段聚合(也叫"加盐聚合"):第一阶段给热点 Key 加随机后缀打散到多个 Task 局部聚合,第二阶段去掉后缀做全局聚合。
java
/**
* 数据倾斜优化 - 两阶段聚合
*/
// 问题:某些 Key 数据量远超其他,导致个别 Task 处理过慢
// 解决:先加盐局部聚合,再去盐全局聚合
// 第一阶段:加盐 + 局部聚合
int saltNum = 16;
DataStream<Tuple3<String, Integer, Long>> localAgg = stream
.map(event -> {
int salt = ThreadLocalRandom.current().nextInt(saltNum);
return Tuple3.of(event.getKey() + "_" + salt, salt, event.getValue());
})
.keyBy(t -> t.f0)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.reduce((a, b) -> Tuple3.of(a.f0, a.f1, a.f2 + b.f2));
// 第二阶段:去盐 + 全局聚合
DataStream<Tuple2<String, Long>> globalAgg = localAgg
.map(t -> Tuple2.of(t.f0.split("_")[0], t.f2))
.keyBy(t -> t.f0)
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.reduce((a, b) -> Tuple2.of(a.f0, a.f1 + b.f1));
7.6 资源配置最佳实践
以下是一份经过生产验证的中等规模集群配置模板,可以作为调优的起点。实际配置需要根据业务特点(状态大小、数据量、延迟要求)进行调整。
yaml
# 生产环境推荐配置(中等规模)
# JobManager
jobmanager.memory.process.size: 4096mb
jobmanager.memory.heap.size: 2048mb
# TaskManager
taskmanager.memory.process.size: 8192mb
taskmanager.numberOfTaskSlots: 4
# 状态后端
state.backend: rocksdb
state.backend.incremental: true
state.backend.rocksdb.localdir: /ssd/flink/rocksdb
# Checkpoint
execution.checkpointing.interval: 60s
execution.checkpointing.min-pause: 30s
state.checkpoints.dir: hdfs:///flink/checkpoints
state.checkpoints.num-retained: 3
# 重启策略
restart-strategy: fixed-delay
restart-strategy.fixed-delay.attempts: 3
restart-strategy.fixed-delay.delay: 30s
# 网络
taskmanager.memory.network.fraction: 0.1
taskmanager.memory.network.min: 128mb
7.7 调优检查清单
┌─────────────────────────────────────────────────────────────────────────┐
│ Flink 性能调优检查清单 │
│ │
│ □ 1. 资源配置 │
│ □ TaskManager 内存是否充足 │
│ □ Slot 数是否合理(通常等于 CPU 核心数) │
│ □ 是否开启了动态资源分配(Reactive Mode) │
│ │
│ □ 2. 并行度 │
│ □ Source 并行度 = Kafka 分区数 │
│ □ Sink 并行度是否适中(避免过多连接) │
│ □ 计算算子并行度是否充分利用 Slot │
│ │
│ □ 3. 状态管理 │
│ □ 生产环境使用 RocksDB 状态后端 │
│ □ RocksDB 数据目录使用 SSD │
│ □ 及时清理过期状态(State TTL) │
│ □ 状态大小是否可控 │
│ │
│ □ 4. Checkpoint │
│ □ Checkpoint 间隔是否合理(30s-5min) │
│ □ 是否开启增量 Checkpoint │
│ □ Checkpoint 是否能在间隔内完成 │
│ □ 反压场景是否启用非对齐 Checkpoint │
│ │
│ □ 5. 反压 │
│ □ 是否存在反压(Flink Web UI 检查) │
│ □ 反压根源是否已定位(从 Sink 向上追溯) │
│ □ 是否存在数据倾斜 │
│ │
│ □ 6. 代码优化 │
│ □ 优先使用 Flink SQL 而非 DataStream API │
│ □ 避免在算子中使用阻塞 IO │
│ □ 使用 Flink 内置类型避免 Kryo 序列化 │
│ □ 窗口使用 AggregateFunction 而非 ProcessWindowFunction │
│ │
│ □ 7. 监控告警 │
│ □ 配置 Checkpoint 失败告警 │
│ □ 配置反压持续告警 │
│ □ 配置消费延迟(Lag)告警 │
│ □ 配置作业重启告警 │
└─────────────────────────────────────────────────────────────────────────┘
八、Flink 源码解析
深入理解 Flink 的源码有助于更好地使用和调优它。本章选取了四个最核心的源码模块进行解析:JobGraph 的生成过程(理解算子链化优化)、Task 的调度与执行(理解作业如何在集群中运行)、Checkpoint 协调器(理解容错的实现机制)、以及 Watermark 的传播(理解时间语义的底层实现)。这些都是面试中的高频考点,也是排查生产问题时经常需要追踪的代码路径。
8.1 JobGraph 生成过程
JobGraph 是 Flink 提交给集群执行的核心数据结构。它从 StreamGraph(保留了用户程序中所有算子的原始关系)优化而来,最关键的优化就是算子链化(Operator Chaining)------将满足条件的相邻算子合并为一个 JobVertex,使它们在同一个线程中执行,避免了不必要的线程切换和数据序列化开销。
java
// 文件:flink-streaming-java/.../graph/StreamGraphGenerator.java
// 核心方法:generate()
/**
* StreamGraph → JobGraph 的核心流程:
* 1. 遍历 StreamNode,构建 StreamGraph
* 2. 判断算子是否可链化(Chaining)
* 3. 可链化的算子合并为一个 JobVertex
* 4. 设置 Slot Sharing Group
*/
public class StreamGraphGenerator {
public StreamGraph generate() {
// 1. 清理并验证 Transformation
alreadyTransformed = new HashMap<>();
// 2. 遍历所有 Transformation,生成 StreamNode
for (Transformation<?> transformation : transformations) {
transform(transformation);
}
// 3. 后处理:设置 Slot Sharing Group、CoLocation
return streamGraph;
}
}
// 文件:flink-streaming-java/.../graph/StreamingJobGraphGenerator.java
// 核心方法:createJobGraph()
/**
* 算子链化判断逻辑
*/
public static boolean isChainable(StreamEdge edge, StreamGraph streamGraph) {
StreamNode upStreamVertex = streamGraph.getSourceVertex(edge);
StreamNode downStreamVertex = streamGraph.getTargetVertex(edge);
return
// 1. 下游只有一个输入
downStreamVertex.getInEdges().size() == 1
// 2. 上下游并行度相同
&& upStreamVertex.getParallelism() == downStreamVertex.getParallelism()
// 3. 连接方式是 Forward
&& edge.getPartitioner() instanceof ForwardPartitioner
// 4. 上下游在同一个 Slot Sharing Group
&& upStreamVertex.getSlotSharingGroup().equals(downStreamVertex.getSlotSharingGroup())
// 5. 链化策略允许
&& downStreamVertex.getOperatorFactory().getChainingStrategy() == ChainingStrategy.ALWAYS
// 6. 上游策略允许
&& upStreamVertex.getOperatorFactory().getChainingStrategy() != ChainingStrategy.NEVER;
}
关键点:
- StreamGraph → JobGraph 的核心是算子链化
- 链化后减少线程切换和序列化开销
- 可通过
disableChaining()/startNewChain()手动控制
8.2 Task 调度与执行
当 JobGraph 提交到 JobManager 后,JobMaster 会将其转换为 ExecutionGraph(按并行度展开),然后通过调度器(DefaultScheduler)将每个 ExecutionVertex 部署到 TaskManager 的 Slot 中执行。每个 Task 在 TaskManager 中以独立线程运行,经历 INITIALIZING → RUNNING → FINISHED 的生命周期。
java
// 文件:flink-runtime/.../scheduler/DefaultScheduler.java
// 核心方法:startScheduling()
/**
* Task 调度流程:
* 1. ExecutionGraph 状态转为 RUNNING
* 2. 按调度策略选择需要部署的 ExecutionVertex
* 3. 向 ResourceManager 申请 Slot
* 4. 将 Task 部署到 TaskManager
*/
// 文件:flink-runtime/.../taskmanager/Task.java
// 核心方法:doRun()
/**
* Task 执行主循环
*/
public class Task implements Runnable {
@Override
public void run() {
try {
// 1. 状态转为 INITIALIZING
transitionState(ExecutionState.INITIALIZING);
// 2. 初始化算子(恢复状态)
invokable.restore();
// 3. 状态转为 RUNNING
transitionState(ExecutionState.RUNNING);
// 4. 执行算子主逻辑
invokable.invoke();
// 5. 执行完成
transitionState(ExecutionState.FINISHED);
} catch (Exception e) {
// 异常处理 → FAILED
transitionState(ExecutionState.FAILED, e);
}
}
}
8.3 Checkpoint 协调器源码
CheckpointCoordinator 是 Flink 容错机制的"大脑",运行在 JobManager 中。它负责按照配置的间隔周期性地触发 Checkpoint,向所有 Source Task 发送触发消息,收集所有 Task 的 ACK(确认完成快照),最终标记 Checkpoint 为成功或失败。理解这段源码有助于诊断 Checkpoint 超时、Checkpoint 失败等生产问题。
java
// 文件:flink-runtime/.../checkpoint/CheckpointCoordinator.java
// 核心方法:triggerCheckpoint()
/**
* Checkpoint 触发流程:
* 1. 生成全局唯一的 Checkpoint ID
* 2. 向所有 Source Task 发送 Trigger 消息
* 3. Source 注入 Barrier 到数据流
* 4. 等待所有 Task 确认(ACK)
* 5. 收到全部 ACK → Checkpoint 成功
*/
public class CheckpointCoordinator {
public CompletableFuture<CompletedCheckpoint> triggerCheckpoint(boolean isPeriodic) {
// 1. 检查是否满足触发条件
CheckpointTriggerRequest request = new CheckpointTriggerRequest(/* ... */);
// 2. 创建 PendingCheckpoint
long checkpointId = checkpointIdCounter.getAndIncrement();
PendingCheckpoint checkpoint = new PendingCheckpoint(
job, checkpointId, timestamp, tasksToTrigger, tasksToAck);
// 3. 向 Source Task 发送触发消息
for (Execution execution : tasksToTrigger) {
execution.triggerCheckpoint(checkpointId, timestamp);
}
// 4. 等待所有 ACK
// → 每个 Task 完成快照后调用 acknowledgeCheckpoint()
// → 收齐后调用 completePendingCheckpoint()
return checkpoint.getCompletionFuture();
}
/**
* 处理单个 Task 的 ACK
*/
public void receiveAcknowledgeMessage(AcknowledgeCheckpoint message) {
PendingCheckpoint checkpoint = pendingCheckpoints.get(message.getCheckpointId());
checkpoint.acknowledgeTask(message.getTaskExecutionId(), message.getSubtaskState());
// 检查是否所有 Task 都已 ACK
if (checkpoint.areAllTasksAcknowledged()) {
completePendingCheckpoint(checkpoint);
}
}
}
8.4 Watermark 传播机制
Watermark 在算子之间的传播遵循一个重要规则:对于多输入算子(如 Join、Union),输出的 Watermark 是所有输入 Watermark 的最小值。这意味着如果某个输入源的数据延迟严重(Watermark 推进缓慢),会拖慢整条处理链路的 Watermark 推进,进而导致窗口触发延迟和状态膨胀。Flink 1.17 引入的 Watermark 对齐功能可以缓解这个问题。
java
// 文件:flink-streaming-java/.../operators/AbstractStreamOperator.java
// Watermark 在算子间的传播
/**
* Watermark 传播规则:
* 1. 单输入算子:Watermark 直接透传
* 2. 多输入算子:取所有输入 Watermark 的最小值
* 3. 窗口算子:Watermark 触发窗口计算
*/
// 文件:flink-streaming-java/.../operators/AbstractStreamOperatorV2.java
public void processWatermark(Watermark watermark) throws Exception {
// 对于多输入算子(如 Join/CoProcess)
// 更新当前输入的 Watermark
inputWatermarks[inputIndex] = watermark.getTimestamp();
// 取所有输入的最小 Watermark
long minWatermark = Long.MAX_VALUE;
for (long wm : inputWatermarks) {
minWatermark = Math.min(minWatermark, wm);
}
// 只有最小 Watermark 推进了,才向下游发送
if (minWatermark > currentWatermark) {
currentWatermark = minWatermark;
// 触发窗口计算
internalTimerService.advanceWatermark(minWatermark);
// 向下游传播
output.emitWatermark(new Watermark(minWatermark));
}
}
┌─────────────────────────────────────────────────────────────────────────┐
│ Watermark 传播示意图 │
│ │
│ Source 1 (W=10) ──┐ │
│ ├──→ Union (W = min(10, 7) = 7) ──→ Window │
│ Source 2 (W=7) ──┘ │
│ │
│ 规则:多输入算子的 Watermark = 所有输入 Watermark 的最小值 │
│ 影响:某个 Source 延迟会拖慢整条链路的 Watermark 推进 │
│ 优化:Watermark 对齐(Flink 1.17+)可缓解此问题 │
└─────────────────────────────────────────────────────────────────────────┘
九、Flink CDC 与实时数仓
**实时数仓(Real-time Data Warehouse)**是近年来数据架构领域最热门的方向之一。传统数仓采用 T+1 的离线批处理模式,数据新鲜度只能达到"天级";而实时数仓借助 Flink CDC 技术,可以将数据新鲜度提升到"秒级"。本章将深入讲解 CDC 的工作原理、Flink CDC Connector 的使用方法、实时数仓的分层架构设计,并通过一个完整的端到端案例展示如何从零搭建实时数仓。
9.1 CDC 原理
CDC(Change Data Capture)是一种捕获数据库变更事件的技术,将数据库的 INSERT、UPDATE、DELETE 操作实时捕获并传递给下游系统。
┌─────────────────────────────────────────────────────────────────────────┐
│ CDC 工作原理 │
│ │
│ ┌────────────┐ ┌────────────────────┐ ┌────────────────────┐ │
│ │ MySQL │ │ CDC Connector │ │ Flink 任务 │ │
│ │ │ │ │ │ │ │
│ │ Binlog │ → │ 解析 Binlog │ → │ +I (Insert) │ │
│ │ ┌──────┐ │ │ 事件序列化 │ │ -U (Update前) │ │
│ │ │INSERT│ │ │ 发送变更事件 │ │ +U (Update后) │ │
│ │ │UPDATE│ │ │ │ │ -D (Delete) │ │
│ │ │DELETE│ │ │ │ │ │ │
│ │ └──────┘ │ └────────────────────┘ └────────────────────┘ │
│ └────────────┘ │
│ │
│ CDC 方式对比: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 基于查询(Query-based) 基于日志(Log-based) │ │
│ │ • 定时轮询 SELECT • 解析 Binlog/WAL │ │
│ │ • 延迟高(分钟级) • 延迟低(秒级) │ │
│ │ • 对源库有压力 • 对源库几乎无影响 │ │
│ │ • 无法捕获 DELETE • 可捕获所有变更 │ │
│ │ • 实现简单 • 实现复杂 │ │
│ │ │ │
│ │ Flink CDC 采用 Log-based 方式 ✅ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
9.2 Flink CDC Connector
Flink CDC 是一组专门用于变更数据捕获的 Source Connector,它基于 Debezium 引擎实现,支持从多种数据库中实时捕获数据变更。Flink CDC 2.0+ 引入了增量快照读取(Incremental Snapshot Reading)机制,解决了传统 CDC 全量读取阶段需要加锁的问题------它将全量数据按主键范围切分为多个 chunk 并行读取,既无锁又能保证 Exactly-Once 语义。
Flink CDC 支持多种数据源的变更捕获:
| 数据源 | Connector | 全量读取 | 增量读取 | Exactly-Once | 无锁读取 |
|---|---|---|---|---|---|
| MySQL | mysql-cdc | ✅ | ✅ | ✅ | ✅(增量快照) |
| PostgreSQL | postgres-cdc | ✅ | ✅ | ✅ | ✅ |
| MongoDB | mongodb-cdc | ✅ | ✅ | ✅ | ✅ |
| Oracle | oracle-cdc | ✅ | ✅ | ✅ | ❌ |
| SQL Server | sqlserver-cdc | ✅ | ✅ | ✅ | ❌ |
| TiDB | tidb-cdc | ✅ | ✅ | ✅ | ✅ |
| OceanBase | oceanbase-cdc | ✅ | ✅ | ✅ | ✅ |
sql
-- Flink CDC 使用示例
-- 创建 MySQL CDC 源表
CREATE TABLE products_source (
id INT,
name STRING,
price DECIMAL(10, 2),
category STRING,
update_time TIMESTAMP(3),
PRIMARY KEY (id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'mysql-host',
'port' = '3306',
'username' = 'flink_cdc',
'password' = '***',
'database-name' = 'shop',
'table-name' = 'products',
'server-time-zone' = 'Asia/Shanghai',
-- 增量快照读取(无锁、并行)
'scan.incremental.snapshot.enabled' = 'true',
'scan.incremental.snapshot.chunk.size' = '8096'
);
9.3 实时数仓架构
实时数仓的分层架构与离线数仓一脉相承(ODS→DWD→DWS→ADS),但每一层都使用 Flink 进行实时处理,中间层通常以 Kafka Topic 作为存储介质(而非 Hive 表)。数据从业务数据库经 Flink CDC 实时捕获进入 ODS 层,再通过 Flink SQL 逐层清洗、关联、聚合,最终写入 Elasticsearch/Redis/MySQL 等存储供 BI 看板和 API 查询。
┌─────────────────────────────────────────────────────────────────────────┐
│ Flink 实时数仓架构 │
│ │
│ ┌──────────────┐ │
│ │ 业务数据库 │ │
│ │ MySQL / PG │ ─── Flink CDC ───┐ │
│ └──────────────┘ │ │
│ ┌──────────────┐ ▼ │
│ │ 日志数据 │ ┌──────────┐ │
│ │ Kafka/文件 │ ──────────→ │ ODS │ 实时原始层 │
│ └──────────────┘ │ (Kafka) │ 保持源数据结构 │
│ ┌──────────────┐ └────┬─────┘ │
│ │ 第三方数据 │ │ │
│ │ API/文件 │ ─────────────────┘│ │
│ └──────────────┘ │ Flink SQL ETL │
│ ▼ │
│ ┌──────────┐ │
│ │ DWD │ 实时明细层 │
│ │ (Kafka) │ 清洗、维度关联 │
│ └────┬─────┘ │
│ │ Flink SQL 聚合 │
│ ▼ │
│ ┌──────────┐ │
│ │ DWS │ 实时汇总层 │
│ │ (Kafka/ │ 按主题轻度汇总 │
│ │ Iceberg)│ │
│ └────┬─────┘ │
│ │ Flink SQL 输出 │
│ ▼ │
│ ┌──────────┐ │
│ │ ADS │ 应用服务层 │
│ │ (ES/ │ 面向看板、API │
│ │ Redis/ │ │
│ │ MySQL) │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ BI 看板 / API │ │
│ │ Grafana / Superset │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
9.4 实战:端到端实时数仓
下面通过一个完整的实战案例,演示如何用纯 Flink SQL 搭建一个四层实时数仓:ODS 层通过 MySQL CDC 实时捕获订单和商品数据;DWD 层将订单与商品维表关联生成实时宽表;DWS 层按品类每分钟聚合统计 GMV 和买家数;ADS 层将结果写入 Elasticsearch 供 Grafana/Superset 实时看板展示。
sql
-- 实战:MySQL CDC → 实时宽表 → 实时聚合 → 实时看板
-- ========== ODS 层:CDC 源表 ==========
CREATE TABLE ods_orders (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
amount DECIMAL(10, 2),
status STRING,
create_time TIMESTAMP(3),
update_time TIMESTAMP(3),
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'mysql:3306',
'database-name' = 'ecommerce',
'table-name' = 'orders',
'username' = 'flink', 'password' = '***'
);
CREATE TABLE ods_products (
product_id BIGINT,
product_name STRING,
category STRING,
brand STRING,
price DECIMAL(10, 2),
PRIMARY KEY (product_id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'mysql:3306',
'database-name' = 'ecommerce',
'table-name' = 'products',
'username' = 'flink', 'password' = '***'
);
-- ========== DWD 层:实时宽表 ==========
CREATE TABLE dwd_order_detail (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
product_name STRING,
category STRING,
brand STRING,
amount DECIMAL(10, 2),
status STRING,
create_time TIMESTAMP(3),
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'dwd_order_detail',
'properties.bootstrap.servers' = 'kafka:9092',
'key.format' = 'json',
'value.format' = 'json'
);
INSERT INTO dwd_order_detail
SELECT
o.order_id, o.user_id, o.product_id,
p.product_name, p.category, p.brand,
o.amount, o.status, o.create_time
FROM ods_orders o
LEFT JOIN ods_products p ON o.product_id = p.product_id;
-- ========== DWS 层:实时汇总 ==========
CREATE TABLE dws_category_stats (
category STRING,
window_start TIMESTAMP(3),
window_end TIMESTAMP(3),
order_count BIGINT,
gmv DECIMAL(15, 2),
buyer_count BIGINT,
PRIMARY KEY (category, window_start) NOT ENFORCED
) WITH (
'connector' = 'upsert-kafka',
'topic' = 'dws_category_stats',
'properties.bootstrap.servers' = 'kafka:9092',
'key.format' = 'json',
'value.format' = 'json'
);
INSERT INTO dws_category_stats
SELECT
category,
window_start,
window_end,
COUNT(*) AS order_count,
SUM(amount) AS gmv,
COUNT(DISTINCT user_id) AS buyer_count
FROM TABLE(
TUMBLE(TABLE dwd_order_detail, DESCRIPTOR(create_time), INTERVAL '1' MINUTE)
)
WHERE status = 'PAID'
GROUP BY category, window_start, window_end;
-- ========== ADS 层:写入 Elasticsearch ==========
CREATE TABLE ads_realtime_dashboard (
category STRING,
window_start TIMESTAMP(3),
order_count BIGINT,
gmv DECIMAL(15, 2),
buyer_count BIGINT,
PRIMARY KEY (category, window_start) NOT ENFORCED
) WITH (
'connector' = 'elasticsearch-7',
'hosts' = 'http://es:9200',
'index' = 'realtime_dashboard'
);
INSERT INTO ads_realtime_dashboard
SELECT category, window_start, order_count, gmv, buyer_count
FROM dws_category_stats;
十、Flink 与 AI 大模型
随着大语言模型(LLM)和 AI 应用的爆发式增长,实时数据处理在 AI 生态中的地位愈发重要。Flink 凭借其低延迟、高吞吐、精确一次语义和强大的状态管理能力,正在从传统的实时数据处理引擎演变为 AI 应用的核心数据基础设施。无论是实时特征工程、在线推理、模型训练数据流水线,还是 RAG 知识库实时更新、模型监控与漂移检测,Flink 都扮演着不可替代的角色。本章将系统介绍 Flink 在 AI 大模型领域的典型应用场景。
10.1 概述:为什么 AI 需要 Flink
AI/LLM 应用并非孤立的模型推理过程------它们依赖大量实时、高质量、低延迟的数据供给。传统的批处理方式无法满足 AI 应用对数据时效性的要求,而 Flink 恰好填补了这一空白。
AI 应用的实时数据需求:
| 场景 | 数据需求 | 延迟要求 |
|---|---|---|
| 实时推荐 | 用户最新行为特征 | 毫秒 ~ 秒级 |
| 风控反欺诈 | 实时交易特征 + 模型推理 | 毫秒级 |
| RAG 知识增强 | 知识库实时更新 | 秒 ~ 分钟级 |
| 模型监控 | 预测结果与真实值对比 | 分钟级 |
| 训练数据生成 | 持续清洗、采样、标注 | 分钟 ~ 小时级 |
| Prompt 流式处理 | 请求审计、限流、路由 | 毫秒级 |
Flink 在 AI 数据栈中的定位:
┌────────────────────────────────────────────────────────────────┐
│ AI 应用层 │
│ 推荐系统 │ 智能客服 │ 内容生成 │ 风控系统 │ 搜索增强 │
└──────────────────────────┬─────────────────────────────────────┘
│
┌──────────────────────────▼─────────────────────────────────────┐
│ 模型服务层 │
│ LLM API │ TensorFlow Serving │ ONNX Runtime │ vLLM │
└──────────────────────────┬─────────────────────────────────────┘
│
┌──────────────────────────▼─────────────────────────────────────┐
│ ★ Flink 实时数据引擎层 ★ │
│ │
│ 特征工程 │ 数据清洗 │ 实时推理 │ 监控告警 │ 知识同步 │
└────────┬──────────┬──────────┬──────────┬──────────┬───────────┘
│ │ │ │ │
┌────▼───┐ ┌───▼────┐ ┌──▼───┐ ┌───▼───┐ ┌───▼────┐
│Feature │ │数据湖 │ │向量 │ │监控 │ │消息 │
│Store │ │对象存储│ │数据库│ │平台 │ │队列 │
└────────┘ └────────┘ └──────┘ └───────┘ └────────┘
从上图可以看到,Flink 处于 AI 应用与底层存储之间的核心枢纽位置,负责数据的实时流转、加工和分发。
10.2 实时特征工程(Real-time Feature Engineering)
特征工程是机器学习中最关键的环节之一------数据和特征决定了模型的上限,而算法只是在逼近这个上限。特征工程的核心任务是从原始数据中提取、构造、转换出对模型预测有价值的特征变量。
传统特征工程依赖离线批处理(如 Spark、Hive),计算结果往往有 T+1 的延迟。但在实时推荐、风控等场景下,用户的最新行为(如"刚刚浏览了某商品""过去 5 分钟连续下了 3 笔订单")对模型预测至关重要,这就需要实时特征工程。
离线特征 vs 实时特征对比:
| 对比维度 | 离线特征 | 实时特征 |
|---|---|---|
| 计算引擎 | Spark / Hive | Flink |
| 数据时效性 | T+1(天级别) | 秒 ~ 分钟级 |
| 典型特征 | 用户历史累计消费、注册天数 | 最近 10 分钟点击次数、实时购物车金额 |
| 存储方式 | Hive/数据湖 | Redis / Feature Store |
| 更新频率 | 每日一次 | 持续更新 |
| 适用场景 | 用户画像、长期趋势 | 实时推荐、实时风控 |
Flink 实时特征计算架构:
┌──────────┐ ┌─────────────────────────────────┐ ┌─────────────┐
│ Kafka │───▶│ Flink 特征计算 │───▶│ Feature │
│ 用户行为 │ │ │ │ Store │
│ 事件流 │ │ ┌───────────┐ ┌─────────────┐ │ │ (Redis/ │
└──────────┘ │ │ 滑动窗口 │ │ 特征拼接 │ │ │ Feast) │
│ │ 聚合统计 │─▶│ 归一化处理 │ │ └──────┬──────┘
│ └───────────┘ └─────────────┘ │ │
└─────────────────────────────────┘ ┌───────▼──────┐
│ 模型推理服务 │
│ (在线预测) │
└──────────────┘
Java 代码示例:实时用户特征计算
java
/**
* 实时用户特征计算:统计用户最近 10 分钟内的行为特征
* 包括点击次数、浏览商品种类数、平均停留时长等
*/
public class RealtimeFeatureEngineering {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 从 Kafka 消费用户行为事件
DataStream<UserBehavior> behaviorStream = env
.addSource(new FlinkKafkaConsumer<>(
"user-behavior",
new UserBehaviorSchema(),
kafkaProperties()))
.assignTimestampsAndWatermarks(
WatermarkStrategy.<UserBehavior>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, ts) -> event.getTimestamp()));
// 按用户 ID 分组,使用滑动窗口计算实时特征
DataStream<UserFeature> featureStream = behaviorStream
.keyBy(UserBehavior::getUserId)
.window(SlidingEventTimeWindows.of(
Time.minutes(10), // 窗口大小:10 分钟
Time.minutes(1))) // 滑动步长:1 分钟
.aggregate(new UserFeatureAggregator());
// 将特征写入 Redis(Feature Store)
featureStream.addSink(new RedisSink<>(redisConfig(), new UserFeatureRedisMapper()));
env.execute("Realtime Feature Engineering");
}
/**
* 用户特征聚合器:在窗口内累计计算多维特征
*/
public static class UserFeatureAggregator
implements AggregateFunction<UserBehavior, FeatureAccumulator, UserFeature> {
@Override
public FeatureAccumulator createAccumulator() {
return new FeatureAccumulator();
}
@Override
public FeatureAccumulator add(UserBehavior behavior, FeatureAccumulator acc) {
acc.clickCount++;
acc.viewedProducts.add(behavior.getProductId());
acc.totalDuration += behavior.getDuration();
acc.maxPrice = Math.max(acc.maxPrice, behavior.getPrice());
// 记录行为类型分布
acc.behaviorCounts.merge(behavior.getType(), 1L, Long::sum);
return acc;
}
@Override
public UserFeature getResult(FeatureAccumulator acc) {
return UserFeature.builder()
.clickCount(acc.clickCount)
.uniqueProductCount(acc.viewedProducts.size())
.avgDuration(acc.totalDuration / Math.max(acc.clickCount, 1))
.maxPrice(acc.maxPrice)
.buyClickRatio(computeRatio(acc.behaviorCounts, "buy", "click"))
.build();
}
@Override
public FeatureAccumulator merge(FeatureAccumulator a, FeatureAccumulator b) {
a.clickCount += b.clickCount;
a.viewedProducts.addAll(b.viewedProducts);
a.totalDuration += b.totalDuration;
a.maxPrice = Math.max(a.maxPrice, b.maxPrice);
b.behaviorCounts.forEach((k, v) -> a.behaviorCounts.merge(k, v, Long::sum));
return a;
}
}
}
10.3 在线模型推理(Online Model Serving)
在线模型推理是指将流式数据实时输入已训练好的模型进行预测,并将预测结果实时输出的过程。与传统的"攒一批数据再预测"不同,在线推理要求每条数据到达后立刻获得预测结果,延迟通常在毫秒到秒级。
典型的应用场景包括:实时推荐排序、交易反欺诈、动态定价、内容审核等。Flink 在此场景中充当数据预处理 + 推理调度的角色------它从消息队列消费原始数据,完成特征提取和拼接,然后调用外部模型服务进行推理,最后将结果写入下游系统。
Flink + 模型推理架构:
┌──────────┐ ┌──────────────────────────────────┐ ┌───────────┐
│ Kafka │───▶│ Flink 推理流水线 │───▶│ Kafka / │
│ 原始事件 │ │ │ │ Redis / │
└──────────┘ │ ┌────────┐ ┌──────────────────┐ │ │ 数据库 │
│ │特征提取│─▶│ AsyncFunction │ │ └───────────┘
│ │& 拼接 │ │ 异步调用模型服务 │ │
│ └────────┘ └────────┬─────────┘ │
│ │ │
└───────────────────────┼────────────┘
│
┌─────────▼──────────┐
│ 模型服务集群 │
│ TF Serving / ONNX │
│ Runtime / vLLM │
└────────────────────┘
这里的关键技术点是 AsyncFunction(异步 I/O)------模型推理通常需要调用外部 HTTP/gRPC 服务,如果使用同步调用,每条数据都要等待网络往返,吞吐量会大幅下降。Flink 的异步 I/O 机制允许同时发出多个推理请求,在等待响应时处理其他数据,从而大幅提升吞吐量。
Java 代码示例:Flink 异步调用模型推理服务
java
/**
* 使用 Flink AsyncFunction 异步调用模型推理服务
* 实现高吞吐的在线实时推理
*/
public class OnlineModelServingJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<RawEvent> eventStream = env.addSource(new FlinkKafkaConsumer<>(
"raw-events", new RawEventSchema(), kafkaProperties()));
// Step 1:特征提取
DataStream<FeatureVector> featureStream = eventStream
.keyBy(RawEvent::getUserId)
.process(new FeatureExtractionFunction());
// Step 2:异步调用模型推理服务
DataStream<PredictionResult> predictionStream = AsyncDataStream
.unorderedWait(
featureStream,
new ModelServingAsyncFunction(),
5000, // 超时时间 5 秒
TimeUnit.MILLISECONDS,
100); // 最大并发请求数
// Step 3:将预测结果写入下游
predictionStream.addSink(new FlinkKafkaProducer<>(
"prediction-results", new PredictionResultSchema(), kafkaProperties()));
env.execute("Online Model Serving");
}
/**
* 异步模型推理函数
* 使用 HTTP 异步客户端调用 TensorFlow Serving / ONNX Runtime
*/
public static class ModelServingAsyncFunction
extends RichAsyncFunction<FeatureVector, PredictionResult> {
private transient AsyncHttpClient httpClient;
@Override
public void open(Configuration parameters) {
// 初始化异步 HTTP 客户端
httpClient = Dsl.asyncHttpClient(Dsl.config()
.setMaxConnections(200)
.setRequestTimeout(3000));
}
@Override
public void asyncInvoke(FeatureVector feature, ResultFuture<PredictionResult> resultFuture) {
// 构建推理请求(JSON 格式)
String requestBody = buildInferenceRequest(feature);
// 异步发送 HTTP 请求到模型服务
httpClient.preparePost("http://model-service:8501/v1/models/fraud_model:predict")
.setHeader("Content-Type", "application/json")
.setBody(requestBody)
.execute()
.toCompletableFuture()
.thenAccept(response -> {
// 解析推理结果
PredictionResult result = parseResponse(response.getResponseBody(), feature);
resultFuture.complete(Collections.singleton(result));
})
.exceptionally(throwable -> {
// 推理失败时返回默认值(降级策略)
PredictionResult fallback = PredictionResult.defaultResult(feature);
resultFuture.complete(Collections.singleton(fallback));
return null;
});
}
@Override
public void timeout(FeatureVector feature, ResultFuture<PredictionResult> resultFuture) {
// 超时降级
resultFuture.complete(Collections.singleton(PredictionResult.timeoutResult(feature)));
}
@Override
public void close() {
if (httpClient != null) {
try { httpClient.close(); } catch (Exception ignored) {}
}
}
}
}
10.4 训练数据实时流水线(Training Data Pipeline)
机器学习模型的效果很大程度上取决于训练数据的质量和时效性。传统的训练数据准备依赖离线批处理,通常需要 T+1 甚至更长时间才能将新数据纳入训练集。在快速变化的业务场景中(如电商推荐、新闻 feed 排序),这种延迟意味着模型看到的永远是"过时的世界"。
训练数据实时流水线的核心思想是:将业务产生的原始数据通过 Flink 实时清洗、过滤、采样、标注,持续生成高质量的训练样本,送入数据湖或对象存储,供训练平台定期拉取。
训练数据实时流水线架构:
┌──────────┐ ┌────────────────────────────────────┐ ┌────────────┐
│ 业务DB │ │ Flink 训练数据流水线 │ │ 对象存储 │
│ / Kafka │───▶│ │───▶│ / 数据湖 │
│ / 日志 │ │ ┌──────┐ ┌──────┐ ┌────────────┐ │ │ (S3/HDFS) │
└──────────┘ │ │数据 │ │正负 │ │ 特征拼接 │ │ └─────┬──────┘
│ │清洗 │→│样本 │→│ & 格式转换 │ │ │
│ │过滤 │ │采样 │ │ │ │ ┌─────▼──────┐
│ └──────┘ └──────┘ └────────────┘ │ │ 训练平台 │
└────────────────────────────────────┘ │ (PyTorch/ │
│ TF/Ray) │
└────────────┘
Java 代码示例:训练样本实时生成
java
/**
* 训练数据实时流水线:从用户行为流中生成推荐模型训练样本
* 正样本:用户点击/购买的商品
* 负样本:曝光但未点击的商品(按比例采样)
*/
public class TrainingDataPipelineJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 消费用户行为日志(包含曝光、点击、购买等事件)
DataStream<UserBehavior> behaviorStream = env
.addSource(new FlinkKafkaConsumer<>(
"user-behavior-log",
new UserBehaviorSchema(),
kafkaProperties()));
// 按用户 + 商品分组,生成训练样本
DataStream<TrainingSample> sampleStream = behaviorStream
.keyBy(b -> b.getUserId() + "_" + b.getProductId())
.process(new TrainingSampleGenerator())
// 负采样:控制正负样本比例为 1:4
.filter(new NegativeSamplingFilter(4));
// 拼接用户特征和商品特征
DataStream<TrainingSample> enrichedStream = sampleStream
.keyBy(TrainingSample::getUserId)
.connect(featureBroadcastStream)
.process(new FeatureEnrichmentFunction());
// 以 Parquet 格式写入数据湖(按小时分区)
FileSink<TrainingSample> sink = FileSink
.forBulkFormat(
new Path("s3://ml-data/training-samples/"),
ParquetAvroWriters.forSpecificRecord(TrainingSample.class))
.withRollingPolicy(OnCheckpointRollingPolicy.build())
.withBucketAssigner(new DateTimeBucketAssigner<>("yyyy-MM-dd/HH"))
.build();
enrichedStream.sinkTo(sink);
env.execute("Training Data Pipeline");
}
/**
* 训练样本生成器:将用户行为转化为有标签的训练样本
*/
public static class TrainingSampleGenerator
extends KeyedProcessFunction<String, UserBehavior, TrainingSample> {
// 记录曝光事件,等待后续点击/购买事件来确定标签
private ValueState<UserBehavior> exposureState;
@Override
public void open(Configuration parameters) {
exposureState = getRuntimeContext().getState(
new ValueStateDescriptor<>("exposure", UserBehavior.class));
}
@Override
public void processElement(UserBehavior behavior, Context ctx, Collector<TrainingSample> out)
throws Exception {
if ("exposure".equals(behavior.getType())) {
// 记录曝光,注册 30 秒定时器(超时视为负样本)
exposureState.update(behavior);
ctx.timerService().registerEventTimeTimer(
behavior.getTimestamp() + 30_000L);
} else if ("click".equals(behavior.getType()) || "buy".equals(behavior.getType())) {
// 有点击/购买行为 → 正样本(label = 1)
UserBehavior exposure = exposureState.value();
if (exposure != null) {
out.collect(TrainingSample.of(exposure, 1));
exposureState.clear();
}
}
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<TrainingSample> out)
throws Exception {
// 曝光后 30 秒内无互动 → 负样本(label = 0)
UserBehavior exposure = exposureState.value();
if (exposure != null) {
out.collect(TrainingSample.of(exposure, 0));
exposureState.clear();
}
}
}
}
十一、技术选型对比
在实际项目中,选择哪个流处理框架往往是架构设计的第一步。Flink、Spark Streaming、Kafka Streams、Storm 各有所长,没有"银弹"式的最优解------选型必须结合具体场景的延迟要求、状态规模、团队技术栈、运维能力等因素综合考虑。本章通过详细的多维度对比和决策树,帮助你做出合理的技术选型。
11.1 Flink vs Spark Streaming
这是最常见的选型对比。核心区别在于处理模型:Flink 是真正的逐事件流处理,而 Spark Streaming(包括 Structured Streaming)本质上是微批处理。这一根本差异导致了延迟、状态管理、事件时间处理等方面的连锁差异。
| 维度 | Flink | Spark Streaming / Structured Streaming |
|---|---|---|
| 处理模型 | 真正的流处理(逐事件) | 微批处理(Mini-batch) |
| 延迟 | 毫秒级 | 秒级(100ms-数秒) |
| 语义保证 | 端到端 Exactly-Once | 微批内 Exactly-Once |
| 状态管理 | 原生支持,TB 级状态 | 有限状态支持 |
| 事件时间 | 原生 Watermark 机制 | 支持但受限于微批 |
| 窗口类型 | 丰富(会话窗口等) | 基础窗口 |
| CEP | 内置 CEP 库 | 无原生 CEP |
| SQL | Flink SQL(流批一体) | Spark SQL(批为主) |
| CDC | Flink CDC 生态丰富 | 需第三方工具 |
| 批处理 | 支持但非主场 | 非常强大 |
| 机器学习 | Flink ML(发展中) | MLlib(成熟) |
| 社区生态 | 快速增长 | 非常成熟 |
| 学习曲线 | 较陡(概念多) | 较平缓 |
11.2 Flink vs Kafka Streams
Kafka Streams 最大的特点是它是一个**库(Library)**而不是框架------你只需要在应用中引入一个 JAR 依赖,不需要部署独立的集群。这种轻量级的部署方式非常适合微服务场景,但也限制了它的状态规模和功能丰富度。
| 维度 | Flink | Kafka Streams |
|---|---|---|
| 部署方式 | 独立集群 | 嵌入应用(Library) |
| 运维复杂度 | 高(需管理集群) | 低(随应用部署) |
| 状态大小 | TB 级 | GB 级(受限于本地磁盘) |
| 数据源 | 多样(Kafka/JDBC/文件等) | 仅 Kafka |
| SQL 支持 | Flink SQL(完善) | KSQL(较简单) |
| Exactly-Once | 端到端支持 | Kafka 事务支持 |
| 窗口/CEP | 丰富 | 基础 |
| 适用场景 | 大规模、复杂场景 | 轻量级、Kafka 生态内 |
11.3 Flink vs Storm
Storm 是流处理领域的先驱,由 Twitter 开发并开源。它提供了真正的逐事件处理和毫秒级延迟,但缺乏内置的状态管理和 Exactly-Once 语义。随着 Flink 的成熟,Storm 已逐渐退出历史舞台------Apache Storm 社区活跃度大幅下降,许多公司已完成从 Storm 到 Flink 的迁移。
| 维度 | Flink | Storm |
|---|---|---|
| 处理模型 | 流处理 + 有状态 | 流处理 + 无状态 |
| 语义 | Exactly-Once | At-Least-Once(Trident: Exactly-Once) |
| 状态 | 内置 TB 级状态 | 无内置状态 |
| 吞吐 | 高 | 低 |
| 延迟 | 低(毫秒级) | 低(毫秒级) |
| API | 丰富(DataStream/SQL/CEP) | 基础(Spout/Bolt) |
| 容错 | Checkpoint(轻量) | ACK 机制(开销大) |
| 现状 | 活跃发展 | 逐渐停止维护 |
11.4 选型决策树
┌─────────────────────────────────────────────────────────────────────────┐
│ 流处理技术选型决策树 │
│ │
│ 你的核心需求是什么? │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 纯流处理 流批一体 轻量级流处理 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 需要大状态? ┌→ Flink ←┐ 数据源仅 Kafka? │
│ │ │ │ │ │ │ │
│ 是 否 │ │ 是 否 │
│ │ │ │ │ │ │ │
│ ▼ ▼ │ │ ▼ ▼ │
│ Flink 延迟 │ │ Kafka Flink │
│ 要求? │ │ Streams │
│ │ │ │ │ │
│ <1s >1s │ │ │
│ │ │ │ │ │
│ ▼ ▼ │ │ │
│ Flink Spark│ │ │
│ Streaming │ │
│ │ │
│ 最终建议: │
│ • 实时计算首选 → Flink │
│ • 批处理为主、兼顾流 → Spark │
│ • Kafka 生态内轻量流 → Kafka Streams │
│ • 遗留系统迁移 → Storm → Flink │
└─────────────────────────────────────────────────────────────────────────┘
十二、Flink 新特性
Flink 社区保持着旺盛的迭代速度,每个大版本都会带来重要的功能增强。本章梳理了 Flink 1.17 到 2.0 版本中最值得关注的五大新特性,它们代表了 Flink 的演进方向:流批一体的全面成熟、Checkpoint 机制的根本性改进、以及开发体验的大幅提升。
12.1 流批一体增强
从 Flink 1.12 开始,DataStream API 支持通过 RuntimeExecutionMode 切换流/批模式,到 Flink 2.0 流批一体全面成熟。
| 版本 | 流批一体里程碑 |
|---|---|
| 1.12 | DataStream API 支持 BATCH 模式 |
| 1.14 | 统一 Source/Sink API |
| 1.16 | 批模式 Hybrid Shuffle 优化 |
| 1.17 | 批模式哈希聚合、排序优化 |
| 1.18 | Generalized Incremental Checkpoint |
| 2.0 | 全面统一 API、存算分离 |
12.2 Generalized Incremental Checkpoint(Flink 1.18+)
这是 Flink 1.18 中最重要的改进之一。传统的增量 Checkpoint 只有 RocksDB 状态后端才支持(因为它依赖 RocksDB 的 SST 文件差异机制)。Generalized Incremental Checkpoint 引入了通用的 Changelog (变更日志)机制:在两次 Checkpoint 之间,只记录状态的变更操作(而非完整状态),上传到持久化存储。这使得所有状态后端(包括 HashMap)都能享受增量 Checkpoint 的优势:更快的 Checkpoint 速度、更短的 Checkpoint 间隔、更快的故障恢复。
┌─────────────────────────────────────────────────────────────────────────┐
│ Generalized Incremental Checkpoint │
│ │
│ 传统增量 Checkpoint(仅 RocksDB): │
│ CP1: [全量快照] │
│ CP2: [增量 Δ1] → 恢复需要:CP1 + Δ1 │
│ CP3: [增量 Δ2] → 恢复需要:CP1 + Δ1 + Δ2 │
│ 问题:仅 RocksDB 支持,依赖 SST 文件 │
│ │
│ Generalized 增量 Checkpoint(所有状态后端通用): │
│ CP1: [全量快照] │
│ CP2: [变更日志 Changelog] → 恢复:CP1 + Changelog │
│ CP3: [变更日志 Changelog] → 恢复:CP1 + Changelog │
│ 优势: │
│ • 所有状态后端通用(HashMap + RocksDB) │
│ • Checkpoint 更快(只上传 Changelog) │
│ • 更短的 Checkpoint 间隔(秒级) │
│ • 更快的故障恢复 │
└─────────────────────────────────────────────────────────────────────────┘
yaml
# 启用 Changelog State Backend
state.changelog.enabled: true
state.changelog.storage: filesystem
dstl.dfs.base-path: hdfs:///flink/changelog
12.3 Watermark 对齐(Flink 1.17+)
在多 Source 并行读取的场景中,不同 Source 的数据到达速度可能差异很大。如果某个 Source 处理速度远快于其他 Source,它产生的 Watermark 会大幅领先,但由于多输入算子取最小 Watermark 的规则,快速 Source 产生的数据会在状态中大量积压(因为窗口无法触发),导致状态膨胀。Watermark 对齐机制通过暂停"跑得太快"的 Source 来解决这个问题。
解决多 Source 场景下 Watermark 推进不均匀导致的状态膨胀问题。
java
// 启用 Watermark 对齐
WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, ts) -> event.getTimestamp())
// 当某个 Source 的 Watermark 比其他 Source 快超过 5 秒时,暂停该 Source
.withWatermarkAlignment("alignment-group", Duration.ofSeconds(5));
12.4 Process Function v2(Flink 2.0)
Flink 2.0 对 Process Function API 进行了重新设计,大幅简化了有状态处理的开发体验。在旧版 API 中,定义状态需要在 open() 方法中通过 getRuntimeContext().getState(descriptor) 注册,代码冗长且分散。新版 API 引入了声明式状态定义------通过注解和声明式字段直接定义状态,减少了大量样板代码。
Flink 2.0 引入全新的 Process Function API,简化有状态处理的开发体验。
java
// Process Function v2(Flink 2.0 预览)
// 声明式状态定义,减少样板代码
@ProcessFunction
public class WordCountFunction {
@StateDeclaration(name = "count")
ValueStateDeclaration<Integer> countState = StateDeclarations.valueState("count", Integer.class);
@OnRecord
public void processElement(String word, Context ctx, Collector<Tuple2<String, Integer>> out)
throws Exception {
ValueState<Integer> count = ctx.getState(countState);
int newCount = (count.value() == null ? 0 : count.value()) + 1;
count.update(newCount);
out.collect(Tuple2.of(word, newCount));
}
}
12.5 Materialized Table(Flink 2.0)
**物化表(Materialized Table)**是 Flink 2.0 在 SQL 层面的重大创新。它允许用户用标准 SQL 定义一张"结果表",并指定数据的"新鲜度"要求(如 FRESHNESS = INTERVAL '1' MINUTE),Flink 会自动决定使用流模式还是批模式来满足这个新鲜度目标。这大大简化了实时数仓的开发------用户不需要关心底层是用流还是批来实现,只需要声明"我希望数据延迟不超过 1 分钟"。
sql
-- Materialized Table:自动管理流批刷新的物化表
CREATE MATERIALIZED TABLE category_daily_stats
PARTITIONED BY (dt)
WITH (
'partition.fields.dt.date-formatter' = 'yyyy-MM-dd'
)
FRESHNESS = INTERVAL '1' MINUTE -- 数据新鲜度要求
AS SELECT
DATE_FORMAT(order_time, 'yyyy-MM-dd') AS dt,
category,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_FORMAT(order_time, 'yyyy-MM-dd'), category;
-- Flink 自动决定使用流模式还是批模式来满足新鲜度要求
十三、总结
经过前面十一章的深入探讨,我们已经从多个维度全面了解了 Apache Flink。作为当前流计算领域的事实标准,Flink 的成功不是偶然的------它在架构设计上做出了正确的选择(流优先、有状态、事件时间),并通过持续迭代不断完善生态和开发体验。
13.1 Flink 核心优势
- 真正的流处理:逐事件处理,毫秒级延迟,非微批模拟
- 有状态计算:内置 TB 级分布式状态管理,Exactly-Once 保证
- 事件时间处理:原生 Watermark 机制,正确处理乱序和迟到数据
- 流批一体:同一套 API 和引擎,消除 Lambda 架构的双重负担
- Flink SQL:声明式 API,降低开发门槛,自动优化
- CDC 生态:与 Flink CDC 深度集成,实时数仓标配
- 强容错:基于 Checkpoint 的快速恢复,生产级稳定性
13.2 学习路线建议
Flink 的学习曲线相对较陡,概念繁多。建议按照以下路线循序渐进------先掌握 DataStream API 的基本用法和 Flink Web UI 的操作,再深入窗口、时间语义和状态管理,然后学习 Flink SQL 和 CDC 实时数仓,最后通过阅读源码和极限调优成为专家。
入门 ──────→ 进阶 ──────→ 深入 ──────→ 专家
│ │ │ │
│ │ │ └─ 源码阅读(调度/Checkpoint/网络栈)
│ │ │ └─ 自定义 Connector 开发
│ │ │ └─ 性能极限调优
│ │ │
│ │ └─ RocksDB 状态后端调优
│ │ └─ Checkpoint / Savepoint 深入
│ │ └─ 反压诊断与处理
│ │ └─ Flink CDC 实时数仓
│ │
│ └─ Flink SQL / Table API
│ └─ 窗口机制与时间语义
│ └─ 状态管理与容错
│ └─ 连接器使用(Kafka/JDBC/ES)
│
└─ DataStream API 基础
└─ 环境搭建与 WordCount
└─ 基本算子(map/filter/keyBy/reduce)
└─ Flink Web UI 使用
13.3 最佳实践清单
- ✅ 优先使用 Flink SQL,复杂场景再用 DataStream API
- ✅ 生产环境使用 RocksDB 状态后端 + 增量 Checkpoint
- ✅ RocksDB 数据目录使用 SSD
- ✅ 设置合理的 Checkpoint 间隔(30s-5min)
- ✅ 使用 Event Time + Watermark 保证结果正确性
- ✅ 窗口聚合使用 AggregateFunction 而非 ProcessWindowFunction
- ✅ 配置 State TTL 清理过期状态
- ✅ Source 并行度与 Kafka 分区数对齐
- ✅ 监控反压、Checkpoint 耗时、消费延迟
- ✅ 使用 Savepoint 进行版本升级和扩缩容
- ❌ 避免在算子中进行阻塞 IO 操作
- ❌ 避免使用 ProcessWindowFunction 处理大窗口
- ❌ 避免全局窗口不设触发器
- ❌ 避免 Kryo 序列化(注册 POJO 或使用 Flink 类型)
附录:环境搭建
工欲善其事,必先利其器。本附录提供三种 Flink 开发环境的搭建指南:本地 Maven 开发环境(适合日常开发调试)、Docker 环境(适合快速体验和功能验证)、以及集群部署(适合生产环境)。
A.1 本地开发环境(Maven)
xml
<!-- pom.xml 核心依赖 -->
<properties>
<flink.version>1.19.0</flink.version>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- Flink Core -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- Flink Clients(本地执行) -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- Flink SQL -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java-bridge</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-planner-loader</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- Kafka Connector -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka</artifactId>
<version>3.1.0-1.19</version>
</dependency>
<!-- JDBC Connector -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc</artifactId>
<version>3.1.2-1.18</version>
</dependency>
<!-- Flink CDC(MySQL) -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-mysql-cdc</artifactId>
<version>3.1.0</version>
</dependency>
<!-- RocksDB State Backend -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- CEP -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>
A.2 Docker 环境
yaml
# docker-compose.yml
version: '3.8'
services:
jobmanager:
image: flink:1.19-java11
ports:
- "8081:8081" # Web UI
command: jobmanager
environment:
- |
FLINK_PROPERTIES=
jobmanager.rpc.address: jobmanager
jobmanager.memory.process.size: 2048m
state.backend: rocksdb
state.checkpoints.dir: file:///tmp/flink-checkpoints
volumes:
- flink-checkpoints:/tmp/flink-checkpoints
taskmanager:
image: flink:1.19-java11
depends_on:
- jobmanager
command: taskmanager
deploy:
replicas: 2
environment:
- |
FLINK_PROPERTIES=
jobmanager.rpc.address: jobmanager
taskmanager.numberOfTaskSlots: 4
taskmanager.memory.process.size: 4096m
taskmanager.memory.managed.fraction: 0.4
kafka:
image: confluentinc/cp-kafka:7.5.0
ports:
- "9092:9092"
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
CLUSTER_ID: 'flink-demo-cluster'
volumes:
flink-checkpoints:
bash
# 启动环境
docker-compose up -d
# 访问 Flink Web UI
open http://localhost:8081
# 提交作业
docker exec -it $(docker ps -q -f name=jobmanager) \
flink run /opt/flink/examples/streaming/WordCount.jar
# 查看作业状态
docker exec -it $(docker ps -q -f name=jobmanager) flink list
# 创建 Savepoint
docker exec -it $(docker ps -q -f name=jobmanager) \
flink savepoint <job-id> file:///tmp/flink-savepoints
A.3 集群部署
bash
# Standalone 模式部署
# 1. 下载 Flink
wget https://dlcdn.apache.org/flink/flink-1.19.0/flink-1.19.0-bin-scala_2.12.tgz
tar -xzf flink-1.19.0-bin-scala_2.12.tgz
cd flink-1.19.0
# 2. 配置 flink-conf.yaml
cat >> conf/flink-conf.yaml << 'EOF'
jobmanager.rpc.address: master-node
jobmanager.memory.process.size: 4096m
taskmanager.memory.process.size: 8192m
taskmanager.numberOfTaskSlots: 4
state.backend: rocksdb
state.backend.incremental: true
state.checkpoints.dir: hdfs:///flink/checkpoints
state.savepoints.dir: hdfs:///flink/savepoints
EOF
# 3. 配置 workers
echo "worker-node-1" > conf/workers
echo "worker-node-2" >> conf/workers
echo "worker-node-3" >> conf/workers
# 4. 启动集群
bin/start-cluster.sh
# 5. 提交作业
bin/flink run -d -p 16 \
-c com.example.MyFlinkJob \
my-flink-job.jar
# 6. 查看作业
bin/flink list
# 7. 停止集群
bin/stop-cluster.sh
A.4 常用术语表
| 术语 | 英文 | 说明 |
|---|---|---|
| 有界流 | Bounded Stream | 有限数据集,批处理场景 |
| 无界流 | Unbounded Stream | 无限数据流,流处理场景 |
| 算子 | Operator | 数据处理的基本单元 |
| 并行度 | Parallelism | 算子的并行实例数 |
| 水位线 | Watermark | 表示事件时间进度的标记 |
| 检查点 | Checkpoint | 分布式状态一致性快照 |
| 保存点 | Savepoint | 手动触发的完整状态快照 |
| 算子链 | Operator Chain | 多个算子合并为一个 Task |
| 任务槽 | Task Slot | TaskManager 中的资源隔离单元 |
| 反压 | Backpressure | 下游处理不过来时的压力反馈 |
| 状态后端 | State Backend | 状态的存储和访问方式 |
| 屏障 | Barrier | Checkpoint 中的流标记 |
| 侧输出 | Side Output | 将数据输出到额外的流 |
| 变更数据捕获 | CDC | Change Data Capture |
| 动态表 | Dynamic Table | Flink SQL 中流的表示形式 |