2. Flink 基础回顾:核心概念与开发环境搭建
好,咱们正式开始 Flink 集成 Iceberg 的课程。在动手搭建实时数仓之前,我强烈建议先把 Flink 的几个核心概念再过一遍。你可能会说:“Flink 我熟啊,不就是流处理框架吗?” 嗯,话是没错,但咱们后面要跟 Iceberg 深度集成,很多细节会直接影响到数据的一致性和性能。所以,这一章咱们稳扎稳打,把基础夯实。
2.1 Flink 核心概念速览
Flink 的核心抽象,说白了就四个东西:DataStream、算子、状态 和 时间语义。我见过不少新手,把这四个概念混在一起,结果写出来的作业要么丢数据,要么延迟爆炸。咱们一个一个拆开看。
2.1.1 DataStream:流动的数据集合
DataStream 是 Flink 中最核心的数据抽象。你可以把它想象成一条永不停歇的数据河流,每个数据点就是一个事件。在 Flink 1.12 之后,DataStream API 和 Table API 在流批一体上做了大量统一,但底层的数据模型还是 DataStream。
关键点: DataStream 是无界的,但你可以通过 window 或 assignTimestampsAndWatermarks 给它加上“边界”。在实时数仓场景下,我们通常处理的是无界流,但 Iceberg 的写入需要按批次提交,这里就需要用到 Flink 的 checkpoint 机制来模拟“微批次”。
2.1.2 算子:数据处理的“手脚架”
Flink 提供了丰富的算子,我习惯把它们分成三类:
- 基础转换算子:
map、flatMap、filter。这些是最常用的,就像炒菜时的切菜、洗菜。 - 分组聚合算子:
keyBy、window、reduce、aggregate。这些是流处理的核心,负责把数据按 key 分组,然后在时间窗口内做计算。 - 连接算子:
connect、coGroup、intervalJoin。用于合并两条流,比如把订单流和支付流关联起来。
我在项目中遇到过一个问题:用 keyBy 之后,数据倾斜导致某个子任务内存溢出。后来发现是 key 的哈希分布不均匀,加了个 rebalance 算子才解决。所以,算子的选择直接影响并行度和资源利用率。
2.1.3 状态:Flink 的“记忆”
状态是 Flink 区别于普通消息队列处理的关键。没有状态,Flink 就是一个无状态的 ETL 工具;有了状态,它才能做累加、去重、窗口计算这些复杂操作。
| 状态类型 | 说明 | 典型场景 |
|---|---|---|
| ValueState | 存储单个值 | 记录最近一次事件时间 |
| ListState | 存储列表 | 缓存一段时间内的所有事件 |
| MapState | 存储键值对 | 维护用户画像的维度数据 |
| ReducingState / AggregatingState | 增量聚合 | 实时计算 PV/UV |
我的建议: 状态后端建议用 RocksDB,尤其是状态量大的场景。虽然读写速度比 Heap 慢一点,但胜在稳定,不会因为状态太大而 OOM。我在生产环境里,超过 10GB 的状态一律用 RocksDB。
2.1.4 时间语义:事件时间 vs 处理时间
这是 Flink 里最容易搞混的概念,也是实时数仓的基石。简单来说:
- 处理时间(Processing Time):数据到达 Flink 算子的机器时间。延迟低,但结果不确定。
- 事件时间(Event Time):数据本身携带的时间戳。延迟稍高,但结果可复现。
在实时数仓里,我们几乎只用 事件时间。为什么?因为 Iceberg 的写入需要按时间分区,如果用处理时间,数据乱序会导致分区错乱。我曾经接手过一个项目,上游 Kafka 数据延迟了 5 分钟,结果下游的报表全乱了。后来改成事件时间 + Watermark,才把数据对齐。
注意: 使用事件时间时,必须正确设置 Watermark。Watermark 太短会导致大量迟到数据被丢弃;太长又会增加延迟。我一般建议设置成 maxOutOfOrderness 的 2-3 倍,比如允许乱序 10 秒,Watermark 就设 20-30 秒。
2.2 Flink 开发环境搭建
概念讲完了,咱们动手搭环境。我个人习惯用 IDEA + Maven + Flink 1.17 的组合。别用太老的版本,Iceberg 对 Flink 1.15+ 支持最好。
2.2.1 项目骨架与依赖
创建一个 Maven 项目,pom.xml 里引入 Flink 的核心依赖:
<properties>
<flink.version>1.17.1</flink.version>
<scala.binary.version>2.12</scala.binary.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java-bridge</artifactId>
<version>${flink.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
避坑指南: 依赖的 scope 一定要设成 provided,否则打包时会和 Flink 集群自带的 jar 包冲突。我曾经因为这个浪费了一整天排查 ClassNotFoundException。
2.2.2 第一个 Flink 程序:WordCount
咱们写一个最简单的流式 WordCount,感受一下 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 StreamingJob {
public static void main(String[] args) throws Exception {
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 从 socket 读取数据(模拟实时流)
DataStream<String> text = env.socketTextStream("localhost", 9999);
// 3. 转换:切分单词,计数
DataStream<Tuple2<String, Integer>> counts = text
.flatMap(new Tokenizer())
.keyBy(value -> value.f0)
.sum(1);
// 4. 输出
counts.print();
// 5. 执行
env.execute("Flink Streaming WordCount");
}
public static class Tokenizer implements FlatMapFunction<String, Tuple2<String, Integer>> {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
String[] words = value.toLowerCase().split("\\W+");
for (String word : words) {
if (word.length() > 0) {
out.collect(new Tuple2<>(word, 1));
}
}
}
}
}
这段代码虽然简单,但包含了 Flink 程序的完整骨架:环境创建 → 数据源 → 转换 → 输出 → 执行。你想想看,后面咱们集成 Iceberg 时,只是把数据源换成 Kafka,把输出换成 Iceberg 表,逻辑是一样的。
2.2.3 本地运行与调试
在 IDEA 里直接运行 main 方法,Flink 会启动一个本地 MiniCluster。你可以用 nc -lk 9999 在终端发送数据,观察控制台输出。
调试技巧: 如果遇到数据倾斜或背压问题,可以在 Flink Web UI(本地默认 localhost:8081)查看每个算子的吞吐量和延迟。我一般先看 backPressure 指标,如果某个算子持续高背压,就说明它成了瓶颈。
2.3 本章知识体系总览
下面这张图把 Flink 的核心概念和开发流程串起来了,你可以对照着理解:
嗯,这张图把 Flink 的整个处理链路串起来了。从数据源进来,经过 DataStream 的转换,中间依赖算子、状态和时间语义,最后输出到 Sink。咱们后面集成 Iceberg,其实就是把 Sink 换成 Iceberg 的 TableSink,同时利用 Flink 的 checkpoint 来保证写入的一致性。
2.4 本章小结
这一章咱们回顾了 Flink 的四大核心概念,并且搭好了开发环境。你可能会觉得内容有点基础,但别急,这些是后面所有实战的基石。尤其是 状态 和 时间语义,在 Iceberg 的实时写入中会反复用到。