3. Flink入门:从概念到第一个WordCount

好,咱们进入Flink的世界。说实话,我第一次接触Flink是在2018年,当时团队要从Storm迁移到更靠谱的流引擎。选型对比了一圈,最终定了Flink。为什么?因为它把「流」这件事想得特别透彻——流就是流,批只是流的特例。这个理念,到现在我都觉得是它最厉害的地方。

3.1 Flink是什么?

Flink是一个分布式、高性能、有状态的流处理框架。你可以把它理解成一个「数据管道工厂」——数据从一头流进去,你在中间装各种处理逻辑,结果从另一头出来。

它跟Spark Streaming最大的区别在哪?Flink是真正的逐条处理,每条数据到了就处理,不攒批。而Spark Streaming是微批,攒一小批再处理。这个差异在延迟敏感的场景下特别明显。我之前做过一个实时风控项目,要求延迟在100毫秒以内,Spark Streaming死活压不下去,换了Flink,轻松搞定。

核心定位:Flink = 流处理引擎 + 批处理能力 + 状态管理 + 精确一次语义

3.2 核心概念:你得先搞懂这几样

学Flink,有几个概念绕不开。我按重要程度排个序,你一个个吃透就行。

3.2.1 DataStream:一切皆流

在Flink里,DataStream是最基本的数据抽象。说白了,它就是一条无限流动的数据序列。你从Kafka读数据,得到一个DataStream;你从Socket读数据,也是一个DataStream。

DataStream分两种:

  • DataStream<T>:普通的流,处理非键控数据
  • KeyedStream<T, KEY>:按key分过组的流,后面才能做状态操作

怎么从普通流变成KeyedStream?很简单,调一个.keyBy()就行。嗯,这里要注意:keyBy不是把数据分到不同分区,而是把相同key的数据路由到同一个subtask。这个区别,我见过不少新手搞混。

3.2.2 算子:流上的操作符

算子就是你对数据流做的各种操作。Flink的算子分几大类:

类别 常见算子 说明
转换 map, flatMap, filter 一对一、一对多、过滤
聚合 keyBy, sum, reduce, window 分组、求和、自定义聚合、窗口
连接 connect, union, join 合并两条流
输出 print, writeAsText, addSink 打印、写文件、输出到外部系统

我个人习惯把算子分成两类:无状态算子和有状态算子。map、filter这些是无状态的,来一条处理一条,不记任何东西。而sum、window这些是有状态的,需要记住中间结果。有状态算子才是Flink真正值钱的地方。

3.2.3 状态:Flink的灵魂

状态是什么?就是算子需要记住的信息。比如你要算过去5分钟的PV,你得记住这5分钟里每个URL的访问次数,这就是状态。

Flink的状态分两种:

  • Operator State:算子级别的状态,每个并行实例一份。常用于Source/Sink,比如记录Kafka消费的offset。
  • Keyed State:按key划分的状态,每个key一份。只能在KeyedStream上用。常用的有ValueState、ListState、MapState等。

我在项目中踩过一个坑:状态默认存在JVM堆内存里,如果数据量大,很容易OOM。后来改用了RocksDB状态后端,把状态存到磁盘,才稳住。所以,生产环境一定要用RocksDB,别图省事用内存。

我的建议:刚开始学的时候,先别纠结状态后端的细节。先用内存状态后端跑通流程,理解了状态的概念,再切RocksDB。一步到位反而容易懵。

3.2.4 Checkpoint:Flink的保命符

Checkpoint是Flink实现精确一次语义的基石。它的原理是:每隔一段时间,把算子的状态做一个全局快照,保存到外部存储(比如HDFS)。如果任务挂了,就从最近的Checkpoint恢复。

Checkpoint的核心机制是Barrier对齐。简单说:

  1. Source算子往数据流里插入特殊标记——Barrier
  2. 每个算子收到Barrier后,把当前状态快照下来
  3. 等所有算子的快照都完成了,这个Checkpoint才算成功

我曾经遇到过一个问题:Checkpoint频繁超时。排查了半天,发现是Kafka消费太慢,Barrier在流里堵住了。后来调大了并行度,才解决。所以,Checkpoint超时,多半是背压问题,别急着调Checkpoint参数。

注意:Checkpoint不是万能的。它保证的是「精确一次」,但恢复时会有延迟。如果你的业务对延迟极度敏感(比如毫秒级),可能需要考虑其他方案。

3.3 环境搭建:别怕,就三步

搭建Flink开发环境,其实很简单。我带你走一遍。

3.3.1 本地模式(开发用)

本地模式不需要集群,直接在IDE里跑。你只需要:

  1. 创建一个Maven项目
  2. 引入Flink依赖
  3. 写代码,直接运行

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是目前比较稳的。

3.3.2 集群模式(生产用)

生产环境一般用Standalone集群或YARN/K8s。我以Standalone为例:

  1. 去官网下载Flink压缩包
  2. 解压,修改conf/flink-conf.yaml,配置jobmanager和taskmanager的地址
  3. 启动集群:./bin/start-cluster.sh
  4. 提交任务:./bin/flink run -c MainClass your-jar.jar

我个人更推荐用YARN模式,因为资源管理更方便。不过初学者先用Standalone练手就行。

3.4 WordCount:Flink版的Hello World

好了,理论说完了,咱们写个WordCount。这个例子虽然简单,但包含了Flink的核心流程:Source → Transformation → Sink。

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 WordCount {
    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 WordCount");
    }

    // 自定义FlatMap函数
    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 运行步骤

  1. 启动一个终端,运行 nc -lk 9999,监听9999端口
  2. 在IDE里运行WordCount程序
  3. 在终端输入一些英文句子,比如 hello world hello flink
  4. 看IDE控制台输出:(hello,2) (world,1) (flink,1)

你想想看,这个程序虽然简单,但背后Flink做了多少事?它维护了每个单词的计数状态,数据来了就更新,而且保证不会丢数据。这就是Flink的魅力。

关键点:keyBy + sum 组合,是Flink有状态计算的经典模式。理解了这个,后面学窗口、Join就顺了。

3.5 本章知识体系

下面这张图,帮你把本章的核心概念串起来:

Flink核心概念体系 Flink核心概念体系 Apache Flink DataStream 算子 状态 Checkpoint DataStream KeyedStream map/flatMap keyBy/sum window ValueState ListState Barrier 快照 Flink的核心:流(DataStream)+ 操作(算子)+ 记忆(状态)+ 容错(Checkpoint) WordCount就是这四个概念的完美结合

这张图把本章的核心概念串起来了。你从Flink出发,往下分到DataStream、算子、状态、Checkpoint四个支柱,再往下是每个支柱的具体实现。WordCount就是这四个支柱的实战演练。