本篇文章,主要通过Flink来实现“Hello Word”、批处理的“Word Count”以及流处理的“Word Count”来体验一下Flink.

1. 版本说明

本系列文章所使用的flikn版本为最新的1.12.0(截至2021年1月)

  • Flink:1.12.0
  • Java:1.8
  • Maven:3.6.3

2. 一个最简单的Flink程序:Hello Word

在学习任何一门语言或者框架的时候,我们总是以“Hello Word”开始。

//Java
System.out.println("Hello,World!");
//C
printf("Hello,World!");
//Python
print("Hello,World!")
//C#
Console.WriteLine("Hello,World!");

所以,在学习Flink时,我们也从输出“Hello Word”开始,来打开Flink的大门。

2.1 最小化依赖

首先我们需要引入Flink相关的依赖,只需要引入flin-clinets即可,它本身已经包含了flink-java和flink-core了。

<properties>
        <flink.version>1.12.0</flink.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
    </dependencies>

2.2 使用Flink输出“Hello Word”

以下代码实现了输出一个List数据源中的元素“Hello Word”:

/**
 * 最简单的Flink程序: Hello Word
 *
 * @author wxg
 */
public class HelloWord {
    public static void main(String[] args) throws Exception {
        //获取Flink执行环境
        ExecutionEnvironment environment = ExecutionEnvironment.getExecutionEnvironment();
        //从集合中获取数据源
        DataSource<String> dataSource = environment.fromCollection(Collections.singletonList("Hello Word"));
        //输出到控制台
        dataSource.print();
    }
}

在Idea中启动它,不出意外的话,会输出以下结果:

Hello Word

当然这段程序并没有具体的处理逻辑和功能,它只是把数据源的内容原样输出,但它确实是在Flink环境中执行的。

3. Flink批处理Word Count程序

word count可以说是大数据界的hello word,所以本次依然以最简单的word count例子来分别构建一个基于批处理和流处理的简单的flink应用。

3.1 最小化依赖

和上面一样,仍然只需要flink-clients依赖即可。

<properties>
        <flink.version>1.12.0</flink.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
    </dependencies>

3.2 编写Flink处理逻辑

依赖导入完成后,我们就可以愉快的来编写Flink程序了。

在没使用Flink前,比如使用MR的时候,我们要做单词计数的功能,通常有以下几步(可参照文末的附录6.1):

  • 定义输入输出数据格式
  • 编写Map函数,将一行行的字符串按照分隔符进行拆分,然后输出格式类似为{word:1}的一个List
  • 编写Reduce函数,将Map阶段的输出按照word(Reduce Key)进行进行聚合统计
  • 输出结果

实际上flink也大致是这样的一个流程,只不过,代码更简洁。

以下是Flink实现的一个简单的统计文本文件中的单词出现次数的代码:

/**
 * 批处理的word count
 *
 * @author wxg
 */
public class BatchWordCount {
    public static void main(String[] args) throws Exception {
        //获取Flink批处理执行环境
        ExecutionEnvironment environment = ExecutionEnvironment.getExecutionEnvironment();

        //从文件中获取数据源
        final String fileName = "E:\\temp\\study-flink\\word-count.txt";
        DataSource<String> dataSource = environment.readTextFile(fileName);
        //单词计数
        dataSource
                //将一行句子按照空格拆分,输入一个字符串,输出一个2元组,key为一个单词,value为1
                .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
                    public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
                        //对读取到的每一行数据按照空格分割
                        String[] split = s.split(" ");
                        //将每个单词放入collector中作为输出,格式类似于{word:1}
                        for (String word : split) {
                            collector.collect(new Tuple2<String, Integer>(word, 1));
                        }
                    }
                })
                //聚合算子,按照第一个字段(即word字段)进行分组
                .groupBy(0)
                //聚合算子,对每一个分租内的数据按照第二个字段进行求和
                .sum(1)
                //打印结果到控制台
                .print();

    }
}

💡 提示:在实际开发时,不建议使用匿名类的形式来写业务逻辑,这样会降低代码的可读性

其中word-count.txt文件内容如下:

hello word
hello flink
hello java
java is best

在idea上运行以上代码,输出结果如下:

(is,1)
(flink,1)
(hello,3)
(best,1)
(java,2)
(word,1)

4. Flink流处理Word Count程序

4.1 最小化依赖

和上面一样,仍然只需要flink-clients依赖即可。

<properties>
        <flink.version>1.12.0</flink.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
    </dependencies>

4.2 编写Flink处理逻辑

和批处理一样,我们仍然从一个文件中获取数据源的数据,然后通过流处理的方式来输出单词计数的结果。

/**
 * 流处理的word count
 *
 * @author wxg
 */
public class StreamWordCount {
    public static void main(String[] args) throws Exception {
        //获取Flink批处理执行环境
        StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();

        //从文件中获取数据源
        final String fileName = "D:\\temp\\study-flink\\word-count.txt";
        DataStreamSource<String> source = environment.readTextFile(fileName);
        //单词计数
        source
                //将一行句子按照空格拆分,输入一个字符串,输出一个2元组,key为一个单词,value为1
                .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
                    public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
                        //对读取到的每一行数据按照空格分割
                        String[] split = s.split(" ");
                        //将每个单词放入collector中作为输出,格式类似于{word:1}
                        for (String word : split) {
                            collector.collect(new Tuple2<String, Integer>(word, 1));
                        }
                    }
                })
                //聚合算子,按照第一个字段(即word字段)进行分组
                .keyBy(v -> v.f0)
                //聚合算子,对每一个分租内的数据按照第二个字段进行求和
                .sum(1)
                .print();

        environment.execute();
    }
}

输出结果:

