Flink实战系列(二)之基础API使用

1,401 阅读7分钟

DataSet/DataStream

Flink中定义两个特殊类来代表数据:DataSet、DataStream,**两者与Java中的集合不同,它们是不可变的,也就是一旦被创建其中的数据不能被增添或者删除,同时它们允许数据可重复。**如果你使用如map、filter等算子,那么新的DataSet/DataStream就在每使用transformations算子之后就会被创建。

程序流程

Flink的程序编写极为规律,每个程序都包含以下部分,这里以WordCount举例,示例数据内容如下:

hadoop,hadoop,hadoop
flink,flink
spark

获取Execution Environment

//流处理
val env = StreamExecutionEnvironment.getExecutionEnvironment

//批处理
val env = ExecutionEnvironment.getExecutionEnvironment

虽然官方提供以下三类获取Environment,但是通常只需要使用getExecutionEnvironment(),它会根据环境来正确地的创建Environment,如果程序执行在IDE编辑器或者作为常规的Java程序执行,它就会在你机器上创建一个本地的Environment;如果使用命令行方式调用Jar包,Flink cluster manager就会执行对应的main方法同时getExecutionEnvironment()就会返回用于集群环境的Environment。

getExecutionEnvironment()

createLocalEnvironment()

createRemoteEnvironment(host: String, port: Int, jarFiles: String*)

加载/创建数据(Source)

如果你想读取文件数据可以用以下方法:

val ds = env.readTextFile("input/wc.txt")

指定Transformations算子

每个transformation算子都会产生新的DataSet/DataStream,以下就是WordCount统计所用到算子:

//wordcount统计
val result = ds.flatMap(_.split(","))
      .map((_, 1))
      .keyBy(0)
      .sum(1)

指定计算之后结果输出到哪里(Sink)

print()方法是让结果打印在控制台以便观察,你也可以使用writeAsText(path: String)来写入文件中。

result.print()

触发

这里需要提醒的是,只有流处理需要显式触发,而批处理只需要指定输出即可。

env.execute(this.getClass.getSimpleName)

⚠️注意:以上操作不会像Java程序一样在main方法被启动之后立刻执行相关操作,而是将每个操作添加在Flink维护的有向无环图(简称DAG)中,只有当被触发之后整个DAG才开始执行,这也就是Lazy Evaluation(懒加载)。
**

并行度Parallel

提出问题

如果你按照上述WordCount例子运行的话,你应该能看到以下输出内容。很明显与Spark的不太一样,为什么会出现(flink,1)、(hadoop,1)等输出内容呢?原因是Flink对比Spark来说,数据不会以批次处理,而是真正的实时处理,当第一个hadoop被加载立即输出(hadoop,1),等到第二个hadoop到达时就变成(hadoop,2),以此类推。

11> (hadoop,1)
11> (hadoop,2)
11> (hadoop,3)
1> (spark,1)
10> (flink,1)
10> (flink,2)

不知道你是否注意到结果之前的数字,比如11、1、10,这些代表什么?为什么hadoop前面会是11,spark前面是1,flink前面是10?要搞清楚这些问题就必须先了解Flink中的并行度。

什么是并行度

前一篇介绍了Flink是分布式且任务并行执行的计算引擎,在执行过程中,数据流会有一个或者多个分区,每个操作Task也会有一个或者多个子操作Task,这里的并行度也就是指定的操作有多少个子操作Task,同一个代码中每个操作的并行度都不一定相同。

parallel_dataflow.svg

Flink中有两类操作:one-to-one和redistributing都与并行度有关:

  • One-to-one

例如Source与map()之间,保留数据的顺序和分区,类似与Spark的窄依赖。

  • Redistributing

例如map()和keyBy/window、keyBy/window和Sink之间数据重新分配到不同子Task上。不同算子会有不同分配方式:keyBy() 通过Key值来Hash进行重新分配、broadcast()和rebalance()则是随机分配。

回答问题

回到之前的问题,输出结果之前的数字其实就是线程号,经过keyBy()重新分配之后同一key都在一个线程中处理。如果不想看到这些线程号,只需要在print()后面加上setParallelism(1)就行。其实有很多中设置并行度方法,之后系列文章会详细写一部分。

多种指定Key的方式

一些transformations算子,例如:join, coGroup, keyBy, groupBy需要使用者指定Key,以便之后的Reduce, GroupReduce, Aggregate, Windows等算子计算。

Tuples指定Key

上方WC例子就是在Tuple中通过index来指定Key的,代码如下:

val result = ds.flatMap(_.split(","))
      .map((_, 1))
      .keyBy(0)
      .sum(1)

