3. 实时流处理基础:Flink 核心概念与 WordCount 实战
好,咱们进入第三章。这一章是硬骨头,也是地基。
实时特征计算,说白了就是跟数据流打交道。而 Flink,是目前业界处理数据流最主流的引擎之一。我这些年做实时架构,从 Storm 到 Spark Streaming 再到 Flink,踩过的坑不少。最后为什么选择 Flink?因为它把「流」这件事想得最透彻。
这一章,我会带你从零搭建 Flink 环境,然后亲手跑一个 WordCount。别小看 WordCount,它背后藏着 Flink 最核心的抽象——DataStream 和 Operator。搞懂了这两个,后面讲实时特征计算,你就知道数据是怎么在管道里流动、怎么被加工的了。
3.1 为什么是 Flink?
先聊个题外话。我记得 2018 年有个项目,需要做毫秒级的用户点击流特征提取。当时团队用的是 Spark Streaming,但微批处理的延迟始终降不下来。后来换成 Flink,延迟直接降到几十毫秒。那次之后,我对 Flink 的「真·流处理」能力有了切身体会。
Flink 的核心优势,我总结为三点:
- 真正的流处理:每条数据到达即处理,不是攒一批再算。延迟低到亚秒级。
- 精确一次语义:在故障恢复时,数据不会丢也不会重复。做金融风控特征时,这个特性救过我。
- 强大的状态管理:算子可以记住历史数据,比如计算滑动窗口内的用户行为次数,Flink 帮你管好状态。
你想想看,如果特征计算不准,后面的模型推理全是错的。所以选 Flink,就是选确定性。
3.2 Flink 核心概念:DataStream 与 Operator
这两个概念,是理解 Flink 的钥匙。
3.2.1 DataStream:数据流的抽象
DataStream 是什么?说白了,它就是一条无限流动的数据序列。每条数据是一个事件,比如一次点击、一条日志、一个传感器读数。
在代码里,DataStream 是一个泛型类。你可以把它想象成一个管道,数据从源头进来,经过各种算子处理,最后输出到某个地方。
举个例子:
// 从 socket 读取数据流
DataStream<String> textStream = env.socketTextStream("localhost", 9999);
这里的 textStream 就是一个 DataStream,里面的每个元素是一行字符串。
我个人习惯把 DataStream 比作「传送带」。传送带上的每个包裹就是一条数据。你可以在传送带上做各种操作——拆包、贴标签、分拣。这些操作,就是 Operator。
3.2.2 Operator:数据流的转换操作
Operator 是对 DataStream 进行转换的函数。Flink 提供了丰富的算子,我挑几个最常用的:
| 算子 | 作用 | 输入 → 输出 |
|---|---|---|
map |
一对一转换 | 1 条 → 1 条 |
flatMap |
一对多转换 | 1 条 → 0 或多条 |
filter |
过滤 | 保留符合条件的 |
keyBy |
按 key 分组 | DataStream → KeyedStream |
window |
开窗口 | KeyedStream → WindowedStream |
sum/reduce |
聚合 | 分组后做累加或归约 |
嗯,这里要注意:keyBy 之后的数据流变成了 KeyedStream,它和普通的 DataStream 不一样。KeyedStream 可以理解为「按 key 分好桶的流」,后续的窗口、聚合操作都基于这些桶进行。
keyBy 之前用了 map 做状态操作。结果状态没有按 key 隔离,数据全乱了。记住:需要按 key 维护状态的操作,一定要放在 keyBy 之后。
3.3 Flink 环境搭建
光说不练假把式。咱们动手搭个环境。
3.3.1 本地开发环境
我建议你直接用 IntelliJ IDEA + Maven。Flink 的 Java API 最成熟,虽然现在也有 PyFlink,但做生产级特征计算,Java/Scala 还是主流。
Maven 依赖如下:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>1.17.0</version>
</dependency>
版本号我用的 1.17,目前比较稳定。如果你用更新的版本,注意检查 API 兼容性。
3.3.2 启动本地集群(可选)
如果你只是想跑个 demo,用 IDE 直接运行就行,Flink 会自动启动一个嵌入式集群。但如果你想体验完整的提交流程,可以下载 Flink 压缩包,解压后运行:
# 启动集群
./bin/start-cluster.sh
# 访问 Web UI
http://localhost:8081
Web UI 是个好东西。我调试任务时,经常盯着 UI 看算子延迟、反压情况。这些信息对调优很有帮助。
3.4 WordCount 实战
好,重头戏来了。咱们写一个实时 WordCount,从 socket 读取文本,实时统计单词出现次数。
3.4.1 完整代码
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 StreamingWordCount {
public static void main(String[] args) throws Exception {
// 1. 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 读取数据源:socket 文本流
DataStream<String> textStream = env.socketTextStream("localhost", 9999);
// 3. 转换:切分单词,映射为 (word, 1)
DataStream<Tuple2<String, Integer>> wordCounts = textStream
.flatMap(new Tokenizer()) // 切分
.keyBy(value -> value.f0) // 按单词分组
.sum(1); // 累加计数
// 4. 输出
wordCounts.print();
// 5. 启动任务
env.execute("Streaming WordCount");
}
// 自定义 FlatMapFunction
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));
}
}
}
}
}
3.4.2 运行步骤
- 在终端启动一个 socket 数据源:
nc -lk 9999 - 在 IDE 中运行
StreamingWordCount的 main 方法 - 在终端输入一些英文句子,比如
hello world hello flink - 观察 IDE 控制台输出:
(hello,2) (world,1) (flink,1)
你会发现,每次输入新句子,结果都会实时更新。这就是流处理的魅力——数据一到,结果就出。
flatMap:把一行文本拆成多个单词,每个单词发射一个 (word, 1) 元组。keyBy:按单词分组,相同单词的数据进入同一个分区。sum(1):对元组的第二个字段(索引 1)做累加。print():将结果输出到标准输出,每个并行子任务一个线程。
3.5 数据流图:从源头到输出
为了让你更直观地理解数据是怎么流动的,我画了一张图。这张图展示了 WordCount 任务中,数据从 socket 进入,经过各个算子,最终输出的完整路径。
这张图里,每个矩形代表一个算子,箭头代表数据流动方向。你可以看到,数据从左边进来,经过四个算子的处理,最后从右边输出。这就是 Flink 任务的基本执行模型——一个有向无环图(DAG)。
3.6 常见问题与避坑
最后,分享几个我实际踩过的坑:
我曾经有个特征计算任务,某个 key 的数据量特别大,导致那个子任务处理不过来,其他子任务却闲着。解决办法:对 key 加盐(salted key),把热点 key 拆成多个子 key,计算完再合并。
Flink 的状态后端有 MemoryStateBackend、FsStateBackend、RocksDBStateBackend。如果状态量很大(比如超过 1GB),千万别用 Memory,否则 OOM 是迟早的事。我一般用 RocksDB,虽然读写慢一点,但稳定。
flatMap 或 map 里加个 System.out.println,打印当前处理的数据。虽然不优雅,但排查问题很管用。
好了,这一章的内容就到这儿。DataStream 和 Operator 是 Flink 的基石,WordCount 是入门的第一道坎。搞懂了这些,后面讲实时特征计算,你就能跟上节奏了。