3. Flink 基础入门:架构、运行模式与第一个程序

好,咱们正式开始接触 Flink。这一章我会带你从架构概览、运行模式,一直走到本地开发环境搭建,最后跑通第一个流处理程序。嗯,内容不少,但每一步我都会穿插一些实际项目里的经验,帮你少走弯路。

3.1 Flink 架构:核心组件与角色

先看一张整体架构图,把 Flink 的“骨架”搭起来。我个人习惯把 Flink 集群分成三个角色:Client、JobManager 和 TaskManager。

一句话总结:Client 提交作业,JobManager 负责调度,TaskManager 真正干活。

Flink 运行时架构 Client 提交作业 / 生成 JobGraph JobManager 调度 / Checkpoint / 恢复 TaskManager Slot 1 | Slot 2 TaskManager Slot 1 | Slot 2 TaskManager Slot 1 | Slot 2 JobGraph → ExecutionGraph Task 分发到 Slot 每个 Slot 一个线程 并行度 = Slot 总数

这里有个关键概念——Slot。每个 TaskManager 可以配置多个 Slot,Slot 是资源调度的最小单位。我记得刚带团队时,有人把 Slot 理解成“线程池里的线程”,其实不完全对。Slot 更像是“资源槽位”,它固定了内存和 CPU 的份额,但一个 Slot 里可以运行多个 subtask(子任务)。

组件 职责 我见过的坑
Client 解析代码、生成 JobGraph、提交给 JobManager Client 挂了不影响已提交的作业
JobManager 调度、Checkpoint、故障恢复 单点故障?用 HA 模式解决
TaskManager 执行 Task,管理 Slot Slot 数别超过 CPU 核数太多

3.2 运行模式:Session、Per-Job、Application

Flink 支持三种运行模式。你想想看,如果每次提交作业都启动一个集群,是不是太浪费了?反过来,如果所有作业共享一个集群,隔离性又差。所以 Flink 给了你选择。

  • Session 模式:先启动集群,再提交作业。适合短作业、交互式调试。缺点:作业之间争抢资源,一个作业 OOM 可能拖垮整个集群。
  • Per-Job 模式:每个作业独享一个集群。资源隔离好,但启动慢。适合生产环境的长作业。
  • Application 模式:从 Application Jar 启动,main 方法在集群上执行。解决了 Per-Job 模式下 Client 需要一直在线的问题。

我的建议:开发测试用 Session 模式,生产环境用 Per-Job 或 Application 模式。我曾经在 Session 模式下跑过 20 多个作业,结果一个作业的背压把整个集群搞挂了——从那以后,生产环境我再也没用过 Session。

3.3 开发环境搭建:从 JDK 到 IDE

环境搭建其实不难,但版本匹配是个容易踩坑的地方。我直接给你一个经过验证的组合。

  1. JDK 8 或 11:Flink 1.13+ 对 JDK 11 支持很好,但我个人习惯用 JDK 8,兼容性最稳。
  2. Maven 3.6+:用于管理依赖和打包。
  3. IDE:IntelliJ IDEA 社区版就够用,记得装 Flink 插件(其实不装也行,但装了能帮你检查代码)。
  4. Flink 本地包:从官网下载二进制包,解压后直接运行 ./bin/start-cluster.sh 就能启动单机版。

注意:Windows 用户请用 WSL2 或 Git Bash,别用 cmd 跑 Flink 脚本。我见过有人用 cmd 跑 start-cluster,结果日志乱码、进程杀不掉——嗯,别问我怎么知道的。

3.4 第一个 Flink 流处理程序:WordCount

好,到了动手环节。我们写一个经典的流式 WordCount,从 socket 读取数据,实时统计单词出现次数。

先创建一个 Maven 项目,在 pom.xml 里引入 Flink 依赖:

<properties>
    <flink.version>1.14.4</flink.version>
    <scala.binary.version>2.12</scala.binary.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

然后写主程序:

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();

        // 2. 从 socket 读取数据(本地 9999 端口)
        DataStream<String> text = env.socketTextStream("localhost", 9999);

        // 3. 转换:切分单词,计数 1
        DataStream<Tuple2<String, Integer>> counts = text
            .flatMap(new Tokenizer())
            .keyBy(value -> value.f0)
            .sum(1);

        // 4. 打印结果
        counts.print();

        // 5. 执行
        env.execute("Socket 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));
                }
            }
        }
    }
}

代码逻辑很清晰:

  • env.socketTextStream 创建一个 socket 数据源
  • flatMap 把每行文本切分成单词
  • keyBy 按单词分组,sum(1) 累加次数
  • print 输出到控制台

运行步骤:

  1. 启动 Flink 集群:./bin/start-cluster.sh
  2. 启动 netcat:nc -lk 9999
  3. 在 IDE 里运行 main 方法,或者在命令行用 flink run -c SocketWordCount your-jar.jar
  4. 在 netcat 窗口输入单词,看控制台输出

你输入 hello world hello,输出应该是:

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

看到这个结果,说明你的 Flink 环境已经跑通了。嗯,第一个程序就这么简单。别小看它,这个模式——source → transformation → sink——是所有 Flink 作业的骨架。

3.5 避坑指南:我踩过的三个坑

  • 坑一:依赖 scope 忘记设 provided。如果你在 IDE 里直接跑,记得把 flink 依赖的 scope 改成 provided,否则会和集群自带的 jar 冲突。我曾经因为这个浪费了半天。
  • 坑二:socket 端口被占用。Flink 默认的 JobManager RPC 端口是 6123,TaskManager 数据端口是 6124。如果你本地有其他服务占用了这些端口,启动会失败。用 lsof -i:6123 检查一下。
  • 坑三:并行度设置不合理。本地调试时并行度默认是 CPU 核数,但 socket 源只能单线程读取。如果你设了并行度 > 1,反而会出问题。建议调试时用 env.setParallelism(1)。

一个小技巧:在 flink-conf.yaml 里把 rest.port: 8081 打开,启动集群后访问 http://localhost:8081 就能看到 Web UI。你可以在这里查看作业状态、背压、Checkpoint 等信息——非常实用。

好了,这一章的内容就到这里。你学会了 Flink 的架构、三种运行模式、环境搭建,还亲手跑了一个流处理程序。接下来,你可以试着把 socket 源换成 Kafka,或者把输出改成文件——这些改动会让你对 Flink 的理解更深一层。