第2章 Flink 快速开始

182 阅读5分钟

2.1、创建项目

在准备好所有的开发环境之后,我们就可以开始开发自己的第一个 Flink 程序了。首先我们要做的,就是在 IDEA 中搭建一个 Flink 项目的框架。我们会使用 Java 项目中常见的 Maven 来进行依赖管理。

2.1.1、创建工程

打开 IntelliJ IDEA,创建一个 Maven 工程

flink_tutorial_start.png

2.1.2、添加项目依赖

在项目的 pom 文件中,添加 Flink 的依赖,包括 flink-java、flink-streaming-java 以及 flink-clients

		</properties>
				<flink.version>1.16.2</flink.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>

2.2、WordCount 代码编写

需求: 统计一段文字中,每个单词出现的频次

2.2.1、编程模版

无论简单与复杂,Flink 程序都由如下几个部分组成

  • 获取一个编程、执行入口环境 env ;
  • 通过数据源组件、加载、创建 datastream 或 datasource;
  • 对 datastream 或 datasource 调用各种处理算子表达计算逻辑 ;
  • 通过 sink 算子指定计算结果的输出方式;
  • 在 env 上触发程序提交运行。

2.2.2、数据准备

在项目根目录下创建一个input 文件夹,并在下面创建文本文件 words.txt,并输入一些文字,例如:

run flink
run java
run scala

2.2.3、批处理

package org.mochi.wc;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.api.java.operators.AggregateOperator;
import org.apache.flink.api.java.operators.DataSource;
import org.apache.flink.api.java.operators.FlatMapOperator;
import org.apache.flink.api.java.operators.UnsortedGrouping;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.util.Collector;

public class BatchWordCount {
    public static void main(String[] args) throws Exception {

        // 1、获取执行环境 env
        ExecutionEnvironment env =
                ExecutionEnvironment.getExecutionEnvironment();

        // 2、从文件读取数据 按行读取(存储的元素就是每行的文本)
        DataSource<String> lineDS =
                env.readTextFile("input/words.txt");

        // 3、转换数据格式
        FlatMapOperator<String, Tuple2<String, Long>> wordAndOne =
                lineDS.flatMap(new FlatMapFunction<String, Tuple2<String, Long>>() {
                    @Override
                    public void flatMap(String line, Collector<Tuple2<String,
                            Long>> out) throws Exception {
                        String[] words = line.split(" ");
                        for (String word : words) {
                            out.collect(Tuple2.of(word,1L));
                        }
                    }
                });

        // 4、按照 word 进行分组
        UnsortedGrouping<Tuple2<String, Long>> wordAndOneUG =
                wordAndOne.groupBy(0);

        // 5、分组内聚合统计
        AggregateOperator<Tuple2<String, Long>> sum =
                wordAndOneUG.sum(1);
        // 6、打印结果
        sum.print();

    }
}

输出结果

(scala,1)
(flink,1)
(run,3)
(java,1)

这种代码的实现方式,是基于 DataSet API 的,也就把数据当作整个数据集来处理转换的。不过Flink 本身是流批统一的处理架构,所以从 Flink 1.12 开始,官方推荐的做法 是直接使用 DataStream API。如果想继续执行批处理的话,那么就在提交任务时通过将执行模式设为 BATCH: $ bin/flink run -Dexecution.runtime-mode=BATCH BatchWordCount.jar。 这样在实际中只要维护一套 DataStream API 就可 以了。

2.2.4、流处理

对于 Flink 而言,流才是整个处理逻辑的底层核心,所以流批统一之后的 DataStream API 更加强大,可以直接处理批处理和流处理的所有场景。 Flink可以对不同类型的输入数据源进行流处理。

2.2.4.1、读取文件

流处理的整体思路与批处理非常类似,代码模式也基本一致

package org.mochi.wc;

import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

