本篇文章,主要通过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并解压:
监听端口
使用如下命令开启监听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包
点击上传按钮,选择打包好的jar上传。上传完成后,我们可以做一些简单设置,例如Main Class、并行度、程序参数以及检查点的保存路径等。
- 运行Job
点击"Submit"即可将jar包提交到Flink环境上运行。
我们可以点击任务详情,看到代码的拓扑结构以及使用到的算子、并行度等信息。
- 查看单词计数结果
同样的,需要在安装了Flink的机器上开启监听端口,然后向端口发送以下数据:
hello word
hello flink
回到Flink UI,查看TaskManager的日志:
单词计数结果:
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>