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的运行时架构画清楚了:

Flink 运行时架构图 客户端 JobManager Dispatcher ResourceManager TaskManager 1 Slot 1 Slot 2 Slot 3 Slot 4 TaskManager 2 Slot 1 Slot 2 Slot 3 Slot 4 数据流 说明 • JobManager 负责调度 • TaskManager 负责计算 • Slot 是资源隔离单位

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的基石,后面的章节我们会反复用到它们。