public class StreamWordCount {
    public static void main(String[] args) throws Exception {

        // 1、 创建流式执行环境
        StreamExecutionEnvironment env =
                StreamExecutionEnvironment.getExecutionEnvironment();
        // 2、 读取文件
        DataStreamSource<String> lineStream =
                env.readTextFile("input/words.txt");

        // 3、 转换、分组、求和、得到统计结果
        SingleOutputStreamOperator<Tuple2<String, Long>> sum =
                lineStream.flatMap(new FlatMapFunction<String, Tuple2<String,
                                                Long>>() {
                            @Override
                            public void flatMap(String line, Collector<Tuple2<String,
                                                                Long>> out) throws Exception {
                                String[] words = line.split(" ");
                                for (String word : words) {
                                    out.collect(Tuple2.of(word, 1L));
                                }
                            }
                        }).keyBy(data -> data.f0)
                        .sum(1);
        // 4、 打印
        sum.print();

        // 5、 执行
        env.execute();
    }
}

输出结果

1> (run,1)
1> (scala,1)
1> (run,2)
1> (run,3)
7> (flink,1)
2> (java,1)

观察与批处理程序 BatchWordCount 的不同

  • 创建执行环境的不同,流处理程序使用的是 StreamExecutionEnvironment
  • 转换处理之后,得到的数据对象类型不同;
  • 分组操作调用的是keyBy方法 ,可以传入一个匿名函数作为键选择器(KeySelector),指定当前分组的 key 是什么;
  • 代码末尾需要调用 env 的 execute 方法,开始执行任务。

DataStream 的抽象

  • DataStream 代表一个数据流,它可以是无界的,也可以是有界的;
  • DataStream 类似于 spark 的 rdd,它是不可变的(immutable);
  • 无法对一个 datastream 进行自由的添加或删除或修改元素;
  • 只能通过算子对 datastream 中的数据进行转换,将一个 datastream 转成另一个datastream;
  • datastream 可以通过 source 算子加载、映射外部数据而来,或者从已存在的datastream转换而来。

Flink 还具有一个类型提取系统,可以分析函数的输入和返回类型,自动获取类型信息, 从而获得对应的序列化器和反序列化器。但是,由于 Java 中泛型擦除的存在,在某些特殊情况下(比如 Lambda表达式中),自动提取的信息是不够精细的,这时就需要显式地提供类型信息,才能使应用程序正常工作或提高其性能。 因为对于 flatMap 里传入的 Lambda 表达式,系统只能推断出返回的是 Tuple2 类型,而无 法得到 Tuple2。只有显式地告诉系统当前的返回类型,才能正确地解析出完整数据。

2.2.4.2、读取 socket 文本流

package org.mochi.wc;

import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

public class SocketStreamWordCount {
    public static void main(String[] args) throws Exception {
        
        // 1、创建流式执行环境
        StreamExecutionEnvironment env =
                StreamExecutionEnvironment.getExecutionEnvironment();

        // 2、读取文本流:第一个参数表示发送端主机名、第二个参数表示端口号
        DataStreamSource<String> lineStream =
                env.socketTextStream("127.0.0.1", 9999);

        // 3、转换、分组、求和,得到统计结果
        SingleOutputStreamOperator<Tuple2<String, Long>> sum =
                lineStream.flatMap((String line, Collector<Tuple2<String, Long>> out)
                                -> {
                            String[] words = line.split(" ");
                            for (String word : words) {
                                out.collect(Tuple2.of(word, 1L));
                            }
                        }).returns(Types.TUPLE(Types.STRING, Types.LONG))
                        .keyBy(data -> data.f0)
                        .sum(1);
        // 4、打印
        sum.print();

        // 5、执行
        env.execute();
    }
}

在 Linux 环境的主机上,执行下列命令,发送数据进行测试:nc -lk 9999

mochicruise@mochidembp ~ % nc -lk 9999

要先启动端口,后启动 SocketStreamWordCount 程序,否则会报超时连接异常

启动 SocketStreamWordCount 程序时我们会发现程序启动之后没有任何输出、也不会退出。这是正常的,因为 Flink 的流处理是事件驱动的,当前程序会一直处于监听状态,只有接收到数据才会执行任务。

我们在 Linux 主机上发送数据,在主机上输入 “cat file1”,则在应用程序控制台输出如下内容:

1> (cat,1)
1> (file1,1)

在主机上再输入 “cat file2”,则在应用程序控制台输出如下内容:

8> (file2,1)
1> (cat,2)