Flink:快速上手flink

目录

前言

Flink之WordCount

Flink流处理API

一、Environment

1.两种Environment

2.获取Environment的三种方式

二、Source

1.从集合中获取数据

2.从文本中获取流

3.从kafka中获取流

3.自定义source

三、Transform

1.转换算子

map

flatmap

filter

keyBy

滚动聚合算子

reduce

Split和Select

Connect和CoMap

Union

connect和union的区别

四、UDF函数类

普通函数类

富函数


 

前言

本文不介绍Flink简介以及其架构、特性等,仅是本人在学习flink过程中对代码方面做的一些笔记,如有不正确之处,欢迎指出。

Flink之WordCount

一个flink程序分为四个阶段:

首先用一段WordCount代码作为示例,说明一个Flink程序的基本流程。

    // 1.创建流处理环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    // 2.接收socket文本流
    val textDstream: DataStream[String] = env.socketTextStream("hd01", 12202)

    // 3.flatMap和Map需要引用的隐式转换
    import org.apache.flink.api.scala._
    // 4.进行计算
    val dataStream: DataStream[(String, Int)] = textDstream.flatMap(_.split("\\s")).filter(_.nonEmpty).map((_, 1)).keyBy(0).sum(1)
    // 5.打印、设置并行度
    dataStream.print().setParallelism(1)

    // 6.启动executor,执行任务
    env.execute("Socket stream word count")

代码解读:

1.创建执行环境(Environment)

和spark需要创建SparkContext一样,flink也需要创建一个ExecutionEnvironment。创建ExecutionEnvironment有三种方式,后面详细说。

2.创建流(Source)

根据数据源的不同,可能返回DataStream和DataSet两种数据类型。DataStream是流式数据,DataSet是类似sparkStreaming的批处理数据。数据源可以是文本文件、kafka、redis等,也可以是自定义的source,后面也会详细说。

3.导入隐式转换

DataStream是没有map、flatmap等方法的,需要导入隐式转换。

4.调用flatmap、map、keyby等算子进行计算(Transform)

其中flatmap、map、filter、keyby称为转化算子,sum则是滚动聚合算子(Rolling Aggregation)。需要注意的是:keyby所返回的数据不是DataStream,而是keyedStream,滚动聚合算子是针对keyedStream的每一个支流做聚合计算,DataStream是无法调用滚动聚合算子的。滚动聚合算子除了sum以外,还有min、max、minBy、maxBy。flink也可以自定义UTF函数,还有富函数、底层API、窗口与时间语义等后面都会详细说。

5.打印并设置并行度(Sink)

经过处理后的数据可以在sink阶段传递给不同地方,如控制台打印、kafka、mysql等。

另外,flink的每个算子后面都可以设置并行度,根据并行度以及API,会生成ExecutionGraph。flink中的执行图分为四层:StreamGraph -> JobGraph -> ExecutionGraph -> 物理执行图。本文不涉及这部分,后续在另一篇谈flink架构与特性中会说明。

6.启动任务

flink任务在环境对象调用execute方法时才会开始运行,参数为任务名。

Flink流处理API

一、Environment

1.两种Environment

Environment是Flink程序的入口,流处理和批处理的Environment是不同的,分别通过StreamExecutionEnvironment和ExecutionEnvironment来获得。

2.获取Environment的三种方式

方式一:getExecutionEnvironment

这种方式会根据程序运行环境自动获取ExecutionEnvironment。如果程序是独立调用的,则此方法返回本地执行环境;如果从命令行客户端调用程序以提交到集群,则此方法返回此集群的执行环境。是最常用的一种创建执行环境的方式。

//获取批处理执行环境
val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment
//获取流处理执行环境
val env = StreamExecutionEnvironment.getExecutionEnvironment

方式二:createLocalEnvironment

返回本地执行环境,需要在调用时指定默认的并行度。

//获取本地执行环境,并设置并行度为1
val env = StreamExecutionEnvironment.createLocalEnvironment(1)

方式三:createRemoteEnvironment

返回集群执行环境,将Jar提交到远程服务器。需要在调用时指定JobManager的IP和端口号,并指定要在集群中运行的Jar包。

//获取集群环境
val env = ExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname", port,"YOURPATH//wordcount.jar")

二、Source

source即是flink程序所处理的数据的来源,可以是一个集合(list)、文本文件、kafka等,也可以自定义source。

1.从集合中获取数据

    //创建执行环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    //从集合中获取流
    val stream1 = env.fromCollection(List("good","bey","word"))

2.从文本中获取流

//从文本中获取流,直接传入文件位置即可
val stream2 = env.readTextFile("FILE_PATH")

3.从kafka中获取流

从kafka中获取流,需要在pom中添加flink链接kafka的连接器依赖。


    org.apache.flink
    flink-connector-kafka-0.11_2.11
    1.7.2

在具体代码中,需要创建一个Properties对象用于传递kafka集群的相关信息及可选配置,用于实例化flink-kafka连接器,然后通过在addSource中传入连接器对象来获取kafka数据流。