⚠️需要注意的是,如果你有个如下所示的DataStream,key(0)则是指的其中的Tuple2<Integer,Float>,如果你想指定Tuple2中的Integer,可以使用类似于_._1._1。

DataStream<Tuple3<Tuple2<Integer, Float>,String,Long>> ds;

字段指定Key

这个是基于字段名来指定Key,特别适合嵌套类型,例如Tuple和POJO。首先得定义一个WC POJO来接受数据,建议使用case class,代码如下:

case class WC(word:String,count:Int)
input.flatMap(_.split(","))
      .map(ele => {
        WC(ele, 1)
      })
      .keyBy("word")
      .sum("count")

Key Selector方法指定Key

Key Selector方法通过使用者将数据作为输入,并返回所想要的Key,代码如下:

input.flatMap(_.split(","))
      .map(x => {
        WC(x, 1)
      })
      .keyBy(_.word)
      .sum("count")

Flink支持的数据类型

  • Java Tuples and Scala Case Classes
  • Java POJOs

_    1. 类必须为public_
_    2. 必须含有无参构造函数_
_    3. 有所有属性的get/set方法_
_    4. 支持序列化_

  • Primitive Types:Flink支持所有Java和Scala原生类型,例如:Integer,String和Double
  • Regular Classes:支持大部分Java和Scala的API和类,除开无法序列化的,例如IO流、文件指针等
  • Values:手动序列化和反序列化,通过实现org.apache.flinktypes.Value接口中的read和write方法
  • Hadoop Writables:实现org.apache.hadoop.Writable接口的类型
  • Special Types:Scala的Either,Option和Try

多种实现Transformation方法

Lambda Functions

就像前面的例子一样,使用Lambda表达式来实现自己的Transformation方法。

env.readTextFile("input/access.log")
        .map(x => {
          val splits = x.split(",")
          (splits(0).toLong,splits(1),splits(2).toInt)
        })
        .filter(_._3 > 5000)
        .print()

Rich Functions

所有的Transformation方法都有对应的Rich方法,例如map就对应RichMapFunction。Rich Function不仅让使用者自己实现相关方法(例如map、filter等),还提供生命周期方法(例如open、close)以及RunTimeContext用来传递参数、广播变量等。

//两种方式:匿名内部类以及创建继承相关Rich的新类
//注意map和filter
env.readTextFile("input/access.log")
        .map(new RichMapFunction[String,Access] {
          //setup methods
          override def open(parameters: Configuration): Unit = {
            println("~~~~~~~~~open~~~~~~~~~~~~")
            super.open(parameters)
          }

          override def map(input: String): Access = {
            val splits = input.split(",")
            Access(splits(0),splits(1),splits(2).toInt)
          }

          //teardown methods
          override def close(): Unit = super.close()
        })
      .filter(new MyFilterFun)
      .print().setParallelism(1)



class MyFilterFun extends RichFilterFunction[Access]{
    override def filter(input: Access): Boolean = {
      input.flow > 5000
    }
  }

累加器

Flink内置两类累加器,这些都是实现Accumulator接口:

  • IntCounter、LongCounter、DoubleCounter等
  • Histogram:离散直方图实现,内部实现为Integer到Integer的映射,用于监控。

如何使用

第一步,在你自己实现的transformation方法中创建一个累加器对象;
第二步,在Rich Function的open方法中注册累加器;
第三步,使用累加器;
第四步,使用JobExecutionResult对象查看累加器结果,代码如下:

//利用累加器来统计多少不符合标准的数据
env.readTextFile("input/access.txt")
      .map(new RichMapFunction[String, Access] {
        //创建累加器
        private val total = new IntCounter()
        private val wrong = new IntCounter()

        override def open(parameters: Configuration): Unit = {
          //注册累加器
          getRuntimeContext.addAccumulator("num_total", total)
          getRuntimeContext.addAccumulator("num_wrong", wrong)
        }

        override def close(): Unit = super.close()

        override def map(input: String): Access = {
          //使用累加器
          total.add(1)
          try {
            val splits = input.split(",")
            Access(splits(0), splits(1), splits(2).toInt)
          } catch {
            case e: Exception => {
              wrong.add(1)
              null
            }
          }
        }
      })

//查看结果
val myJobExecutionResult = env.execute(this.getClass.getSimpleName)
val resultTotal = myJobExecutionResult.getAccumulatorResult[Int]("num_total")
val resultWrong = myJobExecutionResult.getAccumulatorResult[Int]("num_wrong")
println("total num is " + resultTotal + ",wrong num is " + resultWrong)

上文所涉及到的示例代码以及数据皆已上传到Github上,如有需要请直接clone到本地直接运行,🔗链接如下:
github.com/liverrrr/fl…
上文如有错误或是纰漏,👏欢迎各位下方评论指出,大家一起交流学习📖