基础筑基 |第 1--4 周
涵盖章节:Ch01 大数据生态与流处理思维 → Ch02 Flink 架构原理 → Ch03 环境搭建 → Ch04 时间语义与 Watermark
Ch01 · 大数据生态与流处理思维
1.1 为什么需要大数据处理?
传统数据库(MySQL、PostgreSQL)在单机数据量超过百亿条、并发写入超过万级时,性能会急剧下降。大数据技术栈的核心目标是:
- 水平扩展:通过增加机器线性提升处理能力
- 容错性:单节点故障不影响整体任务
- 高吞吐:处理 TB 乃至 PB 级数据
1.2 批处理 vs 流处理
| 维度 | 批处理(Batch) | 流处理(Streaming) |
|---|---|---|
| 数据特征 | 有界(Bounded),数据是静止的 | 无界(Unbounded),数据持续流入 |
| 触发方式 | 手动或定时触发 | 事件驱动,持续运行 |
| 延迟 | 分钟级~小时级 | 毫秒级~秒级 |
| 代表框架 | MapReduce、Hive | Flink、Kafka Streams |
| 典型场景 | 离线报表、模型训练 | 实时监控、风控、推荐 |
一句话记住:批处理是"先攒数据,再一起算";流处理是"数据来一条,处理一条"。
1.3 大数据架构演进
大数据架构经历了三个主要阶段:
Lambda 架构(2011年,Nathan Marz 提出)
同时维护批处理层(Batch Layer)和速度层(Speed Layer),用批结果修正流结果,但维护两套代码成本高。
Kappa 架构(2014年,Jay Kreps 提出)
只保留流处理层,通过重放历史数据替代批处理,简化架构,但对流处理框架要求极高。
流批一体(现代,Flink 主导)
一个框架同时支持高效的批处理和流处理,Flink 是目前最成熟的实现。
Lambda 架构 数据源 批处理层 速度层 合并服务层 ⚠ 双套代码,维护成本高 Kappa 架构 数据源 + 消息队列 统一流处理层 服务层 ✓ 单套代码,重放历史数据 流批一体(Flink) 有界/无界 数据源 Flink Runtime(统一引擎) 批结果 流结果 ★ 一套代码,同时支持批和流
1.4 Flink 在大数据生态中的位置
数据源层 → 传输层 → 计算层 → 存储/服务层
MySQL Kafka Apache Flink Elasticsearch
日志文件 Pulsar (批+流统一) Redis
IoT 设备 RabbitMQ (本课程主角) HBase / HDFS
埋点数据 数据大屏
1.5 Flink vs Spark Streaming 对比
| 对比项 | Apache Flink | Spark Streaming |
|---|---|---|
| 处理模型 | 真正的流处理(逐条) | 微批处理(小批次) |
| 延迟 | 毫秒级 | 秒级(批次间隔) |
| 状态管理 | 原生支持,极其强大 | 较弱 |
| Exactly-Once | 原生支持 | 需额外配置 |
| 时间语义 | 事件时间原生支持 | 支持但不如Flink完善 |
| SQL 支持 | Flink SQL(非常完善) | Spark SQL(以批为主) |
| 适用场景 | 低延迟、复杂状态计算 | 大规模批处理、ML |
Ch02 · Flink 架构原理与核心组件
2.1 Flink 整体架构
Flink 采用经典的 主从架构(Master-Worker),主节点负责调度,工作节点负责执行。
Client 提交 Job 生成 JobGraph JobManager(主节点) Dispatcher 接受 Job 提交 JobMaster 管理单个 Job ResourceManager 管理 TaskManager Slot 资源 Checkpoint Coordinator 触发和协调全局 Checkpoint TaskManager 1 Slot 1 Slot 2 Slot 3 执行 Task(SubTask) 本地状态存储 TaskManager 2 Slot 1 Slot 2 Slot 3 执行 Task(SubTask) 本地状态存储 状态后端(HDFS/S3) 提交 分配Task
2.2 核心概念详解
JobManager(主节点) --- 大脑,负责:
- Dispatcher:接收客户端提交的作业
- JobMaster:每个 Job 对应一个,负责该 Job 的生命周期管理、Task 调度
- ResourceManager:管理 TaskManager 的 Slot 资源
- Checkpoint Coordinator:触发和协调全局一致性快照
TaskManager(工作节点) --- 执行单元,负责:
- 接收并执行 JobManager 分配的 Task(SubTask)
- 管理本地状态
- 与相邻 TaskManager 进行数据交换(网络传输)
Slot(资源单位)
Slot 是 TaskManager 中最小的资源分配单位。每个 Slot 有固定的内存份额,但 CPU 不做硬性隔离。
一个 TaskManager 有 3 个 Slot
→ 最多并行运行 3 个 SubTask
→ 多个 SubTask 可以共享同一个 Slot(通过 Slot Sharing)
2.3 Job 提交与执行流程
① 用户代码 DataStream API / Flink SQL ② StreamGraph 逻辑执行图 算子拓扑结构 ③ JobGraph 客户端生成 合并可链接算子 ④ ExecutionGraph JobManager 生成 并行化展开 ⑤ 物理执行 Task 部署至 TaskManager Slot Client 端 Client 端提交 JM 侧处理 TM 执行
2.4 算子链(Operator Chaining)
Flink 会将满足条件的相邻算子合并成一个 Task (即算子链),在同一线程内执行,避免跨网络的序列化开销。
触发条件:
- 上下游算子并行度相同
- 下游算子只有一个输入源
- 没有跨 Slot 边界
java
// 手动禁用算子链(调试时使用)
env.disableOperatorChaining();
// 在特定算子上断链
stream.map(...).startNewChain()
Ch03 · 环境搭建与第一个 Flink 程序
3.1 前置环境要求
| 软件 | 版本要求 | 说明 |
|---|---|---|
| JDK | 11 或 17(推荐) | Flink 1.17+ 推荐 JDK 11 |
| Maven | 3.6+ | 项目构建工具 |
| IDE | IntelliJ IDEA | 推荐,Scala 插件可选 |
| Flink | 1.18.x(当前最新稳定) | 本地开发模式,无需安装 |
注意:Flink 本地开发模式(Local Mode)不需要单独安装 Flink 服务,Maven 依赖会包含所有运行时组件。
3.2 Maven 项目创建
创建 pom.xml,引入 Flink 依赖:
xml
<properties>
<flink.version>1.18.1</flink.version>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- Flink 流处理核心 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- Flink 客户端(本地运行所需) -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- 日志框架(必须,否则 Flink 报错) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
3.3 第一个程序:WordCount(批处理模式)
java
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 {
// 1. 创建执行环境(本地模式,自动使用可用 CPU 核数)
StreamExecutionEnvironment env =
StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 创建数据源(内存数据)
DataStream<String> textStream = env.fromElements(
"hello flink hello",
"hello world",
"flink is awesome"
);
// 3. 转换:切割 → 分组 → 求和
DataStream<Tuple2<String, Integer>> wordCount = textStream
.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
@Override
public void flatMap(String line, Collector<Tuple2<String, Integer>> out) {
for (String word : line.split("\\s+")) {
out.collect(Tuple2.of(word, 1)); // 每个词输出 (word, 1)
}
}
})
.keyBy(tuple -> tuple.f0) // 按单词分组
.sum(1); // 对第二个字段求和
// 4. 输出结果
wordCount.print();
// 5. 触发执行(Flink 是懒执行,必须调用 execute)
env.execute("WordCount Job");
}
}
程序骨架理解(每个 Flink 程序必须包含):
① 创建 StreamExecutionEnvironment ← 入口,类似 Spark 的 SparkContext
② 添加 Source ← 数据从哪来
③ Transformations(链式操作) ← 中间如何处理
④ 添加 Sink(或 print) ← 结果去哪里
⑤ env.execute("任务名") ← 必须!否则什么都不执行
3.4 Lambda 写法简化(Java 8+)
java
// 等价写法,更简洁
DataStream<Tuple2<String, Integer>> wordCount = textStream
.flatMap((String line, Collector<Tuple2<String, Integer>> out) -> {
for (String word : line.split("\\s+")) {
out.collect(Tuple2.of(word, 1));
}
})
.returns(Types.TUPLE(Types.STRING, Types.INT)) // Lambda 需要手动指定返回类型
.keyBy(t -> t.f0)
.sum(1);
为什么 Lambda 需要
.returns()?Java 的类型擦除机制导致 Flink 无法推断泛型类型,必须手动声明,否则运行时报错。
3.5 预期输出
运行后控制台输出类似(顺序可能不同):
(hello,3)
(flink,2)
(world,1)
(is,1)
(awesome,1)
Ch04 · 时间语义与 Watermark 机制
这是 Flink 中最重要、最容易混淆的概念,请反复理解。
4.1 三种时间语义
← 数据流时间轴 → 事件A t=09:00:01 事件B t=09:00:05 事件C(迟到) 实际t=09:00:03 事件D t=09:00:08 事件时间(Event Time) ★ 推荐使用 • 数据产生时自带的时间戳 • 事件C虽然迟到,但按 09:00:03 处理(正确!) • 结果可重现,语义最准确 ✓ 用于生产环境 处理时间(Processing Time) 最简单 • 数据到达算子时的系统时间 • 事件C按实际到达时间处理 • 不需要 Watermark • 结果不可重现,受系统影响 △ 用于对准确性要求不高的场景 摄入时间(Ingestion Time) 折中方案 • 数据进入 Flink Source 的时间 • 介于两者之间 • 自动分配时间戳 • 现实中较少使用 △ 了解即可
java
// 设置时间语义(Flink 1.12+ 默认已是 Event Time,无需额外设置)
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 默认就是 EventTime,以下两行仅作说明
4.2 为什么需要 Watermark?
在实际网络环境中,数据到达的顺序不等于事件发生的顺序:
- 网络抖动、消息队列分区乱序等原因导致事件迟到
- 如果一直等待迟到数据,窗口永远无法触发计算
- 如果不等待,则会漏掉迟到的数据
Watermark 的本质:一个随数据流传递的特殊标记,含义是:
"我承诺,时间戳 ≤ Watermark 值的数据已经全部到达,可以触发计算了。"
4.3 Watermark 工作原理(核心!)
Watermark 触发窗口计算示意(允许最大乱序 2秒,窗口大小 5秒) time :00 :01 :02 :03 :04 :05 t=:00 事件1 t=:02 事件2 t=:01 事件3(迟到) t=:04 事件4 t=:06 事件5 →触发! WM = max(4) - 2 = 2 → WM=:02 WM = max(6) - 2 = 4 → WM=:04 窗口 [00:00, 00:05) → Watermark ≥ 5 时触发 当 WM 推进到 5 时(即事件时间戳 ≥ 7,减去延迟2),窗口关闭并触发计算 Watermark 计算公式 Watermark(t) = max(已观察到的事件时间戳) - 允许的最大乱序时间 窗口触发条件:Watermark ≥ 窗口结束时间
4.4 代码实现
方式一:固定延迟 Watermark(最常用)
java
DataStream<MyEvent> stream = env.addSource(source);
DataStream<MyEvent> withWatermarks = stream
.assignTimestampsAndWatermarks(
WatermarkStrategy
// 允许最大 3 秒的乱序
.<MyEvent>forBoundedOutOfOrderness(Duration.ofSeconds(3))
// 告诉 Flink 如何从事件中提取时间戳
.withTimestampAssigner((event, recordTimestamp) -> event.getTimestamp())
);
方式二:自定义 Watermark 策略
java
WatermarkStrategy<MyEvent> strategy = new WatermarkStrategy<MyEvent>() {
@Override
public WatermarkGenerator<MyEvent> createWatermarkGenerator(
WatermarkGeneratorSupplier.Context context) {
return new WatermarkGenerator<MyEvent>() {
private long maxTimestamp = Long.MIN_VALUE;
private final long maxOutOfOrderness = 3000L; // 3秒
@Override
public void onEvent(MyEvent event, long eventTimestamp,
WatermarkOutput output) {
// 每条数据到来时更新最大时间戳
maxTimestamp = Math.max(maxTimestamp, eventTimestamp);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 周期性发出 Watermark(默认200ms一次)
output.emitWatermark(new Watermark(maxTimestamp - maxOutOfOrderness - 1));
}
};
}
};
配置 Watermark 发出间隔(默认200ms):
java
env.getConfig().setAutoWatermarkInterval(200); // 毫秒
4.5 处理真正迟到的数据
即使设置了 Watermark,还是可能有极端迟到的数据(超过允许的乱序时间)。Flink 提供了三层处理机制:
java
DataStream<MyEvent> result = withWatermarks
.keyBy(e -> e.getKey())
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
// 第二层:窗口关闭后允许继续等待 30 秒
.allowedLateness(Time.seconds(30))
// 第三层:超过 allowedLateness 的极端迟到数据,输出到侧流
.sideOutputLateData(lateOutputTag)
.sum("value");
// 获取极端迟到数据
DataStream<MyEvent> lateData = result.getSideOutput(lateOutputTag);
第一层 Watermark 延迟 允许一定程度乱序 forBoundedOutOfOrderness 正常乱序数据 → 正确处理 → 第二层 allowedLateness 窗口关闭后继续等待 迟到数据触发窗口更新 轻度迟到数据 → 重算窗口 → 第三层 SideOutput 极端迟到数据 导流至侧输出流 极端迟到 → 侧流处理
4.6 本章知识点总结
| 概念 | 核心要点 |
|---|---|
| Event Time | 事件产生时的时间戳,最准确,生产首选 |
| Processing Time | 系统当前时间,最简单,结果不可重现 |
| Watermark | 用于推进事件时间,解决乱序问题的核心机制 |
| Watermark 公式 | max_timestamp - max_out_of_orderness |
| 窗口触发条件 | Watermark ≥ 窗口结束时间 |
| allowedLateness | 窗口关闭后继续接受迟到数据的等待时间 |
| SideOutput | 处理超过 allowedLateness 的极端迟到数据 |
Phase 1 · 阶段检验
完成本阶段学习后,你应该能够:
- 清晰说出流处理和批处理的核心差异,以及 Flink 解决的核心问题
- 徒手画出 Flink JobManager / TaskManager / Slot 的关系图
- 解释 Slot 和并行度的关系(一个 Task 需要多少 Slot?)
- 独立搭建 Flink Maven 项目并运行 WordCount
- 解释 Watermark 的推进逻辑,说清楚窗口何时触发
下一章:Phase 2 · Ch05 Source 与 Sink:数据的入口与出口