3. Flink基础:架构、运行模式与DataStream API入门
好,咱们进入第三章。这一章我打算把Flink的底子给你讲透。说白了,Flink就是实时计算领域的一匹黑马,现在做实时指标体系建设,你绕不开它。我当年从Storm迁移到Flink的时候,第一感觉就是——这玩意儿的设计思路真清爽。
3.1 Flink架构:核心组件与角色
先看架构。Flink的架构其实不复杂,就两个大块:JobManager 和 TaskManager。你可以把JobManager想象成项目的总指挥,TaskManager就是干活的工人。
核心角色速览:
- JobManager:负责任务调度、检查点协调、故障恢复。它里面还有个ResourceManager,专门管资源分配。
- TaskManager:真正执行计算的节点。每个TaskManager有若干个Task Slot,Slot就是计算资源的最小单位。
- Dispatcher:提供REST接口,接收作业提交。嗯,这个组件在早期版本里叫JobManager的Web前端,后来独立出来了。
我记得刚接触Flink时,有个概念让我纠结了好一阵——Task Slot 和 并行度 到底啥关系?
简单说:Slot是TaskManager上的固定资源槽位,每个Slot可以运行一个SubTask。并行度决定了一个算子有几个SubTask。如果你有2个TaskManager,每个有2个Slot,并行度设为4,那刚好占满。但如果并行度设为5,就会有一个SubTask排队等待。
我的经验: 生产环境中,我习惯把Slot数设成与CPU核数一致。比如机器是16核,我就设16个Slot。这样每个Slot独占一个线程,避免资源争抢。
下面这张图,把Flink的运行时架构画清楚了:
3.2 Flink运行模式:三种部署方式
Flink支持三种运行模式,说白了就是看你把作业提交给谁管。
| 模式 | 适用场景 | 特点 |
|---|---|---|
| Session模式 | 开发测试、短作业 | 共享JobManager,作业启动快,但资源隔离差 |
| Per-Job模式 | 生产环境、长作业 | 每个作业独享JobManager,资源隔离好 |
| Application模式 | 大规模生产集群 | Main方法在集群执行,适合CI/CD |
我个人习惯在开发阶段用Session模式,省事。但上了生产,我强烈建议用Per-Job或Application模式。为什么?
我曾经在一个Session集群上同时跑了5个作业,结果一个作业的背压把整个集群搞崩了,其他4个作业全挂。从那以后,生产环境我再也不用Session模式了。
避坑指南: Session模式下,如果某个作业发生OOM,会拖垮整个JobManager。建议生产环境至少用Per-Job模式,每个作业独立进程,互不干扰。
3.3 Flink DataStream API入门
DataStream API是Flink的核心API,咱们做实时指标,天天跟它打交道。说白了,它就是一套用来处理无界数据流的算子集合。
先看一个最简单的例子——从Socket读取数据并做WordCount:
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 从Socket读取数据流
DataStream<String> textStream = env.socketTextStream("localhost", 9999);
// 3. 转换操作
DataStream<Tuple2<String, Integer>> wordCount = textStream
.flatMap((String line, Collector<String> out) -> {
for (String word : line.split(" ")) {
out.collect(word);
}
})
.returns(Types.STRING)
.map(word -> Tuple2.of(word, 1))
.returns(Types.TUPLE(Types.STRING, Types.INT))
.keyBy(value -> value.f0)
.sum(1);
// 4. 打印结果
wordCount.print();
// 5. 启动任务
env.execute("Socket WordCount");
这段代码看着简单,但有几个关键点我得提醒你:
- env.socketTextStream:这是Source,数据入口。生产环境一般用Kafka Source。
- flatMap / map:这是Transformation,数据处理的核心。
- keyBy:这是分组操作,类似SQL里的GROUP BY。
- sum:这是聚合操作,实时累加。
你想想看,整个实时指标的计算逻辑,本质上就是Source -> Transformation -> Sink 这条流水线。Source负责接入数据,Transformation负责清洗、聚合、计算,Sink负责输出结果。
我的习惯: 写DataStream API时,我总会在每个算子后面加上.returns()指定类型。虽然Flink有类型推断,但显式声明能避免很多运行时类型擦除导致的坑。
3.4 Flink时间语义:Event Time、Ingestion Time、Processing Time
时间语义是Flink里最容易搞混的概念,也是实时指标计算的核心。我当年面试候选人时,必问这个问题。
Flink支持三种时间:
| 时间语义 | 定义 | 特点 | 适用场景 |
|---|---|---|---|
| Event Time | 事件实际发生的时间 | 最准确,但需要处理乱序 | 金融交易、IoT传感器 |
| Ingestion Time | 事件进入Flink的时间 | 介于Event和Processing之间 | 对准确性要求中等 |
| Processing Time | 算子处理事件的时间 | 最快,但结果不稳定 | 实时监控、Dashboard |
做实时指标体系建设,我几乎只用Event Time。为什么?因为指标计算必须基于业务时间,而不是系统处理时间。你想想看,如果用户下单时间是10:00,但因为网络延迟,Flink在10:05才收到数据。如果用Processing Time计算,这笔订单就算到了10:05的窗口里,指标就歪了。
使用Event Time时,必须配合Watermark来处理乱序数据。Watermark说白了就是一个时间戳,告诉Flink:「在这个时间戳之前的数据,我已经收得差不多了,你可以触发窗口计算了。」
// 设置Event Time
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
// 分配时间戳和Watermark
DataStream<MyEvent> withTimestamps = stream
.assignTimestampsAndWatermarks(
WatermarkStrategy.<MyEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner((event, timestamp) -> event.getEventTime())
);
// 基于Event Time的滚动窗口
DataStream<MyEvent> windowed = withTimestamps
.keyBy(event -> event.getUserId())
.window(TumblingEventTimeWindows.of(Time.minutes(1)))
.sum("amount");
避坑指南: 我曾经在一个项目中,Watermark延迟设得太小(2秒),结果大量数据因为乱序被丢弃,指标严重偏低。后来改成10秒,才恢复正常。记住:Watermark延迟要根据数据乱序程度来设,宁可多等几秒,也别丢数据。
嗯,到这里Flink的基础就讲完了。架构让你知道系统怎么搭,运行模式让你知道怎么部署,DataStream API让你知道怎么写代码,时间语义让你知道怎么算得准。这四块是Flink的基石,后面的章节我们会反复用到它们。