Flink 编程模型第2关:Flink 程序结构
任务描述
本关任务:使用 Flink 计算引擎完成 WordCount 代码。
相关知识
为了完成本关任务,你需要掌握:
1.Flink 程序结构框架介绍
Flink 程序结构框架介绍
和其他分布式处理引擎一样,Flink 应用程序也遵循着一定的编程模式。不管是使用 DataStream API 还是 DataSet API 基本具有相同的程序结构。下面我们从一个 WordCount 程序说起,通过流式计算的方式实现对文本文件中的单词数量进行统计,然后将结果输出在给定路径中。
Streaming WordCount 实例代码
import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
object WordCount {
def main(args: Array[String]): Unit = {
//第一步:设定执行环境设定
val env=StreamExecutionEnvironment.getExecutionEnvironment
//第二步:指定数据源地址,读取输入数据
val text: DataStream[String] = env.readTextFile("wordcount.txt")
//第三步:对数据集指定转换操作逻辑
val counts: DataStream[(String, Int)] = text.flatMap(_.toLowerCase.split(" "))
.filter(_.nonEmpty)
.map((_, 1))
.keyBy(0)
.sum(1)
//第四步:指定计算结果输出位置
//获取参数
val params = ParameterTool.fromArgs(args)
//如果没有指定输出,则默认打印到控制台
if (params.has("output")) {
//指定输出
//执行环境默认的并行度是:4,这边设置SocketSink的并行度:1
counts.writeAsText(params.get("output")).setParallelism(1);
} else {
System.out.println("Printing result to stdout. Use --output to specify output path.");
//控制台打印
counts.print();
}
//第五步:指定名称并触发流式任务
env.execute("Streaming WordCount")
}
}
整个 Flink 程序一共分为 5 步,分别为设定 Flink 执行环境、创建和加载数据集、对数据集指定转换操作逻辑、指定计算结果输出位置、调用 execute 方法触发程序执行。对于所有的 Flink 应用程序基本都含有这 5 个步骤,下面将详细介绍每个步骤。
1.Execution Environment
运行 Flink 程序第一步就是获取相应的执行环境,执行环境决定了程序执行在什么环境(例如本地运行环境或者集群运行环境)中。同时不同的运行环境决定了应用的类型,批量处理作业和流式处理作业分别使用的是不同的 Execution Environment。例如 StreamExecutionEnvironment 是用来做流式数据处理环境,ExecutionEnvironment 是批量数据处理环境。可以使用三种方式获取 Execution Environment,例如 StreamExecution-Environment。
//设定 Flink 运行环境,如果在本地启动则创建本地环境,如果是在集群上启动,则创建集群环境。
StreamExecutionEnvironment.getExecutionEnvironment
//指定并行度创建本地执行环境
StreamExecutionEnvironment.createLocalEnvironment(5)
//指定远程 JobManagerIP 和 RPC 端口以及运行程序所在 jar 包及其依赖包
StreamExecutionEnvironment.createRemoteEnvironment("JobManagerHost",6021,5,"/user/application.jar")
其中第三种方法可以直接从本地代码中创建与远程集群的 Flink JobManager 的 RPC 连接,通过指定应用程序所在的 jar 包,将运行程序远程拷贝到 JobManager 节点上,然后将 Flink 应用程序运行在 远程的环境中,本地程序相当于一个客户端。
和 StreamExecutionEnvironment 构建过程一样,开发批量应用需要获取 Execution-Environment 来构建批量应用开发环境,如以下代码实例通过调用 ExecutionEnvironment 的静态方法来获取批计算环境。
//设定FlinK运行环境,如果在本地启动则创建本地环境,如果是在集群上启动,则创建集群环境
ExecutionEnvironment.getExecutionEnvironment
//指定并行度创建本地执行环境
ExecutionEnvironment.createLocalEnvironment(5)
//指定远程 JobManagerIP 和 RPC 端口以及运行程序所在 jar 包及其依赖包
ExecutionEnvironment.createRemoteEnvironment("JobManagerHost",6021,5,"/user/application.jar")
针对 Scala 和 Java 不同的编程语言环境,Flink 分别制定了不同的语言同时分别定义了不同的 Execution Environment 接口。StreamExecutionEnvironment Scala 开发接口在 org.apache.flink.streaming.api.scala 中,Java 开发接口在 org.apache.flink.streaming.api.java包中;ExecutionEnvironment Scala 接口在 org.apache.flink.api.scala 包中,Java 开发接口在org.apache.flink.api.java 包中。用户使用不同语言开发 Flink 应用时需要引入不同环境对应的执行环境。
2.初始化数据
创建完成 ExecutionEnvironment 后,需要将数据引入到 Flink 系统中。Execution-Environment 提供不同的数据接入接口完成数据的初始化,将外部数据转换成 DataStream<T>或 DataSet<T> 数据集。如以下代码所示,通过调用 readTextFile()方法读取 file:///pathfile路径中的数据并转换成 DataStream<String>数据集。
val text: DataStream[String] = env.readTextFile("wordcount.txt")
通过读取文件并转换为 DataStream[String]数据集,这样就完成了从本地文件到分布式数据集的转换,同时在 Flink 中提供了多种从外部读取数据的连接器,包括批量和实时的数据连接器,能够将 Flink 系统和其他第三方系统连接,直接获取外部数据。
3.执行转换操作
数据外部系统读取并转换成 DataStream 或者 DataSet 数据集中,下一步就将对数据集进行各种转换操作。Flink中的 Transformation 操作都是通过不同的 Operator 来实现,每个 Operator 内部通过实现 Function 接口完成数据处理逻辑的定义。在 DataStream API 和 DataSet API 提供了大量的转换算子,例如 map、flatMap、filter、keyBy等,用户只需要定义每种算子执行的函数逻辑,然后应用在数据转换操作 Dperator 接口中即可。如下代码实现了对输入的文本数据集通过 FlatMap 算子转换成数组,然后过滤非空字段,将每个单词进行统计,得到最后的词频统计结果。
val counts: DataStream[(String, Int)] = text
.flatMap(_.toLowerCase.split(" "))//执行 FlatMap 转换操作,_ 代表该行数据内容,toLowerCase转换小写,并通过空格切割
.filter(_.nonEmpty)//执行 Filter 操作过滤空字段
.map((_, 1))//执行 map 转换操作,转换成 key-value 接口
.keyBy(0)//按照制定 key 对数据重分区
.sum(1)//执行求和运算操作
在上述代码中,通过 Scala 接口处理数据,极大地简化数据处理逻辑的定义,只需要通过传入相应 Lambda 计算表达式,就能完成 Function 定义。特殊情况下用户也可以通过实现 Function 接口来完成定义数据处理逻辑。然后将定义好的 Function 应用在对应的算子中即可。Flink 中定义 Function 的计算逻辑可以通过如下几种方式完成定义。
(1)通过创建 Class 实现 Function 接口
Flink 中提供了大量的函数供用户使用,例如以下代码通过定义 MyMapFunction Class 实现 MapFunction 接口,然后调用 DataStream 的 map() 方法将 MyMapFunction实现类传入,完成对实现将数据集中字符串记录转换成大写的数据处理。
val dataStream: DataStream[String] = env.fromElements("hello", "flink")
dataStream.map(new MyMapFunction).print()
class MyMapFunction extends MapFunction[String,String]{
override def map(t: String): String = {t.toUpperCase()}
}
执行程序结果输出:
1> HELLO
2> FLINK
(2)通过创建匿名类实现 Function 接口
除了以上单独定义 Class 来实现 Function 接口指出,也可以直接在 map() 方法中创建匿名实现类的方式定义函数计算逻辑,例如以下代码获取元素的平方。
val env = StreamExecutionEnvironment.getExecutionEnvironment
val dataStream: DataStream[Integer] = env.fromElements(4, 2, 3, 12, 16,8,1,6,90,14,17,4,9)
//通过实现 MapFuncion 匿名实现类来定义map函数计算逻辑
dataStream.map(new MapFunction[Integer,Integer] {
override def map(t: Integer): Integer = {
t*t
}
})
执行程序结果输出:
2> 9
2> 1
2> 289
1> 4
1> 64
1> 196
3> 144
3> 36
3> 16
4> 16
4> 256
4> 8100
4> 81
(3)通过实现 RichFunction接口
前面提到的转换操作都实现了 Function 接口,例如 MaoFunction 和 FlatMap-Function接口,在 Flink 中同时提供了 RichFunction 接口,主要用于比较高级的数据处理场景,RichFunction 接口中有 open、close、getRuntimeContext 和 setRuntimeContext 等方法来获取状态、缓存等系统内部数据。和 MapFunction 相似,RichFunction 子类中也有 RichMap-Function,如下代码通过实现 RichMapFunction 定义数据处理逻辑。
val env = StreamExecutionEnvironment.getExecutionEnvironment
val dataStream: DataStream[String] = env.fromElements("123", "321","258","666")
//定义匿名实现 RichMapFunction 接口,完成对字符串到整形数字的转换
dataStream.map(new RichMapFunction[String,Int] {
override def map(in: String): Int = {in.toInt}
}).print()
env.execute()
执行程序结果输出:
3> 258
1> 123
4> 666
2> 321
4.分区 key 指定
在 DataStream 数据经过不同的算子转换过程中,某些算子需要根据指定的 key 进行转换,常见的有 join、coGroup、groupBy类算子,需要先将 DataStream 或 DataSet 数据集转换成对应的 KeyedStream 和 GroupedDataSet,主要目的是将相同 key 值的数据路由到相同的 Pipeline 中,然后进行下一步的计算操作。需要注意的是,在 Flink 中这种操作并不是真正意义上将数据集转换为 Key-Value 结构,而是一种虚拟 key,目的仅仅是帮助后面的基于 Key 的算子使用,分区 key 可以通过两种方式指定:
(1)根据字段位置指定
在 DataStream API 中通过 keyBy() 方法将 DataStream 数据集根据指定的 key 转换成重新分区 KeyedStream,如以下代码所示,对数据集按照相同key进行sum()聚合操作。
val env = StreamExecutionEnvironment.getExecutionEnvironment
val dataStream: DataStream[(String, Int)] = env.fromElements(("a", 1), ("c", 2),("a", 1),("a", 1),("a", 1),("c", 1))
//根据第一个字段重新分区,然后对第二个字段进行求和运算
dataStream.keyBy(0).sum(1).print()
env.execute()
执行程序结果输出:
2> (c,2)
2> (c,3)
3> (a,1)
3> (a,2)
3> (a,3)
3> (a,4)
在 DataSet API 中,如果对数据根据某一条件聚合数据,对数据进行聚合时候,也需要对数据进行重新分区。如以下代码所示,使用 DataSet API 对数据集根据第一个字段作为 GroupBy 的 key,然后对第二个字段进行求和运算。
val env = ExecutionEnvironment.getExecutionEnvironment
val dataSet = env.fromElements(("hello", 1), ("flink", 3),("hello", 5),("hello", 3),("flink", 2))
//根据第一个字段进行数据重分区
val groupedDataSet: GroupedDataSet[(String, Int)] = dataSet.groupBy(0)
//求取相同key 值下第二个字段的最大值
groupedDataSet.max(1).print()
执行程序结果输出:
(flink,3)
(hello,5)
(2)根据字段名称指定
KeyBy 和 GroupBy 的 Key 除了能够通过字段位置来指定之外,也可以根据字段的名称来指定。使用字段名称需要 DataStream 中的数据结构类型必须是 Tuple 类或者 POJO 类。如以下代码所示,通过指定 name 字段名称来确定 groupby 的 key 字段。
//使用 POJO 类,要将类写在 main 方法外面
case class Persion(name: String, age: Int)
def main(args: Array[String]): Unit = {
val env = ExecutionEnvironment.getExecutionEnvironment
val dataSet = env.fromElements(Persion("Alex",18),Persion("Peter",43),Persion("Alex",35))
//指定 name 字段名称来确定 groupby 字段
dataSet.groupBy("name").max(1).print()
}
执行程序结果输出:
Persion(Alex,35)
Persion(Peter,43)
如果程序使用 Tuple 数据类型,通常情况下字段名称从 1 开始计算,字段位置索引从 0 开始计算,以下代码中两种方式是等价的。
val env = ExecutionEnvironment.getExecutionEnvironment
val dataSet = env.fromElements(("Alex",18),("Peter",43),("Alex",35))
//通过名称指定第一个字段名称
dataSet.groupBy("_1").max(1).print()
//通过位置指定第一个字段
//dataSet.groupBy(0).max(1).print()
执行程序结果输出:
(Alex,35)
(Peter,43)
5.输出结果
数据集经过转换操作之后,形成最终的结果数据集,一般需要将数据集输出在外部系统中或者输出在控制台之上。在 Flink DataStream 和 DataSet 接口中定义了基本的数据输出方法,例如基于文件输出 writeAsText(),基于控制台输出 print() 等。同时 Flink 在系统中定义了大量的 Connector,方便用户和外部系统交互,用户可以直接通过调用 addSink() 添加输出系统定义的 DataSink 类算子,这样就能将数据输出到外部系统。以下实例调用 DataStream API 中的 writeAsText()和print()方法将数据集输出在文件和客户端中。
//将数据输出到文件中
counts.writeAsText("savafile")
//将数据输出控制台
counts.print()
6.程序触发
所有的计算逻辑全部操作定义好后,需要调用 ExecutionEnvironment 的 execute() 方法来触发应用程序的执行,其中 execute() 方法返回的结果类型为 JobExecutionResult,里面包含了程序执行的时间和累加器等指标。需要注意的是,execute() 方法运行程序,如果不调用则 Flink 流式程序不会执行,但对于 DataSet API 输出算子中以及包含对 execute() 方法的调用,则不需要显性调用 execute() 方法,否则会出现程序异常。
//调用 StreamExecutionEnvironment 的 execute 方法执行流式应用程序
env.execute("APP Name")
编程要求
根据提示,在右侧编辑器补充代码,完成 WordCount 程序。
使用 ExecutionEnvironment 获取批计算环境;
使用 readTextFile 方法读取 /root/files/wordcount.txt 数据;
将读取到的数据存放人 DataSet 内并开始执行转换操作;
使用 flatMap 算子完成对数据转换大小写并切割
使用 filter 算子筛选为空的数据
使用 map 算子将单词变为元组型式数据:(word,1)
使用 groupBy 算子对word进行分区
使用 sum 算子对各值进行相加
将数据输出至文件/root/files/result.txt文件中。
调用 execute() 方法使得程序触发。
测试说明
平台会对你编写的代码进行测试:
测试输入:无;
预期输出:
(and,2)
(as,4)
(be,1)
(beast,2)
(beauty,2)
(before,1)
(can,1)
(ever,2)
(it,1)
(just,1)
(old,1)
(same,1)
(tale,1)
(the,3)
(time,1)
(true,1)
开始你的任务吧,祝你成功!