4. Apache Flink基础:Flink架构与运行模式、DataStream API入门、事件时间与处理时间

好,咱们进入Flink的世界。说实话,在数据湖仓的增量处理场景里,Flink几乎成了标配。为什么?因为它天生就是为「流」而生的。批处理它能干,流处理它更擅长。今天这一章,我带你把Flink的骨架摸清楚。

4.1 Flink架构与运行模式

先看架构。Flink是个典型的Master-Worker架构。你想想看,一个集群总得有个「大脑」来调度任务,也得有「工人」来干活。

  • JobManager:这就是大脑。负责接收作业、调度任务、做Checkpoint协调。它挂了,整个集群就瘫痪了。所以生产环境一定要配高可用。
  • TaskManager:这是工人。真正干活的地方,每个TaskManager里有多个Task Slot,Slot就是执行任务的最小单元。

我记得刚接触Flink时,有个概念特别容易搞混——并行度和Slot数量的关系。简单说:Slot是资源上限,并行度是你想用多少资源。并行度不能超过Slot总数,否则任务会排队等资源。

核心要点:一个TaskManager可以配置多个Slot,每个Slot独占一部分内存。但Slot之间共享CPU和网络带宽。所以别把Slot数设得太大,否则资源争抢会很严重。

再来看运行模式。Flink支持三种部署方式:

模式 适用场景 特点
Session Mode 短作业、开发测试 共享JobManager,资源复用,但作业间会互相影响
Per-Job Mode 生产环境长作业 每个作业独享JobManager,隔离性好,资源用完即释放
Application Mode 大规模生产集群 Main方法在集群端执行,适合镜像化部署,启动快

我个人习惯在生产环境用Application Mode。为什么?因为Per-Job Mode虽然隔离性好,但每次提交作业都要在客户端执行Main方法,如果客户端挂了,作业就提交失败了。Application Mode把Main方法挪到集群里跑,稳得多。

避坑指南:我曾经在Session Mode下跑多个实时ETL作业,结果一个作业的背压把整个集群拖垮了。从那以后,生产环境我再也不敢用Session Mode跑核心作业。

下面这张图帮你理清Flink的运行时架构:

Flink 运行时架构 JobManager 调度 / Checkpoint / 容错 TaskManager 1 Slot 1 Slot 2 Slot 3 TaskManager 2 Slot 1 Slot 2 Slot 3 数据源 (Kafka) 数据汇 (HDFS) JobManager 负责任务调度与容错 TaskManager 通过 Slot 执行具体算子

4.2 DataStream API入门

好,架构看完了,咱们上手写代码。DataStream API是Flink最核心的编程接口。说白了,就是把数据流想象成一条河,你在河上装各种「处理站」——过滤、转换、聚合、分流。

先看一个最简单的例子:从Socket读取数据,做词频统计。

// 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");

这段代码里,有几点我想强调一下:

  • returns() 方法:Flink的类型系统在Java里会泛型擦除,你不显式指定类型,它可能给你报错。我刚开始写的时候经常漏掉,结果运行时抛异常。
  • keyBy 和 sum:这是Flink最经典的滚动聚合。注意,keyBy之后的数据是逻辑分区,不是全局排序。

小技巧:如果你用Lambda表达式,记得用 .returns() 告诉Flink返回类型。或者直接用匿名内部类,省得类型擦除搞鬼。

DataStream API的算子可以分为三大类:

类别 算子 说明
Source fromCollection / socketTextStream / addSource 数据入口,Kafka Connector是最常用的
Transformation map / flatMap / filter / keyBy / window 核心处理逻辑,链式调用
Sink print / writeAsText / addSink 数据出口,写入外部系统

我个人建议,刚开始学的时候,多写几个map和flatMap的例子。这两个算子几乎覆盖了80%的数据转换场景。map是一对一,flatMap是一对多,搞懂它们,后面学窗口和状态就轻松了。

4.3 事件时间与处理时间

这是Flink里最容易踩坑的地方,没有之一。我见过太多人因为时间语义没选对,导致数据对不上账。

Flink支持三种时间语义:

  • Processing Time:处理时间,就是机器当前的时间。简单、延迟低,但结果不确定。比如你8点处理一条7点的数据,时间戳就是8点。
  • Event Time:事件时间,数据本身携带的时间戳。比如IoT设备上报时带的采集时间。结果确定,但需要处理乱序。
  • Ingestion Time:摄入时间,数据进入Flink的时间。介于两者之间,用得不多。

为什么说Event Time重要?你想想看,在数据湖仓场景里,我们经常要回溯历史数据。如果用Processing Time,今天重跑昨天的数据,时间戳全变成今天了,聚合结果完全不对。所以,生产环境几乎都用Event Time。

我曾经接手过一个实时报表项目,上游数据经常延迟5-10分钟。开发同学用了Processing Time,结果每天凌晨的报表数据总是对不上。后来改成Event Time + Watermark,问题才解决。记住:只要数据可能乱序,就别用Processing Time做聚合。

用Event Time必须配合Watermark。Watermark是Flink用来衡量事件时间进度的机制。它本质上是一个时间戳,告诉Flink:「在这个时间戳之前的数据,我已经收得差不多了,你可以触发计算了」。

看个例子:

// 设置事件时间语义
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

// 自定义Watermark生成器
DataStream<MyEvent> streamWithWatermarks = inputStream
    .assignTimestampsAndWatermarks(
        WatermarkStrategy.<MyEvent>forBoundedOutOfOrderness(Duration.ofSeconds(5))
            .withTimestampAssigner((event, timestamp) -> event.getEventTime())
    );

// 滚动窗口,每10秒统计一次
streamWithWatermarks
    .keyBy(event -> event.getUserId())
    .window(TumblingEventTimeWindows.of(Time.seconds(10)))
    .sum("amount")
    .print();

这里forBoundedOutOfOrderness(Duration.ofSeconds(5))的意思是:允许数据最多乱序5秒。也就是说,Watermark = 当前收到的事件时间最大值 - 5秒。这样即使有些数据来晚了,只要不超过5秒,都能被正确归入对应的窗口。

核心原则:Watermark的延迟时间设置,取决于你的业务容忍度。设得太小,乱序数据会被丢弃;设得太大,窗口结果出得慢。我一般建议先设成预估延迟的2倍,上线后再根据实际数据调整。

最后,用一张图总结时间语义和Watermark的关系:

Flink 时间语义与 Watermark 0s 5s 10s 15s 20s 事件A(3s) 事件B(8s) 事件C(12s) 乱序事件(4s) Watermark = 当前最大事件时间 - 允许乱序延迟 窗口 [0s, 10s) 窗口 [10s, 20s) 当 Watermark 超过窗口结束时间时,触发窗口计算 乱序事件(4s) 在 Watermark 到达 10s 前到来,仍可归入 [0s,10s) 窗口 若乱序事件在 Watermark 超过 10s 后才到,则被丢弃或进入侧输出