//创建Properties对象,并设置相关参数
val properties = new Properties()
properties.setProperty("bootstrap.servers", "localhost:9092")
properties.setProperty("group.id", "consumer-group")
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
properties.setProperty("auto.offset.reset", "latest")


//在addSource中传入连接器对象,获取流
val stream3 = env.addSource(new FlinkKafkaConsumer011[String]("kafka_test", new SimpleStringSchema(), properties))

3.自定义source

从获取kafka流的代码中就可以看出,获取流可以通过调用执行环境对象的addSource方法来获取,且addSource方法的参数是一个SourceFunction。所以,只要自定义实现SourceFunction,就可以实现自定义source。

具体实现步骤如下:

1.创建运行标记、重写cancel方法和run方法。

2.在run方法中使用SourceContext的collect方法返回生成的数据。

class MySensorSource extends SourceFunction[String]{

    // flag: 表示数据源是否还在正常运行
    var running: Boolean = true
    
    //重现取消source方法,使其可以关闭
    override def cancel(): Unit = {
        running = false
    }

    //重写run方法,run方法是source生成数据的主要方法
    //SourceFunction.SourceContext[String]的泛型与SourceFunction一致,可以是自定义的类,此处为String
    override def run(ctx: SourceFunction.SourceContext[String]): Unit = {
        // 初始化一个随机数发生器
        val rand = new Random()


        while(running){
            // 生成随机数,作为ID
            var str = "ID" + rand.nextInt() 
            // 获取当前时间戳,拼接ID
            val curTime = System.currentTimeMillis()
            var str = str + curTime
            //返回给上下文环境
            ctx.collect(str )

            Thread.sleep(100)    
        }
    }
}

source生成的数据可以是自定义的类,这样可以更细粒度、更方便、更明确地进行计算,以上demo生成的是String,如果要生成自定义类型的数据,只需要指定代码中的两个泛型即可。

三、Transform

Transform阶段可以通过flink自带的算子和自定义的函数进行计算,下面对flink常用算子进行介绍。

1.转换算子

map

对每条数据进行转换,与spark中的map一致(以下flatmap和filter也是,掌握spark的可直接跳过)。

//将每条数据都乘以2
val streamMap = stream.map { x => x * 2 }

flatmap

将每条数据通过一定逻辑进行展开,并分割成多个数据。如:

//将每条数据进行分割,分割符尾空格
val streamFlatMap = stream.flatMap{
    x => x.split(" ")
}

filter

过滤,对每一条数据进行判断,返回结果为true的就留下,否则过滤掉。如:

//将每条数据对3取摩,留下哈希值等于1的数据
val streamFilter = stream.filter{
    x => x % 3 == 1
}

keyBy

将一个流在逻辑上拆分成不相交的分区,每个分区包含具有相同key的元素,在内部以hash的形式实现的。但是实际上还是一个流,只是相当于对每条数据打上标签而已。DataStream调用keyBy后,返回的是KeyedStream。

keyBy的参数可以指定按照哪个关键字或者元组的哪个位置的数据进行keyed。如:

// Key by field "someKey"
dataStream.keyBy("someKey") 
// Key by the first element of a Tuple(数组)
dataStream.keyBy(0) 

 

KeyedStream可以调用滚动聚合算子对key相同的数据进行计算,之后可以通过reduce算子返回一个计算后的DataStream。

滚动聚合算子

这些算子可以针对KeyedStream的每一个支流做聚合。

  1. sum():计算分区内指定列或属性的总和。
  2. min():找出分区中最小的值。
  3. max():找出分区中最大的值。
  4. minBy():与min不同的是,min只能找出最小的值,而minBy则是可以找出最小值的整条数据。
  5. maxBy():与max不同的是,max只能找出最小的值,而maxBy则是可以找出最大值的整条数据。

reduce

一个分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值,返回的流中包含每一次聚合的结果,而不是只返回最后一次聚合的最终结果。

也就是分别对应于keyBy、window/timeWindow 处理后的数据,根据ReduceFunction将元素与上一个reduce后的结果合并,产出合并之后的结果。

如文章开头的WordCount可以改为用reduce实现:

    val dataStream: DataStream[(String, Int)] = textDstream.flatMap(_.split("\\s")).filter(_.nonEmpty).map((_, 1)).keyBy(0).reduce((x,y)=>(x._0, x._0 + y._0))

其中x是上一批次的结果,y是当前数据。

Split和Select

Split是把DataStream转换成SplitStream。可以把一个流在逻辑上拆分成多个流,与keyBy一样,经过split后的流会转化为SplitStream,只是在逻辑上分为了多个而已,实际上仍是一个流,可以通过Select选择分支流,来将流彻底地分开。

Flink:快速上手flink_第1张图片

select可以选中SplitStream中的某个支流,并返回该流。

Flink:快速上手flink_第2张图片

