第3章:Flink基础入门:Flink架构、DataStream API概览、Flink开发环境搭建(Maven/SBT)

好,咱们进入第三章。这一章我打算带大家把Flink的底裤扒开看看——不是真的扒,而是从架构层面理解它为什么这么快、这么稳。然后咱们再上手搭环境,写第一个DataStream程序。嗯,这部分内容我当年自学时踩了不少坑,今天一并分享出来。

3.1 Flink架构:从“流水线”到“分布式引擎”

先问大家一个问题:你觉得一个实时计算引擎,最核心要解决什么问题?

我个人理解,就两个:数据不丢算得够快。Flink的架构设计,说白了就是围绕这两点展开的。

3.1.1 角色划分:JobManager vs TaskManager

Flink集群里有两个关键角色:

  • JobManager:大脑。负责调度任务、协调检查点、处理故障恢复。你可以把它想象成工地上的包工头,只管派活和验收,不亲自搬砖。
  • TaskManager:苦力。真正干活的节点,每个TaskManager里有多个Task Slot(任务槽),一个Slot就是一个线程执行单元。

我记得第一次部署Flink集群时,我犯了个低级错误——把JobManager和TaskManager部署在同一台机器上。结果任务一多,JobManager直接OOM了。后来才明白,生产环境一定要分开部署,JobManager的机器要留足内存给元数据和检查点管理。

⚠️ 避坑指南:Task Slot的数量不是越大越好。我曾经把Slot数设成CPU核心数的2倍,结果线程切换开销反而拖慢了吞吐。建议Slot数 = CPU核心数,或者略少。

3.1.2 数据流动:从Source到Sink

Flink程序的基本结构,其实就三块:

  1. Source:数据源。比如Kafka、Socket、文件系统。
  2. Transformation:转换操作。比如map、filter、window。
  3. Sink:输出目的地。比如Kafka、MySQL、HDFS。

你想想看,这不就是一条流水线吗?数据从源头流进来,经过一道道工序加工,最后打包出货。Flink的牛逼之处在于,它能让这条流水线在成百上千台机器上并行跑,还能保证数据不乱序、不丢失。

3.2 DataStream API概览:你写的第一个“Hello World”

DataStream API是Flink最核心的编程接口。说白了,它就是一套让你能操作“数据流”的API。我习惯把它分成三类操作:

操作类型 常用算子 说明
基础转换 map, flatMap, filter 一对一或一对多的元素处理
分组聚合 keyBy, sum, reduce 按key分组后做聚合计算
窗口操作 window, windowAll 按时间或数量划分窗口

这里我特别想强调一点:DataStream API是延迟执行的。什么意思?就是你写了一堆map、filter,其实它们只是构建了一个执行计划,直到你调用execute(),Flink才会真正开始跑。这个设计跟Spark的懒加载很像,好处是Flink可以优化整个执行图。

💡 个人经验:调试时我喜欢用.print()算子,它会把数据直接输出到控制台。但注意,生产环境千万别用,否则日志会爆炸。我曾经有个同事在生产代码里忘了删print,结果TaskManager的日志一天涨了200GB...

3.3 Flink开发环境搭建:Maven/SBT实战

好,理论说完了,咱们动手。搭建Flink开发环境,我推荐两种方式:Maven(Java/Scala)和SBT(Scala)。我个人更习惯Maven,因为生态更成熟。

3.3.1 使用Maven创建Flink项目

第一步,用Maven Archetype生成项目骨架:

mvn archetype:generate \
  -DarchetypeGroupId=org.apache.flink \
  -DarchetypeArtifactId=flink-quickstart-java \
  -DarchetypeVersion=1.17.0 \
  -DgroupId=com.example \
  -DartifactId=flink-demo \
  -Dversion=1.0-SNAPSHOT