4> (hello,1)
2> (java,1)
4> (hello,2)
2> (java,2)
4> (hello,3)
10> (flink,1)
10> (best,1)
9> (word,1)
12> (is,1)

我们可以和批处理的程序对比以下,可以发现他们只有以下区别:

在批处理中,我们获取的Flink执行环境为:

ExecutionEnvironment.getExecutionEnvironment();,使用的分组方式为groupBy()

在流处理中,我们获取的Flink执行环境为:

StreamExecutionEnvironment.getExecutionEnvironment();,使用的分组方式为keyBy()

仅仅是是修改了获取Flink执行环境类型以及分组方式,其他代码均未有任何改动,即可切换到流处理,这也是Flink批流一体化提现的一方面。

4.3 实时统计单词个数

上边的例子并不能提现处流处理的特点,并且从输出结果来看,似乎也不是很直观。这是因为我们使用的数据源是文件中的数据,是一个有界的数据流,即它适合用于批计算而不适合流计算。接下来我们更换数据源为Socket,从Socket中实时接收单词数据来统计单词个数。

在使用socket作为数据源时,首先需要安装下netcat工具,方便向指定端口发送数据。

netcat安装

https://eternallybored.org/misc/netcat/

下载netcat 1.12并解压:

flink界面端口_flink界面端口

监听端口

使用如下命令开启监听8000端口:

nc -l -p 8000

然后继续来写代码。

我们依然沿用上一节的代码,只需要更改数据源为从socket中获取即可:

environment.socketTextStream(host, port);

/**
 * 从socket中实时接收数据的word count
 *
 * @author wxg
 */
public class SocketStreamWordCount {
    public static void main(String[] args) throws Exception {
        //获取Flink批处理执行环境
        StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();

        final String host = "localhost";
        final int port = 8000;
        //从socket中获取数据源
        DataStreamSource<String> source = environment.socketTextStream(host, port);
        //单词计数
        source
                //将一行句子按照空格拆分,输入一个字符串,输出一个2元组,key为一个单词,value为1
                .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
                    public void flatMap(String s, Collector<Tuple2<String, Integer>> collector) throws Exception {
                        //对读取到的每一行数据按照空格分割
                        String[] split = s.split(" ");
                        //将每个单词放入collector中作为输出,格式类似于{word:1}
                        for (String word : split) {
                            collector.collect(new Tuple2<String, Integer>(word, 1));
                        }
                    }
                })
                //聚合算子,按照第一个字段(即word字段)进行分组
                .keyBy(v -> v.f0)
                //聚合算子,对每一个分租内的数据按照第二个字段进行求和
                .sum(1)
                .print();

        environment.execute();
    }
}

发送测试数据

运行以上代码,并在监听的8000端口中依次发送以下数据:

hello word
hello flink

在控制台可以看到输出内容:

4> (hello,1)
2> (java,1)
10> (flink,1)
4> (hello,2)

我们每发送一条数据,Flink就会进行一次统计,并且该程序不会自动结束(除非手动停止,或者监听端口被关闭)。

观察输出结果,可能会有两个疑问:

  • 前面的数字是什么?
    Flink为了提高性能,会使用多个线程来处理,默认线程数为CPU核心数(测试电脑为6物理核心,12逻辑处理器,取值为12),我们在keyBy时,指定了第一个字段为分区字段,Flink就会对该字段进行hash,均匀的落到各个线程上,前面的数字便是线程序号标识。
  • 为什么会重复打印同一个单词的计数?
    因为是流处理,数据在源源不断的产生,来一条就计算一条,所以输出结果是一个累加的过程,就会重复打印了。

5. 提交Flink程序到集群环境运行

Flink提供了多种提交方式,例如web ui提交、命令行提交以及远程提交。具体的提交方式的介绍会在后面的文章中详细说明。本次使用最简单的web ui提供的提交界面来完成Flink程序的提交。

  • 上传jar包

flink界面端口_flink_02

点击上传按钮,选择打包好的jar上传。上传完成后,我们可以做一些简单设置,例如Main Class、并行度、程序参数以及检查点的保存路径等。

  • 运行Job

点击"Submit"即可将jar包提交到Flink环境上运行。

我们可以点击任务详情,看到代码的拓扑结构以及使用到的算子、并行度等信息。

flink界面端口_数据_03

  • 查看单词计数结果

同样的,需要在安装了Flink的机器上开启监听端口,然后向端口发送以下数据:

hello word
hello flink

回到Flink UI,查看TaskManager的日志:

flink界面端口_flink界面端口_04

单词计数结果:

flink界面端口_Word_05

6. 附录

6.1 MR实现的Word Count程序

public class WordCount {

  public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{

    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();

    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }
  }

  public static class IntSumReducer
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values,
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }

  public static void main(String[] args) throws Exception {
    Configuration conf = new Configuration();
    Job job = Job.getInstance(conf, "word count");
    job.setJarByClass(WordCount.class);
    job.setMapperClass(TokenizerMapper.class);
    job.setCombinerClass(IntSumReducer.class);
    job.setReducerClass(IntSumReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(IntWritable.class);
    FileInputFormat.addInputPath(job, new Path(args[0]));
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    System.exit(job.waitForCompletion(true) ? 0 : 1);
  }
}

6.2 Flink打包Jar包Maven配置参考

<build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.xml</include>
                    <include>**/*.properties</include>
                </includes>
            </resource>

            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <optimize>true</optimize>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <outputDirectory>${project.build.directory}
                    </outputDirectory>
                    <skip>true</skip>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.1.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <artifactSet>
                                <excludes>
                                    <exclude>com.google.code.findbugs:jsr305
                                    </exclude>
                                    <exclude>org.slf4j:*</exclude>
                                    <exclude>log4j:*</exclude>
                                </excludes>
                            </artifactSet>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.xxx.your-main-class
                                    </mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>