如:将user类型的数据流根据user的ID分为奇数ID和偶数ID

//根据ID进行split,对流中每条数据进行判断,splitFunction的返回值必须是一个可迭代对象,且返回值就是支流的标签。
val splitStream = stream2
  .split( user => {
    if (user.id % 2 == 1){
        Seq("Odd ")
    }  else{
        Seq("Even")
    }
  } )

//通过select取出打上Odd标签的支流
val odd = splitStream.select("Odd ")
//通过select取出打上Even标签的支流
val even= splitStream.select("Even")
通过select取出打上Odd和Even标签的支流
val all = splitStream.select("Odd ", "Even")

Connect和CoMap

Connect可以把两个且只能是两个DataStream合并成一个ConnectedStream,而在该ConnectedStream内部,仍是两个相互独立的DataStream,两个DataStream的数据类型可以不一致。

Flink:快速上手flink_第3张图片

所以ConnectedStream的map、flatmap算子都有两个参数,分别是处理ConnectedStream内部两个DataStream的mapFunction或flatFunction,且两个流之间是可以共享状态的。

Flink:快速上手flink_第4张图片

//从kafka的不同topic中获取两个流
val stream1 = env.addSource(new FlinkKafkaConsumer011[String]("test1", new SimpleStringSchema(), properties))

val stream2 = env.addSource(new FlinkKafkaConsumer011[String]("test2", new SimpleStringSchema(), properties))

//connect
val connected = stream1.connect(stream2 )

//ConnectedStreams的map需要传递两个function
val coMap = connected.map(
    stream1=> (stream1._1, stream1._2),
    stream2 => (stream2._1, stream2._2)
)

Union

union是把两个或两个以上的DataStream真正合并成一个DataStream,但是DataStream的数据类型必须一致。

//从kafka的不同topic中获取两个流
val stream1 = env.addSource(new FlinkKafkaConsumer011[String]("test1", new SimpleStringSchema(), properties))

val stream2 = env.addSource(new FlinkKafkaConsumer011[String]("test2", new SimpleStringSchema(), properties))

//union
val stream3 = stream1 .union(stream2 )

connect和union的区别

1.connect只能合并两个流,union可以将两个或两个以上的流进行合并。

2.connect可以将数据类型不一致的流进行合并,union只能合并数据类型一致的流。

3.connect合并后,得到的是ConnectedStream,union合并后得到的仍是DataStream。ConnectedStream内部仍是两个流,可以对其分别调用不同的transformat算子进行转换,且两个流共享状态,也就是说两个流之间的计算结果是可以相互依赖的。

四、UDF函数类

在之前调用流的算子时,可以发现每个转换算子除了可以传入一个方法对象以外,还可以传入一个类对象,如:

不同算子可以传入的类对象也是不同的,如map算子对应MapFunction类,fliter算子对应FilterFunction。这些类即UDF函数类,通过UDF函数类可以更细粒度地完成转换操作。

UDF函数类分为两种:普通函数类(Function Classes)和富函数类(Rich Functions)。

每个算子都有一个对应的函数接口和富函数接口,自定义UDF函数只需实现接口并重写其中的方法即可。

两种函数的区别为:普通函数类只需实现一个算子对应的方法,如MapFunction实现map方法,FilterFunction实现filter方法等。而富函数类除了算子方法外,还需要实现open方法、close方法、getRuntimeContext等具有生命周期特征的方法。

普通函数类

以map算子为例,对应的UDF函数抽象类为MapFunction,实现该接口,并重写map方法:

class UdfTest extends MapFunction[String]{
  override def map(value: String): O = ???
}

UDF函数类与直接传入UDF函数方法的优点在于:函数方法会被每条数据调用一次;函数类只会实例化一次,每条数据调用的是函数类的map方法。所以一些只需创建一次,但每条数据计算都会使用到的变量或对象,就可以在创建函数类时创建,如jdbc等。

富函数

富函数可以说是同时实现了RichFunction和Function接口。如RichMapFunction继承了AbstractRichFunction抽象类,并实现了MapFunction,而AbstractRichFunction又实现了RichFunction接口,MapFunction实现了Function接口。生命周期方法就是在RichFunction中被定义的

public abstract class RichMapFunction extends AbstractRichFunction implements MapFunction {

	private static final long serialVersionUID = 1L;

	@Override
	public abstract OUT map(IN value) throws Exception;
}

Flink:快速上手flink_第5张图片

富函数的生命周期方法:

open():函数的初始化方法。在实际工作方法之前调用,因此适合一次性设置工作。如初始化一个连接器。

close():在最后一次调用主工作方法之后调用的,此方法可用于清理工作。如资源回收。

getRuntimeContext():获取RuntimeContext对象。

getIterationRuntimeContext():获取IterationRuntimeContext对象,多个RuntimeContext数量,等于并行度。

setRuntimeContext():设置函数的运行时上下文。在创建函数的并行实例时由框架调用。

你可能感兴趣的:(flink)