4. Flink基础入门:Flink架构、运行环境搭建、DataStream API初探、WordCount实战

好,我们正式开始接触实时计算的核心引擎——Flink。这一章是地基,我会尽量讲得实在一些。你想想看,如果连Flink怎么跑起来的都不清楚,后面做ODS到DWD的清洗逻辑肯定会踩坑。我个人习惯是,学一个新东西先看它的骨架,再动手搭环境,最后跑个Demo验证手感。咱们就按这个节奏来。

4.1 Flink架构:它到底是怎么工作的?

Flink本质上是一个「有状态的流处理引擎」。说白了,它把一切数据都当成流来处理——即使是批数据,也被它当作「有界流」。这个思想很关键,后面你会体会到。

它的架构分为两层:运行时层和API层。

  • JobManager:集群的「大脑」。负责调度任务、协调Checkpoint、故障恢复。我遇到过一个问题:JobManager挂掉后任务全崩,后来才知道高可用必须配ZooKeeper。
  • TaskManager:真正干活的「工人」。每个TaskManager有多个Slot,Slot就是计算资源的最小单位。一个Slot可以跑一个子任务。
  • Client:提交任务的入口。它把作业编译成JobGraph,发给JobManager,然后就可以退出了(除非你跑的是交互模式)。

核心概念:并行度(Parallelism)决定了一个算子有多少个并行实例。我刚开始调优时,总以为并行度越大越好,结果资源浪费严重。记住:并行度 = min(数据分区数, 可用Slot数)。

下面这张图展示了Flink的运行时架构和数据流转关系,我建议你多看两眼,后面调试任务时你会反复用到这些概念。

Flink运行时架构图 Flink 运行时架构 Client(客户端) JobManager(作业管理器) 调度 · Checkpoint协调 · 故障恢复 TaskManager 1 Slot 1 | Slot 2 数据源 · 算子 TaskManager 2 Slot 1 | Slot 2 数据源 · 算子 TaskManager 3 Slot 1 | Slot 2 数据源 · 算子 数据汇聚 · 输出(Sink) 说明:Client提交作业 → JobManager调度 → TaskManager并行执行 → 结果输出到Sink

4.2 运行环境搭建:别让环境卡住你

搭建Flink环境其实不复杂,但有几个细节容易翻车。我建议你直接用本地模式先跑起来,等熟悉了再上集群。

4.2.1 前置条件

  • JDK 8 或 11(我推荐JDK 11,Flink 1.14+对JDK 11支持更好)
  • Maven 3.6+(如果你用Java开发)
  • IDE(IntelliJ IDEA最佳,记得装Scala插件)

4.2.2 下载与启动

# 下载Flink(以1.17为例)
wget https://dlcdn.apache.org/flink/flink-1.17.0/flink-1.17.0-bin-scala_2.12.tgz
tar -xzf flink-1.17.0-bin-scala_2.12.tgz
cd flink-1.17.0

# 启动本地集群
./bin/start-cluster.sh

# 检查是否启动成功
jps  # 应该看到 StandaloneSessionClusterEntrypoint 和 TaskManagerRunner

启动后,浏览器打开 http://localhost:8081,你会看到Flink的Web UI。嗯,这里要注意:如果8081端口被占用,可以在 conf/flink-conf.yaml 里改 rest.port。

小技巧:我习惯在 flink-conf.yaml 里把 taskmanager.numberOfTaskSlots 设成4,这样本地调试时可以跑多个并行子任务,方便测试并行度逻辑。

4.2.3 项目依赖

如果你用Maven,pom.xml里加这些依赖:

<properties>
    <flink.version>1.17.0</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>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-clients</artifactId>
        <version>${flink.version}</version>
    </dependency>
</dependencies>

避坑:我曾经因为 flink-streaming-java 和 flink-clients 版本不一致,导致运行时ClassNotFoundException。记住:所有Flink依赖的版本号必须完全一致。

4.3 DataStream API初探:核心概念速览

DataStream API是Flink处理无界流数据的核心API。说白了,你写的每个算子都是在操作一个「无限的数据流」。我刚开始接触时,总觉得它跟Spark Streaming很像,但用久了发现Flink的「事件驱动」模型更灵活。

几个核心概念你必须记住:

概念 说明 我的经验
Source 数据输入源(Kafka、Socket、文件等) 生产环境90%用Kafka Source
Transformation 数据转换算子(map、filter、keyBy等) keyBy后一定要做状态清理,否则内存会爆
Sink 数据输出目标(Kafka、MySQL、HDFS等) 记得开启Exactly-Once语义
KeyedStream 按key分区的流,后续可以做状态操作 keyBy的key不能为null,否则运行时抛异常
Window 将无界流切分成有限块做聚合 事件时间窗口要配合Watermark使用

一个典型的DataStream作业流程是这样的:

Source(读数据) → Transformation(清洗/转换) → KeyBy(分组) → Window(开窗) → Sink(输出)

4.4 WordCount实战:第一个Flink程序

好了,理论说够了,咱们动手写个WordCount。这是实时计算界的「Hello World」,但我会加点料——让它从Socket读数据,实时统计单词出现次数。

4.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 SocketWordCount {
    public static void main(String[] args) throws Exception {
        // 1. 创建执行环境
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(2);  // 设置并行度,我习惯先设2,调试方便

        // 2. 从Socket读取数据(本地用nc命令模拟)
        DataStream<String> textStream = env.socketTextStream("localhost", 9999);

        // 3. 数据转换:切分单词,计数1
        DataStream<Tuple2<String, Integer>> wordCounts = textStream
                .flatMap(new Tokenizer())          // 切分单词
                .keyBy(value -> value.f0)          // 按单词分组
                .sum(1);                           // 累加计数

        // 4. 输出结果(打印到控制台)
        wordCounts.print();

        // 5. 启动任务
        env.execute("Socket 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("\\s+");
            for (String word : words) {
                if (word.length() > 0) {
                    out.collect(new Tuple2<>(word, 1));
                }
            }
        }
    }
}

4.4.2 运行步骤

  1. 启动Socket数据源:在终端执行 nc -lk 9999(Linux/Mac自带,Windows需要装ncat)
  2. 运行Flink程序:在IDE里直接跑main方法,或者打成jar包提交到集群
  3. 输入数据:在nc窗口输入 hello world hello flink,观察控制台输出

4.4.3 输出示例

(hello,1)
(world,1)
(hello,2)
(flink,1)

你会看到,hello 出现了两次,计数累加到了2。这就是Flink的「有状态计算」——它记住了之前出现过的单词次数。

关键点:这里的 sum(1) 实际上是一个有状态算子。Flink内部维护了一个Map状态,key是单词,value是当前计数。每次新数据进来,都会更新这个状态。我在项目中遇到过状态无限增长的问题,后来加了TTL(Time-To-Live)才解决。

4.4.4 常见问题排查

问题 原因 解决方法
连接Socket超时 nc没启动或端口不对 检查 netstat -an | grep 9999
输出结果不累加 没有keyBy,直接sum了 确保keyBy指定了正确的key
并行度与Slot不匹配 Slot数小于并行度 调整 taskmanager.numberOfTaskSlots

调试技巧:我经常在 flatMap 里加一行 System.out.println("收到数据: " + value),这样能快速确认数据是否到达了算子。生产环境当然不能用print,但本地调试时这招很管用。

好了,这一章的内容就是这些。你跟着把WordCount跑通,Flink的基础就算入门了。后面我们会在这个基础上,加入更复杂的清洗逻辑——比如过滤脏数据、字段映射、维表关联。那些才是ODS到DWD的核心战场。