这个命令会生成一个标准的Flink项目,里面自带一个StreamingJob.java示例。嗯,这里要注意:Archetype版本一定要跟你安装的Flink集群版本一致。我曾经因为版本不匹配,折腾了半天才发现是客户端和服务器版本对不上。

3.3.2 核心依赖配置

生成项目后,打开pom.xml,核心依赖就这几个:

<dependencies>
    <!-- Flink核心依赖 -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java</artifactId>
        <version>1.17.0</version>
        <scope>provided</scope>
    </dependency>
    <!-- 如果要用Kafka连接器 -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-kafka</artifactId>
        <version>1.17.0</version>
    </dependency>
</dependencies>
🔧 小技巧scope=provided表示这个依赖在运行时由Flink集群提供,打包时不会包含。这样可以减小JAR包体积。但如果你是在本地IDE里跑测试,记得改成compile,否则会报ClassNotFoundException。

3.3.3 第一个DataStream程序:单词计数

来,写一个最简单的单词计数程序。这个例子我每次培训都会讲,因为它麻雀虽小五脏俱全:

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((String line, Collector<Tuple2<String, Integer>> out) -> {
                for (String word : line.split(" ")) {
                    out.collect(new Tuple2<>(word, 1));
                }
            })
            .keyBy(value -> value.f0)
            .sum(1);

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

        // 5. 执行
        env.execute("Socket WordCount");
    }
}

这段代码的逻辑很直白:从socket读一行文本,按空格切分成单词,然后按单词分组,累加次数。最后打印到控制台。

你可能会问:为什么要用flatMap而不是map?因为一行文本会切出多个单词,这是一对多的关系,flatMap正好合适。我刚开始学的时候也搞混过,后来记住一个口诀:一对一用map,一对多用flatMap

3.3.4 使用SBT(Scala用户)

如果你用Scala,SBT是更自然的选择。在build.sbt里这样配:

name := "flink-scala-demo"
version := "1.0"
scalaVersion := "2.12.17"

libraryDependencies ++= Seq(
  "org.apache.flink" %% "flink-streaming-scala" % "1.17.0" % "provided",
  "org.apache.flink" % "flink-connector-kafka" % "1.17.0"
)

注意这里用了%%,SBT会自动匹配Scala版本的后缀。比如flink-streaming-scala_2.12。这个细节我当年没注意,手动写版本号写错了,编译报错查了半天。

3.4 本地运行与调试

写完代码怎么跑?我一般分两步:

  1. 本地IDE直接运行:Flink会自动启动一个嵌入式集群,适合调试。
  2. 打包提交到集群:用mvn clean package打成JAR,然后flink run -c com.example.WordCount target/flink-demo-1.0-SNAPSHOT.jar

本地运行时,记得先启动一个socket数据源:

nc -lk 9999

然后在终端里输入单词,你就能在IDE的控制台看到实时输出的计数结果了。嗯,第一次看到数据流起来的时候,还是挺有成就感的。

⚠️ 注意:本地调试时,如果遇到ClassNotFoundException,八成是依赖scope设成了provided。临时改成compile就行,提交到集群前再改回来。

3.5 本章小结

这一章我们干了三件事:理解了Flink的架构(JobManager/TaskManager)、熟悉了DataStream API的核心算子、亲手搭了开发环境并跑通了第一个程序。说实话,Flink入门并不难,难的是后面处理乱序数据、状态管理、检查点这些进阶内容。不过别急,咱们一章一章来。

下一章,我会带大家深入DataStream API的细节,重点讲时间语义和Watermark——这可是实时计算的灵魂。到时候我会分享一个我在金融风控项目中遇到的乱序数据坑,保证让你印象深刻。

📌 课后练习:试着把上面的WordCount程序改成从Kafka读取数据,输出到文件。提示:需要引入flink-connector-kafka依赖,并配置Kafka的bootstrap.servers。

好,今天就到这儿。有问题欢迎在课程群里讨论,或者直接找我。咱们下章见。