Apache Flink介绍、架构、原理以及实现

文章目录

一 Flink简介

1.1 什么是flink

Apache Flink是由Apache软件基金会开发的开源流处理框架,其核心是用Java和Scala编写的分布式流数据流引擎。Flink以数据并行和流水线方式执行任意流数据程序,Flink的流水线运行时系统可以执行批处理和流处理程序。此外,Flink的运行时本身也支持迭代算法的执行。

1.2 flink的特点

  • 批流统一

  • 支持高吞吐、低延迟、高性能的流处

  • 支持带有事件时间的窗口(Window)操作

  • 支持有状态计算的Exactly-once语义

  • 支持高度灵活的窗口(Window)操作,支持基于time、count、session窗口操作

  • 支持具有Backpressure功能的持续流模型

  • 支持基于轻量级分布式快照(Snapshot)实现的容错

  • 支持迭代计算

  • Flink在JVM内部实现了自己的内存管理

  • 支持程序自动优化:避免特定情况下Shuffle、排序等昂贵操作,中间结果有必要进行缓存

1.3 编程API

SQL/Table API :Flink对流数据的进一步封装,也实现了非常丰富的API

DataStream :对流数据进行开发,Flink封装了针对流数据操作的API

ProcessFunction :最底层开发,自定义实现功能模块
在这里插入图片描述

二 Flink架构

2.1 架构图

在这里插入图片描述

2.2 运行组件

  • JobManger(作业管理器)
  • TaskManger(任务管理器)
  • ResourceMager(资源管理器)
  • Dispacher(分发器)

JobManager

  • 控制一个应用程序执行的主进程,也就是说,每个应用程序都会被一个不同的JobManager所控制执行。
  • JobManager会先接收到要执行的应用程序,这个应用程序会包括:作业图(JobGraph)、 逻辑数据流图(logical dataflow graph)和打包了所有的类、库和其它资源的JAR包。
  • JobManager会把JobGraph转换成-个物理层面的数据流图,这个图被叫做"执行图”(ExecutionGraph) ,包含了所有可以并发执行的任务。
  • JobManager会向资源管理器(ResourceManager) 请求执行任务必要的资源,也就是任务管理器(TaskManager)上的插槽(slots) 。一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的TaskManager.上。而在运行过程中,JobManager会负责所有需要中央协调的操作,比如说检查点(checkpoints) 的协调。

TaskManger

  • Flink中的工作进程。通常在Flink中会有多个TaskManager运行, 每-个TaskManager都包含了-定数量的插槽(slots) 。插槽的数量限制了TaskManager能够执行的任务数量。
  • 启动之后, TaskManager会向资源管理器注册它的插槽; 收到资源管理器的指令后,TaskManager就会将一 个或者 多个插槽提供给JobManager调用。JobManager就可以向插槽分配任务(tasks) 来执行了。
  • 在执行过程中,一个TaskManager可以跟其它运行同一-应用程序的TaskManager交换数据。

ResourceMager

  • 主要负责管理任务管理器(TaskManager) 的插槽(slot)TaskManger插槽是Flink中定义的处理资源单元。
  • Flink为不同的环境和资源管理工具提供了不同资源管理器,比如YARN、小Mesos、K8s, 以及standalone部署。
  • 当JobManager申请插槽 资源时,ResourceManager会将 有空闲插槽的TaskManager分配给JobManager。如果ResourceManager没有足够的插槽来满足JobManager的请求,它还可以向资源提供平台发起会话,以提供启动TaskManager进程的容器。

Dispacher

  • 可以跨作业运行,它为应用提交提供了REST接口。
  • 当一个应用被提交执行时,分发器就会启动并将应用移交给一个JobManager。
  • Dispatcher也会启动一个Web UI,用来方便地展示和监控作业执行的信息。
  • Dispatcher在架构中可能并不是必需的,这取决于应用提交运行的方式。

2.3 关键词含义

Client

Flink用来提交任务的客户端,可以用命令提交,也可以用浏览器提交

Task

Task是一个阶段多个功能相同suntask的集合,类似spark中的taskset

Subtask

Subtask是flink中任务执行最小单元,是一个java类的实例,这份java类中有属性和方法,完成具体的计算逻辑

Operator chain

没有shuffle的多个算子合并在一个subtask中就形成了Operator chain,类似spark中的pipeline

Slot

Flink中计算资源进行隔离的单元,一个slot中可以运行多个subtask,但是这些subtask必须是来自同一个job的不同task的subtask

State

Flink任务运行过程中计算的中间结果

Checkpoint

Flink用来将中间结果持久化的指定的存储系统的一种定期执行的机制

stateBackend

Flink用来存储中间计算结果的存储系统,flink支持三种statebackend。分别是memory,fsbackend,rocksDB

三 Flink原理

3.1 任务

  • 所有的Flink程序都是由三部分组成的: Source 、Transformation 和Sink。
  • Source负责读取数据源,Transformation 利用各种算子进行处理加工,Sink 负责输出

3.2 任务提交流程

首先由Dispatcher接收提交的应用,然后启动JobManger,JobManger根据自己生成的执行图去向ResourceManger申请对应的Slots数量,ResourceManger接收到请求后会根据向自己注册的TaskManager所有Slots数量去判断是否有足够的资源执行任务,然后满足资源需求,ResourceManger就会发送Slots所存在的TaskManager申请资源的JobManger,然后TaskManger连接JobManger,JobManager则分配执行任务,然后TaskManager开始执行分配的任务,不同TaskManager在执行过程中可能存在交换数据

在这里插入图片描述

3.3 任务提交流程(YARN版)

首先客户端上传Flink的Jar包和配置,然后提交job至ResourceManager(yarn),由ResourceManager启用ApplicationMaster,在ApplicatiionMaster内启动Flink组件JobManager、ResourceManager(flink) ,然后JobManager向ResourceManager(flink)申请所需的资源,ResourceManager(flink)在向ResourceManager(yarn)申请资源,ResourceManager(yarn),根据申请资源数量,在一定数量的NodeManager节点内启动的TaskManager,然后TaskManager向ResourceManger(flink)注册Slots,然后ResourceManager向TaskManager发送申请Slots的JobManager,然后TaskManger连接JobManager,JobManager则发送分配的任务,TaskManager就会根据分配的任务开始执行,其实在yarn管理资源的时候,就是当接收到一个Job任务时,就会启动一个Flink集群,当任务处理完时,这个集群资源也就释放了

在这里插入图片描述

3.4 任务执行图

  • Flink中的执行图可以分成四层: StreamGraph -> JobGraph -> ExecutionGraph->物理执行图
  • StreamGraph:是根据用户通过Stream API编写的代码生成的最初的图。用来表示程序的拓扑结构。
  • JobGraph: StreamGraph经过优化后生成了JobGraph,提交给JobManager的数据结构。主要的优化为,将多个符合条件的节点chain在一起作为一个节点
  • ExecutionGraph: JobManager 根据JobGraph生成ExecutionGraph。ExecutionGraph是JobGraph的并行化版本,是调度层最核心的数据结构。
  • 物理执行图: JobManager 根据ExecutionGraph对Job进行调度后,在各个TaskManager.上部署Task后形成的“图”,并不是一个具体的数据结构。

在这里插入图片描述

3.5 任务链

  • Flink采用了一种称为任务链的优化技术,可以在特定条件下减少本地通信的开销。为了满足任务链的要求,必须将两个或多个算子设为相同的并行度,并通过本地转发(localforward)的方式进行连接
  • 相同并行度的one to one操作,Flink 这样相连的算子链接在一起形成
  • 一个task,原来的算子成为里面的subtask
  • 并行度相同、并且是one-to-one操作,两个条件缺一不可

disableChaining

  • 使用disableChaining的算子禁止跟前后合并任务链

startNewChain

  • 使用startNewChain的算子指从当前算子开始合并任务链,不与该算子之前的任务合并

slotSharingGroup(共享组)

  • 指定任务的共享组,当一个Job中的存在比较复杂且非常耗资源的操作时,可以让该操作独享一个slot,不与其他共享组重合

四 Flink和其他框架对比

框架 优点 缺点
Storm 低延迟 吞吐量低、不能保证exactly-once、编程API不丰富
Spark Streaming 吞吐量高、可以保证exactly-once、编程API丰富 延迟较高
Flink 低延迟、吞吐量高、可以保证exactly-once、编程API丰富 快速迭代中,API变化比较快

五 Flink安装部署

下载地址

解压安装

  • 执行:tar -zxvf flink-1.10.1-bin-scala_2.11.tgz

配置文件

  • 配置解压后conf/下的flink-conf.yaml文件:vi conf/flink-conf.yaml
# JobManager runs.
#指定jobmanger地址
jobmanager.rpc.address: localhost

# The RPC port where the JobManager is reachable.
#内部通信端口号
jobmanager.rpc.port: 6123


# The heap size for the JobManager JVM
#配置JobManager JVM堆内存大小
jobmanager.heap.size: 1024m


# The heap size for the TaskManager JVM
#配置TaskManager JVM堆内存大小
taskmanager.heap.size: 1024m


# The number of task slots that each TaskManager offers. Each slot runs one parallel pipeline.
#配置taskmanager默认Slots数
taskmanager.numberOfTaskSlots: 1

# The parallelism used for programs that did not specify and other parallelism.
#配置默认并行度
parallelism.default: 1

  • 修改conf/下workers文件,指定所有Taskmanager地址

将配置好的Flink发送至其他节点

  • 执行:scp /opt/flink/* 用户名@主机名:$PWD/opt/flink

启动flink

  • 执行:./bin/start-cluster.sh

查看进程

#JobManager进程
StandaloneSessionClusterEntrypoint
#TaskManager进程
TaskManagerRunner 

命令行提交任务

#参数说明:
#-m指定主机名后面的端口为JobManager的REST的端口,而不是RPC的端口,RPC通信端口是6123
#-p 指定是并行度
#-c 指定main方法的全类名
./bin/flink run -m 192.168.xx.xx:8081 -p 4 -c com.wedoctor.flink.WordCount /home/pgxl/liuzc/flink-project-scala-1.0.jar --hostname 192.168.xx.xx --port 7777

访问Web页面

  • 地址:192.168.**.**:8081

Web提交任务

  • 上传Jar包
    在这里插入图片描述

  • 选择打好的Jar包
    在这里插入图片描述

  • 查看上传的jar包并进行配置
    在这里插入图片描述

  • 指定Class以及配置host与port
    在这里插入图片描述

  • 查看Plan,没有问题就可以submit了
    在这里插入图片描述

  • 然后就可以看到以下的界面,Task允许状态都是正常的
    在这里插入图片描述

测试数据

  • 在端口7777下输入数据
hello word
hello Flink
hello java
hello scala
hello word
hello kafka
  • 查看输出数据
    在这里插入图片描述
    在这里插入图片描述

六 Flink第一个示例

  • 使用Idea创建Maven项目
  • 添加Flink依赖
	<!- 根据自己的需求添加依赖这里仅需要以下两个,修改对应的scala版本,以及所想用的flink版本->
	<dependency>
      <groupId>org.apache.flink</groupId>
      <artifactId>flink-scala_2.11</artifactId>
      <version>1.10.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.flink</groupId>
      <artifactId>flink-streaming-scala_2.11</artifactId>
      <version>1.10.2</version>
    </dependency>

  • 创建scala object
import org.apache.flink.streaming.api.scala._

object FlinkTest {
    
    
  def main(args: Array[String]): Unit = {
    
    
    //创建flink环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置并行度
    env.setParallelism(4)
    //创建流
    val stream: DataStream[String] = env.fromCollection(List(
      "hello java",
      "hello flink",
      "hello java",
      "hello flink"

    ))
    stream.print()
    env.execute("demo")
  }

}
  • 输出结果
2> hello flink
4> hello flink
3> hello java
1> hello java

七 Flink Source数据源

7.1 文件做为数据源

  • 格式
env.readTextFile("路径/文件名.格式")
  • 示例
import org.apache.flink.streaming.api.scala._

//字符串转对象
case class WaterSensor(id:String,ts:Long,vc:Double)
object SourceFile {
    
    
  def main(args: Array[String]): Unit = {
    
    
  //创建流执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //设置并行度
    env.setParallelism(1)
	//读取本地文件数据
    val fileDS: DataStream[String] = env.readTextFile("in/StringToClass.txt")
	//直接输出至控制台
    fileWater.print()
    //执行
    env.execute("textFile")
  }
}

7.2 端口做为数据源

  • 格式
env.socketTextStream("192.168.**.**",端口号)
  • 示例
import org.apache.flink.api.java.utils.ParameterTool
import org.apache.flink.streaming.api.scala._

object SourcePort {
    
    
  def main(args: Array[String]): Unit = {
    
    
  
    //创建流处理环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    
    //指定端口地址与端口号
    val inputDataStream: DataStream[String] = env.socketTextStream("192.168.**.**",7777)
    
    //对获取的数据进行WorldCount
    val resultDataSet: DataStream[(String, Int)] = inputDataStream
      .flatMap(_.split(" "))
      .filter(_.nonEmpty)
      .map((_,1))
      .keyBy(0)
      .sum(1)

    //输出结果
    resultDataSet.print()
    
    //执行
    env.execute("port")
  }
}

启动程序后,在可以在本机或者Linux上启用端口,命令:nc -lk [端口号]

7.3 Kafka做为数据源

  • 格式
//prop是配置连接Kafka的信息
env.addSource(new FlinkKafkaConsumer[数据类型]([Topic名],new SimpleStringSchema(),prop))
  • 示例

读取kafka需要添加依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_2.11</artifactId>
<version>1.10.2</version>
</dependency>

import java.util.Properties
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer
import org.apache.kafka.clients.consumer.ConsumerConfig

object SourceKafka {
    
    
  def main(args: Array[String]): Unit = {
    
    
  	//创建流执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    
    //创建Properties对象,设置连接kafka信息
    val prop = new Properties()
    //Kafka连接地址与端口
    prop.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.**.**:9092")
    //消费者组名称
    prop.setProperty(ConsumerConfig.GROUP_ID_CONFIG,"Kafka_To_Flink")
    //KEY的数据类型
    prop.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer")
    //VALLUE的数据类型
    prop.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer")
    //配置读取策略
    //earliest: 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
	//latest: 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
	//none: topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
    prop.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest")
		
	//读取Kafka中名为‘sensor’的Toipc中的数据
    val KafkaToStream: DataStream[String] = env.addSource(new FlinkKafkaConsumer[String]("sensor", new SimpleStringSchema(), prop))
    //直接输出至控制台
    KafkaToStream.print()
    //执行
    env.execute("kafka")
  }
}

7.4 自定义数据源(示例:MySQL)

  • 格式
import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.scala._
import scala.util.Random

//自定义类型
case class WaterSensor(id:String,ts:Long,vc:Double)
//继承SourceFunction类,指定输出的数据类型
class MySensorSource extends SourceFunction[WaterSensor]{
    
    
  //重写run方法
  override def run(sourceContext: SourceFunction.SourceContext[Source.WaterSensor]): Unit = {
    
    
  	//具体实现
  	...	
  }
  
  override def cancel(): Unit = ???
}

object SourceMy{
    
    
  def main(args: Array[String]): Unit = {
    
    
  //创建流执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //使用自定义数据源
    val mydefDStream: DataStream[Source.WaterSensor] = env.addSource(new MySensorSource)
    //直接输出
    mydefDStream.print()
    //执行
    env.execute("abc")


  }
}
  • 示例:读取MySQL指定表的数据

添加MySQL依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>

import java.sql.{
    
    Connection, DriverManager,  ResultSet}
import org.apache.flink.streaming.api.functions.source.SourceFunction
import org.apache.flink.streaming.api.scala._

//自定义类型
case class table_flink_to_mysql(id:String,cnt:Int)
//实现自定义Source
class MysqlSource(Database:String,Table:String) extends SourceFunction[table_flink_to_mysql]{
    
    

  //配置连接MySQL信息
  private val driver="com.mysql.jdbc.Driver"
  private val url="jdbc:mysql://192.168.**.**:3306/"+Database
  private val username="root"
  private val password="root123"

  private var connerction: Connection = null

  //获取connerction对象
  def getConnerction():Unit={
    
    
    Class.forName(driver)
    connerction = DriverManager.getConnection(url,username,password)
  }

  //查询语句
  val selectTable="select * from "+Table
  
  //重写run方法
  override def run(sourceContext: SourceFunction.SourceContext[table_aaa]): Unit = {
    
    
    getConnerction
    var rs:ResultSet=connerction.createStatement().executeQuery(selectTable)
    while(rs.next()){
    
    
      //读取Table中的字段
      var id =rs.getString("id")
      var cnt=rs.getInt("cnt")
      //以包装成table_flink_to_mysql类型输出
      sourceContext.collect(table_flink_to_mysql(id,cnt))
    }
  }
  override def cancel(): Unit = ???
}

object SourceMySQL{
    
    
  def main(args: Array[String]): Unit = {
    
    
    //创建流执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //使用自定义数据源,传入参数,Database、Table
    val mydefDStream: DataStream[table_aaa] = env.addSource(new MysqlSource("test","flink_to_mysql"))
    //输出拉取到的数据
    mydefDStream.print()
    //执行
    env.execute("readMySQL")
  }
}

八 Flink Transform算子

Map

映射:将数据流中的数据进行转换, 形成新的数据流,消费一个元素并产出一个元素

  • 参数:Scala匿名函数或MapFunction
  • 返回:DataStream
import org.apache.flink.streaming.api.scala._

object Transfrom_map {
    
    

  def main(args: Array[String]): Unit = {
    
    
      //1.创建执行的环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    //2.从集合中读取数据
    val sensorDS: DataStream[WaterSensor] = env.fromCollection(
      // List(1,2,3,4,5)
      List(
        WaterSensor("ws_001", 1577844001, 45.0),
        WaterSensor("ws_002", 1577844015, 43.0),
        WaterSensor("ws_003", 1577844020, 42.0)
      )
    )

    val sensorDSMap = sensorDS.map(x => (x.id+"_1",x.ts+"_1",x.vc + 1))

    //3.打印
    sensorDSMap.print()
    //4.执行
    env.execute("sensor")

  }

  /**
   * 定义样例类:水位传感器:用于接收空高数据
   *
   * @param id 传感器编号
   * @param ts 时间戳
   * @param vc 空高
   */
  case class WaterSensor(id: String, ts: Long, vc: Double)


}

自定义MapFunction

  • Flink为每一个算子的参数都至少提供了Scala匿名函数和函数类两种的方式,其中如果使用函数类作为参数的话,需要让自定义函数继承指定的父类或实现特定的接口。例如:MapFunction
//sensor-data.log 文件数据
sensor_1,1549044122,10
sensor_1,1549044123,20
sensor_1,1549044124,30
sensor_2,1549044125,40
sensor_1,1549044126,50
sensor_2,1549044127,60
sensor_1,1549044128,70
sensor_3,1549044129,80
sensor_3,1549044130,90
sensor_3,1549044130,100

import org.apache.flink.streaming.api.scala._

object SourceFileMap {
    
    

  def main(args: Array[String]): Unit = {
    
    
    //1.创建执行的环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //2.从指定路径获取数据
    val fileDS: DataStream[String] = env.readTextFile("input/sensor-data.log")

    val MapDS = fileDS.map(
      lines => {
    
    
        //更加逗号切割 获取每个元素
        val datas: Array[String] = lines.split(",")
        WaterSensor(datas(0), datas(1).toLong, datas(2).toInt)
      }
    )
	//使用继承了MapFunction的类
	//fileDS.MyMapFunction

    //3.打印
    MapDS.print()

    //4.执行
    env.execute("map")

  }

  /**
   * 定义样例类:水位传感器:用于接收空高数据
   *
   * @param id 传感器编号
   * @param ts 时间戳
   * @param vc 空高
   */
  case class WaterSensor(id: String, ts: Long, vc: Double)

 /**
   * 自定义继承 MapFunction
   * MapFunction[T,O]
   * 自定义输入和输出
   *
   */
  class MyMapFunction extends MapFunction[String,WaterSensor]{
    
    
    override def map(t: String): WaterSensor = {
    
    

      val datas: Array[String] = t.split(",")

      WaterSensor(datas(0),datas(1).toLong,datas(2).toInt)
    }
  }

}

RichMapFunction

所有Flink函数类都有其Rich版本。它与常规函数的不同在于,可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。也有意味着提供了更多的,更丰富的功能。例如:RichMapFunction

import org.apache.flink.api.common.functions.{
    
    MapFunction, RichMapFunction}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.scala._

object Transform_RichMapFunction {
    
    

  def main(args: Array[String]): Unit = {
    
    
    //1.创建执行的环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //2.从指定路径获取数据
    val sensorDS: DataStream[String] = env.readTextFile("input/sensor-data.log")

    val myMapDS: DataStream[WaterSensor] = sensorDS.map(new MyRichMapFunction)

    //3.打印
    myMapDS.print()

    //4.执行
    env.execute("map")

  }

  /**
   * 自定义继承 RicMapFunction
   * RicMapFunction[T,O]
   * 自定义输入和输出
   *
   */
  class MyRichMapFunction extends RichMapFunction[String,WaterSensor]{
    
    

    override def map(value: String): WaterSensor = {
    
    
      val datas: Array[String] = value.split(",")
      //      WaterSensor(datas(0), datas(1).toLong, datas(2).toInt)
      WaterSensor(getRuntimeContext.getTaskName, datas(1).toLong, datas(2).toInt)
    }

    // 富函数提供了生命周期方法
    override def open(parameters: Configuration): Unit = {
    
    }

    override def close(): Unit = {
    
    }


  }

  /**
   * 定义样例类:水位传感器:用于接收空高数据
   *
   * @param id 传感器编号
   * @param ts 时间戳
   * @param vc 空高
   */
  case class WaterSensor(id: String, ts: Long, vc: Double)

}

Rich Function有一个生命周期的概念。典型的生命周期方法有:

  • open()方法是rich function的初始化方法,当一个算子例如map或者filter被调 用之前open()会被调用
  • close()方法是生命周期中的最后一个调用的方法,做一些清理工作
  • getRuntimeContext()方法提供了函数的RuntimeContext的一些信息,例如函数执行 的并行度,任务的名字,以及state状态

flatMap

  • 扁平映射:将数据流中的整体拆分成一个一个的个体使用,消费一个元素并产生零到多个元素
  • 参数:Scala匿名函数或FlatMapFunction
  • 返回:DataStream
import org.apache.flink.streaming.api.scala._

object Transform_FlatMap {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 1.创建执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 2.读取数据
    val listDS: DataStream[List[Int]] = env.fromCollection(
      List(
        List(1, 2, 3, 4),
        List(5, 6, 7,1,1,1)
      )
    )

    val resultDS: DataStream[Int] = listDS.flatMap(list => list)

    resultDS.print()
    // 4. 执行
    env.execute()
  }


}

filter

  • 过滤:根据指定的规则将满足条件(true)的数据保留,不满足条件(false)的数据丢弃
  • 参数:Scala匿名函数或FilterFunction
  • 返回:DataStream
import org.apache.flink.streaming.api.scala._

object Transform_Filter {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 1.创建执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 2.读取数据
    val listDS: DataStream[List[Int]] = env.fromCollection(
      List(
        List(1, 2, 3, 4,1, 2, 3, 4),
        List(5, 6, 7,1,1,1,1, 2, 3, 4,1, 2, 3, 4),
        List(1, 2, 3, 4),
        List(5, 6, 7,1,1,1),
        List(1, 2, 3, 4),
        List(5, 6, 7,1,1,1)
      )
    )
    // true就留下,false就抛弃
    listDS.filter(num => {
    
    
      num.size>5
      })
      .print("filter")
    // 4. 执行
    env.execute()
  }
}

keyBy

  • 在Spark中有一个GroupBy的算子,用于根据指定的规则将数据进行分组,在flink中也有类似的功能,那就是keyBy,根据指定的key对数据进行分流
  • 分流:根据指定的Key将元素发送到不同的分区,相同的Key会被分到一个分区(这里分区指的就是下游算子多个并行节点的其中一个)。keyBy()是通过哈希来分区的
import org.apache.flink.streaming.api.scala._

object Transform_KeyBy {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 1.创建执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 2.读取数据
    val sensorDS: DataStream[String] = env.readTextFile("input/sensor-data.log")

    //3.转换为样例类
    val mapDS = sensorDS.map(
      lines => {
    
    
        val datas = lines.split(",")
        WaterSensor(datas(0), datas(1).toLong, datas(2).toInt)
      }
    )

    // 4. 使用keyby进行分组
    // TODO 关于返回的key的类型:
    // 1. 如果是位置索引 或 字段名称 ,程序无法推断出key的类型,所以给一个java的Tuple类型
    // 2. 如果是匿名函数 或 函数类 的方式,可以推断出key的类型,比较推荐使用
    // *** 分组的概念:分组只是逻辑上进行分组,打上了记号(标签),跟并行度没有绝对的关系
    //      同一个分组的数据在一起(不离不弃)
    //      同一个分区里可以有多个不同的组

    //        val sensorKS: KeyedStream[WaterSensor, Tuple] = mapDS.keyBy(0)
    //    val sensorKS: KeyedStream[WaterSensor, Tuple] = mapDS.keyBy("id")
    val sensorKS: KeyedStream[WaterSensor, String] = mapDS.keyBy(_.id)
    //    val sensorKS: KeyedStream[WaterSensor, String] = mapDS.keyBy(
    //      new KeySelector[WaterSensor, String] {
    
    
    //        override def getKey(value: WaterSensor): String = {
    
    
    //          value.id
    //        }
    //      }
    //    )

    sensorKS.print().setParallelism(5)

    // 4. 执行
    env.execute()
  }

  /**
   * 定义样例类:水位传感器:用于接收空高数据
   *
   * @param id 传感器编号
   * @param ts 时间戳
   * @param vc 空高
   */
  case class WaterSensor(id: String, ts: Long, vc: Double)
}

shuffle

  • 打乱重组(洗牌):将数据按照均匀分布打散到下游
  • 返回:DataStream
import org.apache.flink.streaming.api.scala._

object Transform_Shuffle {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 1.创建执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 2.读取数据
    val sensorDS: DataStream[String] = env.readTextFile("input/sensor-data.log")

    val shuffleDS = sensorDS.shuffle

    sensorDS.print("data")

    shuffleDS.print("shuffle")
    // 4. 执行
    env.execute()
  }
}

split

  • 在某些情况下,我们需要将数据流根据某些特征拆分成两个或者多个数据流,给不同数据流增加标记以便于从流中取出。
import org.apache.flink.streaming.api.scala._

object Transform_Split {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 1.创建执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 2.读取数据
    val sensorDS: DataStream[String] = env.readTextFile("input/sensor-data.log")

    // 3.转换成样例类
    val mapDS: DataStream[WaterSensor] = sensorDS.map(
      lines => {
    
    
        val datas: Array[String] = lines.split(",")
        WaterSensor(datas(0), datas(1).toLong, datas(2).toInt)
      }
    )
    val splitDS: SplitStream[WaterSensor] = mapDS.split(
      sensor => {
    
    
        if (sensor.vc < 40) {
    
    
          Seq("info")
        } else if (sensor.vc < 80) {
    
    
          Seq("warn")
        } else {
    
    
          Seq("error")
        }
      }
    )
    val errorDS: DataStream[WaterSensor] = splitDS.select("error")
    val warnDS: DataStream[WaterSensor] = splitDS.select("warn")
    val infoDS: DataStream[WaterSensor] = splitDS.select("info")

    infoDS.print("info")
    warnDS.print("warn")
    errorDS.print("error")

    // 4. 执行
    env.execute()
  }

  /**
   * 定义样例类:水位传感器:用于接收空高数据
   *
   * @param id 传感器编号
   * @param ts 时间戳
   * @param vc 空高
   */
  case class WaterSensor(id: String, ts: Long, vc: Double)
}

connect

  • 在某些情况下,我们需要将两个不同来源的数据流进行连接,实现数据匹配,比如订单支付和第三方交易信息,这两个信息的数据就来自于不同数据源,连接后,将订单支付和第三方交易信息进行对账,此时,才能算真正的支付完成
  • Flink中的connect算子可以连接两个保持他们类型的数据流,两个数据流被Connect之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立。
import org.apache.flink.streaming.api.scala._

object Transform_Connect {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 1.创建执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 2.读取数据
    val sensorDS: DataStream[String] = env.readTextFile("input/sensor-data.log")

    // 3.转换成样例类
    val mapDS: DataStream[WaterSensor] = sensorDS.map(
      lines => {
    
    
        val datas: Array[String] = lines.split(",")
        WaterSensor(datas(0), datas(1).toLong, datas(2).toInt)
      }
    )

    // 4. 从集合中再读取一条流
    val numDS: DataStream[Int] = env.fromCollection(List(1, 2, 3, 4, 5, 6))

    val resultCS: ConnectedStreams[WaterSensor, Int] = mapDS.connect(numDS)

    // coMap表示连接流调用的map,各自都需要一个 function
    resultCS.map(
      sensor=>sensor.id,
      num=>num+1
    ).print()

    // 4. 执行
    env.execute()
  }

  /**
   * 定义样例类:水位传感器:用于接收空高数据
   *
   * @param id 传感器编号
   * @param ts 时间戳
   * @param vc 空高
   */
  case class WaterSensor(id: String, ts: Long, vc: Double)
}

union

connect与 union 区别:

  • union之前两个流的类型必须是一样,connect可以不一样
  • onnect只能操作两个流,union可以操作多个。
import org.apache.flink.streaming.api.scala._

object Transform_Union {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 1.创建执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 2. 从集合中读取流
    val num1DS: DataStream[Int] = env.fromCollection(List(1, 2, 3, 4))
    val num2DS: DataStream[Int] = env.fromCollection(List(7, 8, 9, 10))
    val num3DS: DataStream[Int] = env.fromCollection(List(17, 18, 19, 110))

    // TODO union 真正将多条流合并成一条流
    // 合并的流,类型必须一致
    // 可以合并多条流,只要类型一致
    num1DS.union(num2DS).union(num3DS)
      .print()
    

    // 4. 执行
    env.execute()
  }

  /**
   * 定义样例类:水位传感器:用于接收空高数据
   *
   * @param id 传感器编号
   * @param ts 时间戳
   * @param vc 空高
   */
  case class WaterSensor(id: String, ts: Long, vc: Double)
}

Operator

Flink作为计算框架,主要应用于数据计算处理上, 所以在keyBy对数据进行分流后,可以对数据进行相应的统计分析

  • 滚动聚合算子(Rolling Aggregation)这些算子可以针对KeyedStream的每一个支流做聚合。执行完成后,会将聚合的结果合成一个流返回,所以结果都是DataStream sum()

process

  • Flink在数据流通过keyBy进行分流处理后,如果想要处理过程中获取环境相关信息,可以采用process算子自定义实现 1)继承KeyedProcessFunction抽象类,并定义泛型:[KEY, IN, OUT]
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

object Transform_Process {
    
    

  def main(args: Array[String]): Unit = {
    
    

    // 1.创建执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    // 2.读取数据
    val sensorDS: DataStream[String] = env.readTextFile("input/sensor-data.log")

    // 3.转换成样例类
    val mapDS: DataStream[WaterSensor] = sensorDS.map(
      lines => {
    
    
        val datas: Array[String] = lines.split(",")
        WaterSensor(datas(0), datas(1).toLong, datas(2).toInt)
      }
    )
    //按照ID  进行分组
    val sensorKS: KeyedStream[WaterSensor, String] = mapDS.keyBy(_.id)

    sensorKS.process(new MyKeyedProcessFunction)

    // 4. 执行
    env.execute()
  }

  // 自定义KeyedProcessFunction,是一个特殊的富函数
  // 1.实现KeyedProcessFunction,指定泛型:K - key的类型, I - 上游数据的类型, O - 输出的数据类型
  // 2.重写 processElement方法,定义 每条数据来的时候 的 处理逻辑
  class MyKeyedProcessFunction extends KeyedProcessFunction[String, WaterSensor, String] {
    
    
    /**
     * 处理逻辑:来一条处理一条
     *
     * @param value 一条数据
     * @param ctx   上下文对象
     * @param out   采集器:收集数据,并输出
     */
    override def processElement(value: WaterSensor, ctx: KeyedProcessFunction[String, WaterSensor, String]#Context, out: Collector[String]): Unit = {
    
    
      out.collect("我来到process啦,分组的key是="+ctx.getCurrentKey+",数据=" + value)
      // 如果key是tuple,即keyby的时候,使用的是 位置索引 或 字段名称,那么key获取到是一个tuple
      //      ctx.getCurrentKey.asInstanceOf[Tuple1].f0 //Tuple1需要手动引入Java的Tuple
    }
  }

  /**
   * 定义样例类:水位传感器:用于接收空高数据
   *
   * @param id 传感器编号
   * @param ts 时间戳
   * @param vc 空高
   */
  case class WaterSensor(id: String, ts: Long, vc: Double)
}

九 Flink Sink输出端

9.1 写入本地文件

  • 格式
dataStream.writeAsCsv("路径/文件名")
  • 示例
import org.apache.flink.streaming.api.scala.{
    
    DataStream, StreamExecutionEnvironment}

object SinkCSV {
    
    
  def main(args: Array[String]): Unit = {
    
    
  
    //创建流执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    
	//读取本地文件数据
    val dataStream: DataStream[String] = env.readTextFile("in/a.txt")

    //以csv格式写入本地文件
    dataStream.writeAsCsv("in/b.txt")

	//执行
    env.execute()
  }

9.2 写入Kafka

  • 格式
	//KafkaToStream是DataStream
    KafkaToStream.addSink(new FlinkKafkaProducer[数据类型]("192.168.**.**:9092","Topic",new SimpleStringSchema()))
  • 示例
import java.util.Properties
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.connectors.kafka.{
    
    FlinkKafkaConsumer, FlinkKafkaProducer}
import org.apache.kafka.clients.consumer.ConsumerConfig

object  SinkKafka {
    
    
  def main(args: Array[String]): Unit = {
    
    
  
    //创建流执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    
    //读取本地文件数据
    val dataStream: DataStream[String] = env.readTextFile("in/a.txt")
    
    //将获取的数据传到kafka
    KafkaToStream.addSink(new FlinkKafkaProducer[String]("192.168.**.**:9092","senserout",new SimpleStringSchema()))
    
    //执行
    env.execute("kafkademo2")
  }
}

9.3 自定义Sink(示例:写入MySQL)

  • 格式
//自定义Sink,继承RichSinkFunction类
class MyRichSinkToMySQL extends RichSinkFunction[student] {
    
    

  override def open(parameters: Configuration): Unit = {
    
    
    //建立连接
    ...
  }

  override def invoke(value: student): Unit = {
    
    
    //实现
	...
  }

  override def close(): Unit = {
    
    
    //关闭连接
	...
}
  • 示例
import java.sql.{
    
    Connection, DriverManager, PreparedStatement}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction
import org.apache.flink.streaming.api.scala._

//自定义类型Student
case class student(id:Int,name:String)
object SinkMySQL {
    
    
  def main(args: Array[String]): Unit = {
    
    
  //创建流执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
	//读取本地文件数据
    val dataStream: DataStream[String] = env.readTextFile("in/a.txt")
	//转换成自定义类型
    val students: DataStream[student] = dataStream.map(x => {
    
    
      val strings: Array[String] = x.split(",")
      student(strings(0).toInt, strings(1))
    })
	//指定Sink,输出至MySQL
    students.addSink(new MyRichSinkToMySQL)
    //执行
    env.execute()
  }

}

//自定义Sink输出至MySQL
class MyRichSinkToMySQL extends RichSinkFunction[student] {
    
    
  var conn: Connection =_
  var insertTmp: PreparedStatement =_
  var updateTmp: PreparedStatement =_

  override def open(parameters: Configuration): Unit = {
    
    
    //建立连接
    //配置MySQL地址,用户名,密码
    conn=DriverManager.getConnection("jdbc:mysql://192.168.**.**:3306/test","root","root123")
    //预写insert语句
    insertTmp=conn.prepareStatement("insert into aaa (id,name) values (?,?)")
    //预写uodate语句
    updateTmp=conn.prepareStatement("update aaa set name = ? where id = ? ")
  }

  override def invoke(value: student): Unit = {
    
    
  	//执行更新语句
    updateTmp.setString(1,value.name)
    updateTmp.setInt(2,value.id)
    //执行
    updateTmp.execute()

    //判断更新条数,如果为0,说明没有初始值,改为执行插入语句
    if (updateTmp.getUpdateCount == 0 ){
    
    
      insertTmp.setInt(1,value.id)
      insertTmp.setString(2,value.name)
      insertTmp.execute()
    }
  }

  override def close(): Unit = {
    
    
    //关闭连接
    insertTmp.close()
    updateTmp.close()
    conn.close()
  }
}

十 Flink window窗口

10.1 什么是窗口?

在这里插入图片描述

  • 一般真实的流都是无界的,怎样处理无界的数据?
  • 可以把无限的数据流进行切分,得到有限的数据集进行处理一也就是得到有界流
  • 窗口(window) 就是将无限流切割为有限流的一种方式,它会将流数据分发到有限大小的桶(bucket) 中进行分析

10.2 窗口类型

时间窗口(Time Window)

  • 滚动时间窗C]
  • 滑动时间窗口
  • 会话窗口

计数窗口(Count Window)

  • 滚动计数窗口
  • 滑动计数窗口

10.3 滚动窗口

在这里插入图片描述

  • 将数据依据固定的窗度长度对数据进行切分
  • 时间对齐,窗口长度固定,没有重叠

10.4滑动窗口

在这里插入图片描述

  • 将数据依据固定的窗C长度对数据进行切分
  • 时间对挤,窗口长度固定,没有重叠

10.4 会话窗口

在这里插入图片描述

  • 由一系列事件组合一一个指定时间长度的timeout间隙组成,也就是一段时间没有接收到新数据就会生成新的窗口
  • 特点:时间无对挤

10.5 窗口API

  • 窗口分配器->window()方法
  • 我们可以用.window()来定义一个窗口,然后基于这个window去做一些聚合或者其它处理操作。注意window()方法必须在keyBy之后才能用。
  • link 提供了更加简单的.timeWindow和.countWindow方法,用于定义时间窗口和计数窗口。

10.6 窗口分配器(window assigner)

  • window() 方法接收的输入参数是一个 WindowAssigner
  • WindowAssigner负责将每条输入的数据分发到正确的window中
  • Flink 提供了通用的WindowAssigner
    滚动窗口(tumbling window)
    滑动窗口(sliding window)
    会话窗口(session window)
    全局窗口(global window)

10.7 创建不同类型的窗口

  • 滚动时间窗口(tumbling time window)
//方式一
.window(TumblingEventTimeWindows.of(Time.seconds(15)))
//方式二
.timeWindow(Time.seconds(15))
  • 滑动时间窗口(sliding time window)
//窗口长度15秒,滑动步长5秒

//方式一
.window( SlidingProcessingTimeWindows.of(Time.seconds(10),Time.seconds(3)))
//方式二
.timeWindow(Time.seconds(15),Time.seconds(5))
  • 会话窗口(session window)
//会话时长10分钟
.window(EventTimeSessionWindows.withGap(Time.minutes(10))
  • 滚动计数窗口
//计数10条
.countWindow(10)
  • 滑动计数窗口
//计数10条,滑动2条
.countWindow(102)

10.8 窗口函数

  • window function定义了要对窗口中收集的数据做的计算操作
  • 可以分为两类
  1. 增量聚合函数(incremental aggregation functions)
    每条数据到来就进行计算,保持-个简单的状态
    ReduceFunction, AggregateFunction
  2. 全窗口函数(full window functions)
    先把窗C ]所有数据收集起来,等到计算的时候会遍历所有数据
    ProcessWindowFunction

10.9 其他API

  • trigger()触发器:定义window什么时候关闭,触发计算并输出结果
  • evictor()移除器:定义移除某些数据的逻辑
  • allowedLateness()允许处理迟到的数据
  • sideOutputLateData()将迟到的数据放入侧输出流
  • getSideOutput()获取侧输出流

十一 时间语义与WaterMark

11.1 时间语义

  • 我们可以直接在代码中,对执行环境调用setStreamTimeCharacteristic方法,设置流的时间特性
	val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //EventTime 事件发生事件
    //IngestionTime 事件进入Flink事件
    //ProcessingTime 事件处理事件
    //给env创建的每一个Stream追加时间特性
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
  • 具体的时间,还需要从数据中提取时间戳(timestamp)

在 Flink的流式处理中, 绝大部分的业务都会使用 eventTime一般只在eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime

11.2 乱序数据的影响

在这里插入图片描述

  • 当Flink 以Event Time模式处理数据流时,它会根据数据里的时间戳来处理基于时间的算子
  • 由于网络、分布式等原因,会导致乱序数据的产生
  • 这就衍生出来一个问题,如上图所示,假设窗口为0-5,当接收到数据5时,就关闭的了窗口,那迟到的数据2、3,就会丢失,那怎么处理这种情况呢?所以就有了watermark机制

11.3 Watermark(水位线)

  • 怎样避免乱序数据带来计算不正确?
  • 遇到一个时间戳达到了窗口关闭时间,不应该立刻触发窗口计算,而是等待一段时间,等迟到的数据来了再关闭窗口
  • Watermark是一种衡量Event Time进展的机制,可以设定延迟触发
  • Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark机制结合window来实现;
  • 数据流中的Watermark用于表示timestamp小于Watermark的数据,都已经到达了,因此,window的执行也是由Watermark触发的。
  • watermark用来让程序自己平衡延迟和结果正确性

11.4 Watermark特点

在这里插入图片描述

  • watermark 是一条特殊的数据记录
  • watermark必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退.
  • watermark与数据的时间戳相关

11.5 watermark的传递

在这里插入图片描述

  • 上游通过广播的方式将watermark传递至下游,下游会有一个watermark的分区来接收多个并行子任务广播的watermark值

11.6 Watermark的使用

import Source.WaterSensor
import org.apache.flink.api.common.state._
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time

//定义带时间戳字段的样例类
case class WaterSensor(id:String,ts:Long,vc:Double)
object FlinkEventTime_wt {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //为env创建的每一个Stream追加时间特性
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

	//从端口读取数据
    val dataStream: DataStream[String] = env.socketTextStream("192.168.**.**",7777)
	
	//将读取的数据转换成WaterSensor类型
    dataStream.map(x=>{
    
    
      val strings: Array[String] = x.split(",")
      WaterSensor(strings(0),strings(1).toLong,strings(2).toDouble)
    })
      //方式一(推荐):数据密集时,可以使用周期性生成watermark
      .assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[WaterSensor](Time.seconds(3)) {
    
    //设置乱序延迟为3秒
      		//将处理过的数据提取出时间戳,乘以1000转成以毫秒的形式计数
      		override def extractTimestamp(t: WaterSensor): Long = t.ts*1000L
      })
      //方式二:分配升序时间戳,传入毫秒时间戳
      //.assignAscendingTimestamps(_.ts*1000L)
    
	//设置窗口为10秒并以及3秒的长度滑动
    val dataStream3: WindowedStream[(String, Double), String, TimeWindow] = dataStream2.map(x=>(x.id,x.vc)).keyBy(x=>x._1).timeWindow(Time.seconds(10),Time.seconds(3))

    //对window中的数据进行的处理
    val dataStream4: DataStream[(String, Double)] = dataStream3.reduce((x,y)=>(x._1,x._2+y._2))
    
    //输出window中的数据
    dataStream4.print("ev_wate_win")
    
    //启动
    env.execute("watermark")
  }
}

11.7 其他处理迟到数据得方式

  • 即使当窗口关闭了,但我们还可以设置,窗口销毁时间
//设置窗口最终销毁时间,延迟一分钟
.allowedLateness(Time.minutes(1))
  • 侧输出流
//设置侧输出流,处理窗口销毁后得迟到数据
.sideOutputLateData(new OutputTag[ApacheLogEvent]("late")) 

示例

	dataStream.filter(_.method=="GET").keyBy(_.url)
	  //设置窗口
      .timeWindow(Time.minutes(10),Time.seconds(5))
      //设置窗口最终销毁时间,延迟一分钟,处理迟到数据
      .allowedLateness(Time.minutes(1))
      //设置侧输出流,处理窗口销毁后得迟到数据
      .sideOutputLateData(new OutputTag[ApacheLogEvent]("late")) 
      .aggregate(new PageCountAgg(),new PageViewCountWindowResult())

十二 Flink状态

12.1 状态管理

12.1.1 状态概念

在这里插入图片描述

  • 由一个任务维护,并且用来计算某个结果的所有数据,都属于这个任务的状态
  • 可以认为状态就是一个本地变量,可以被任务的业务逻辑访问
  • Flink会进行状态管理,包括状态一致性、 故障处理以及高效存储和访问,以便开发人员可以专注于应用程序的逻辑

12.1.2 状态的分类

  • 在Flink中,状态始终与特定算子相关联
  • 为了使运行时的Flink了解算子的状态,算子需要预先注册其状态
  • 总的说来,有两种类型的状态:
    算子状态(Operator State):算子状态的作用范围限定为算子任务
    键控状态(Keyed State):根据输入数据流中定义的键(key) 来维护和访问

12.1.3 算子状态

在这里插入图片描述

  • 算子状态的作用范围限定为算子任务,由同一并行任务所处理的所有数据都可以访问到相同的状态
  • 状态对于同一子任务而言是共享的
  • 算子状态不能由相同或不同算子的另一个子任务访问

12.1.4 算子状态数据结构

列表状态(List state)

  • 将状态表示为一组数据的列表

联合列表状态(Union list state)

  • 也将状态表示为数据的列表。它与常规列表状态的区别在于,在发生故障时,或者从保存点(savepoint) 启动应用程序时如何恢复

广播状态(Broadcast state)

  • 如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应用广播状态。

12.1.5 键控状态

在这里插入图片描述

  • 键控状态是根据输入数据流中定义的键 (key) 来维护和访问的
  • Flink为每个key维护-个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个key对应的状态
  • 当任务处理一条数据时, 它会自动将状态的访问范围限定为当前数据的key

12.1.6 键控状态数据结构

值状态(Value state)

  • 将状态表示为单个的值

列表状态(Liststate)

  • 将状态表示为一-组数据的列表

映射状态(Map state)

  • 将状态表示为一-组 Key-Value对

聚合状态(Reducing state & Aggregating State)

  • 将状态表示为一-个用于聚合操作的列表

12.1.7 键控状态的使用

键控状态的类型有很多种例如ValueState、MapState、ListState等等

  • 声明一个键控状态
//几种不同类型的定义格式

//ValueState
var 键控名:ValueState[数据类型]=getRuntimeContext.getState(new ValueStateDescriptor[数据类型]("自定义名称",classOf[数据类型]))

//ListState
val 键控名: ListState[数据类型] = getRuntimeContext.getListState(new ListStateDescriptor[数据类型]("自定义名称",classOf[数据类型]))

//MapState(map与其他状态类型稍有不同,就是需要两个classOf)
val 键控名: MapState[key的数据类型, value的数据类型] = getRuntimeContext.getMapState(new MapStateDescriptor[key的数据类型,value的数据类型]("自定义名称",classOf[key的数据类型],classOf[value的数据类型]))

ValueStae类型示例

  • 声明
//定义键控状态
var test:ValueState[String]=getRuntimeContext.getState(new ValueStateDescriptor[String]("testabc",classOf[String]))
  • 读取当前状态
//读取状态值
val testabc:String=test.value()
  • 更新状态值
//更新状态值
test.update("zs")

12.2 状态后端

12.2.1 什么是状态后端?

  • 每传入一条数据,有状态的算子任务都会读取和更新状态
  • 由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问
  • 状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端(state backend)
  • 状态后端主要负责两件事:本地的状态管理,以及将检查点(checkpoint)状态写入远程存储

12.2.2 状态后端的存储模式

MemoryStateBackend

  • 内存级的状态后端,会将键控状态作为内存中的对象进行管理,将它们存储在TaskManager的JVM堆上,而将checkpoint存储在JobManager的内存中
  • 特点:快速、低延迟,但不稳定
  • 源码
public MemoryStateBackend() {
    
    this(null, null, DEFAULT_MAX_STATE_SIZE, TernaryBoolean.UNDEFINED);}
  • 示例
	val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
	//配置使用MemoryStateBackend
    env.setStateBackend(new MemoryStateBackend)

FsStateBackend

  • 将checkpoint 存到远程的持久化文件系统(FileSystem) 上,而对于本地状态,跟MemoryStateBackend - -样,也会存在TaskManager的JVM堆上
  • 同时拥有内存级的本地访问速度, 和更好的容错保证
  • 源码
	public FsStateBackend(String checkpointDataUri) {
    
    this(new Path(checkpointDataUri));}
  • 示例
	val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
	//配置使用FsStateBackend
    env.setStateBackend(new FsStateBackend("存储路径"))

RocksDBStateBackend

  • 将所有状态序列化后,存入本地的RocksDB中存储。

RocksDBStateBackend比较特殊,如果需要使用,需要添加依赖:
根据自己的使用的scala和flink版本进行修改
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-statebackend-rocksdb_2.11</artifactId>
<version>1.10.1</version>
</dependency>

  • 源码
public RocksDBStateBackend(String checkpointDataUri, boolean enableIncrementalCheckpointing) throws IOException {
    
    	
		this((new Path(checkpointDataUri)).toUri(), enableIncrementalCheckpointing);
    }
  • 示例
	val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
	//RocksDBStateBackend除了配置存储路径,还需要配置是否增量存储,否则就是全量存储
    env.setStateBackend(new RocksDBStateBackend("存储路径",true))

12.3 状态一致性

12.3.1 状态一致性级别

  • AT-MOST-ONCE (最多一次):当任务故障时,最简单的做法是什么都不干,既不恢复丢失的状态,也不重
    播丢失的数据。At-most-once 语义的含义是最多处理一次事件。

  • AT-LEAST-ONCE (至少一次):在大多数的真实应用场景,我们希望不丢失事件。这种类型的保障称为at-
    least-once,意思是所有的事件都得到了处理,而一些事件还可能被处理多次。

  • EXACTLY-ONCE (精确一次):恰好处理一次是最严格的保证,也是最难实现的。恰好处理一次语义不仅仅
    意味着没有事件丢失,还意味着针对每一个数据,内部状态仅仅更新一次。

12.3.2 端到端状态一致性

  • 目前我们看到的一致性保证都是由流处理器实现的,也就是说都是在Flink流处理器内部保证的;而在真实应用中,流处理应用除了流处理器以外还包含了数据源(例如Kafka)和输出到持久化系统
  • 端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终;每一个组件都保证了它自己的一致性
  • 整个端到端的一致性级别取决于所有组件中一致性最弱的组件

12.3.3 EXACTLY-ONCE (精确一次)

  • 内部保证:checkpoint
  • Source端:可重设数据的读取位置
  • Sink端:从故障恢复时,数据不会重复写入外部系统
    ➢幂等写入
    ➢事务写入

12.3.3.1 幂等写入(Idempotent Writes)

在这里插入图片描述

  • 所谓幂等操作,是说一个操作,可以重复执行很多次,但只导致一次结果更改,也就是说,后面再重复执行就不起作用了

12.3.3.2 事务写入(Transactional Writes)

  • 事务(Transaction)
    ➢应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中作的所有更改都会被撤消
    ➢具有原子性:一个事务中的一系列的操作要么全部成功,要么一个都不做
  • 实现思想:构建的事务对应着checkpoint,等到checkpoint真正完成的时候,才把所有对应的结果写入sink系统中
  • 实现方式
    ➢预写日志
    ➢两阶段提交
a. 预写日志(Write- Ahead-Log, WAL)
  • 把结果数据先当成状态保存,然后在收到checkpoint完成的通知时,一次性写入sink系统,相当于批处理了,增加延迟,降低了实时性
  • 简单易于实现,由于数据提前在状态后端中做了缓存,所以无论什么sink系统,都能用这种方式一批搞定
  • DataStream API提供了一个模板类: GenericWriteAheadSink, 来实现这种事务性sink
  • 但依然无法完美解决,如果sink在批量输出时挂掉,一样会又重新回滚,重新输出,依旧会又重复数据
b. 两阶段提交(Two-Phase-Commit, 2PC)
  • 对于每个checkpoint, sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里
  • 然后将这些数据写入外部sink系统,但不提交它们,这时只是"预提交”
  • 当它收到checkpoint完成的通知时,它才正式提交事务,实现结果的真正写入。这种方式真正实现了exactly-once,它需要一个 提供事务支持的外部sink系统。Flink 提供了TwoPhaseCommitSinkFunction接口

12.3.4 2PC对外部sink系统的要求

  • 外部sink系统必须提供事务支持,或者sink任务必须能够模拟外部系统上的事务
  • 在checkpoint的间隔期间里,必须能够开启一个事务并接受数据写入
  • 在收到checkpoint完成的通知之前,事务必须是"等待提交的状态。在故障恢复的情况下,这可能需要一些时间。如果这个时候sink系统关闭事务(例如超时了) ,那么未提交的数据就会丢失
  • sink任务必须能够在进程失败后恢复事务
  • 提交事务必须是幂等操作

12.3.5 不同Source和Sink的一致性保证

在这里插入图片描述

12.3.6 示例:Flink+ Kafka端到端状态一致性的保证

  • 内部:利用checkpoint机制,把状态存盘,发生故障的时候可以恢复,保证内部的状态一致性
  • source:kafka consumer作为source,可以将偏移量保存下来,如果后续任务出现了故障,恢复的时候可以由连接器重置偏移量,重新消费数据,保证一致性
  • sink:kafka producer作为sink,采用两阶段提交sink,需要实现一个TwoPhaseC ommitSinkFunction

12.3.6.1 原理详解:Flink+ Kafka端到端状态一致性的保证

原理图一
在这里插入图片描述

  • JobManager 协调各个TaskManager进行checkpoint存储
  • checkpoint保存在 StateBackend中,默认StateBackend是内存级的,也可以改为文件级的进行持久化保存

原理图二
在这里插入图片描述

  • 当checkpoint启动时,JobManager 会将检查点分界线(barrier) 注入数据流
  • barrier会在算子间传递下去

原理图三

在这里插入图片描述

  • 每个算子会对当前的状态做个快照,保存到状态后端
  • checkpoint机制可以保证内部的状态-致性

原理图四
在这里插入图片描述

  • 每个内部的 transform任务遇到barrier时,都会把状态存到checkpoint里
  • sink任务首先把数据写入外部kafka,这些数据都属于预提交的事务;遇到barrier时,把状态保存到状态后端,并开启新的预提交事务,但前面的事务还没有关闭,需要等到这次Checkpoint返回完成才会提交事务

原理图五
在这里插入图片描述

  • 当所有算子任务的快照完成,也就是这次的checkpoint完成时,JobManager 会向所有任务发通知,确认这次checkpoint完成
  • sink任务收到确认通知,正式提交之前的事务,kafka中未确认数据改为“已确认”

使用两阶段提交需要注意一个问题,就是如果kafka的时候等待超过15分钟没有被提交就会自动丢弃数据,如果Checkpoint操作超过15分钟就会发生这种情况,所有在做配置的时候Checkpoint超时时间尽量小一些

12.3.6.2 Exactly-once两阶段提交步骤详解

  • 第一条数据来了之后,开启一个kafka的事务(transaction) ,正常写入kafka分区日志但标记为未提交,这就是”预提交’
  • jobmanager 触发checkpoint操作,barrier 从source开始向下传递,遇到barrier的算子将状态存入状态后端,并通知jobmanager
  • sink 连接器收到barrier,保存当前状态,存入checkpoint,通知jobmanager, 并开启下一阶段的事务,用于提交下个检查点的数据
  • jobmanager 收到所有任务的通知,发出确认信息,表示checkpoint 完成
  • sink任务收到jobmanager的确认信息,正式提交这段时间的数据
  • 外部kafka关闭事务,提交的数据可以正常消费了。

十三 Flink Process Function

13.1 什么是ProcessFunction?

  • 我们之前学习的转换算子是无法访问事件的时间戳信息和水位线信息的。而这在一些应用场景下,极为重要。例如MapFunction这样的map转换算子就无法访问时间戳或者当前事件的事件时间。
  • 基于此,DataStreamAPI提供了一系列的Low-Level转换算子。可以访问时间戳、watermark以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。ProcessFunction用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的window函数和转换算子无法实现)。例如,Flink SQL就是使用Process Function实现的。

13.2 ProcessFunction种类

针对不同得流F1ink提供了8个Process Function

  • ProcessFunction :最原始,自定义程度高,什么都能做
  • KeyedProcessFunction:keyby后使用得process中传入得Process Function
  • CoProcessFunction:connect后使用得process中传入得Process Function
  • ProcessJoinFunction:两条流Join连接后使用得process中传入得Process Function
  • BroadcastProcessFunction:广播流使用得process中传入得Process Function
  • KeyedBroadcastProcessFunction:keyby广播流使用得process中传入得Process Function
  • ProcessWindowFunction:开窗后使用得process中传入得Process Function
  • ProcessAllWindowFunction:AllWindow后使用得process中传入得Process Function

13.2.1 ProcessFunction实现

  • processElement(v: IN, ctx: Context, out: Collector[OUT]),流中的每一个元素都会调用这个方法,调用结果将会放在Collector 数据类型中输出。Context可以访问元素的时间戳,元素的key,以及TimerService时间服务。Context还可以将结果输出到别的流(sideoutputs)。

  • onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])是一个回调函数。当之前注册的定时器触发时调用。参数timestamp为定时器所设定的触发的时间戳。Collector 为输出结果的集合。OnTimerContext和processElement的Context参数一样,提供了上下文的一些信息,例如定时器触发的时间信息(事件时间或者处理时间)。

使用示例

/**********************************简单使用proceess实现测输出流的功能*********************************************/
import Source.WaterSensor
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.ProcessFunction
import org.apache.flink.streaming.api.scala._ 
import org.apache.flink.util.Collector

object ProcessFunction {
    
    
  def main(args: Array[String]): Unit = {
    
    
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    env.setParallelism(1)
    //获取端口传入的数据
    val Stream: DataStream[String] = env.socketTextStream("192.168.**.**", 7777)
	//使用Process,创建SlipeProcessFunction类,并传入默认值
    val data: DataStream[Int] = Stream.process(new SlipeTmpProcess(30.0))
	//主流输出hige
    data.print("hige")
    //测输出流输出low
    data.getSideOutput(new OutputTag[String]("low")).print("low")

    env.execute()
  }
}

//实现SlipeTmpProcess 继承ProcessFunction,设置传入数据类型为Int,输出数据类型为String
class SlipeTmpProcess(d: Double) extends ProcessFunction[Int,String]{
    
    
  override def processElement(value: Int, ctx: ProcessFunction[Int, String]#Context, out: Collector[String]): Unit = {
    
    
    //当接收的数据大于默认值30时属于高的
    if (value.vc>d){
    
    
      out.collect("当前数值高于默认值")
    }else{
    
    
      //不满足条件的数据输入到测输出流
      //调用output方法
      ctx.output(new OutputTag[WaterSensor]("low"),"当前数值低于默认值")
    }
  }
}

13.2.2 KeyedProcessFunction实现

这里我们重点介绍KeyedProcessFunction.
KeyedProcessFunction用来操作KeyedStream。KeyedProcessFunction会处理流的每一个元素,输出为0个、1个或者多个元素。所有的Process Function都继承自RichFunction接口,所以都有open()、close()和getRuntimeContext0等方法。而KeyedProcessFunction[KEY, IN, OUT]还额外提供了两个方法:

代码示例

//**************************这个示例模拟接收一个温度传感器得数据进行监控温度数据并作出反应*****************************
import Source.WaterSensor
import org.apache.flink.api.common.state.{
    
    ValueState, ValueStateDescriptor}
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector

//定义样例类
case class WaterSensor(id:String,ts:Long,vc:Double)
object ProcessTest {
    
    
  def main(args: Array[String]): Unit = {
    
    
  //创建流处理执行环境
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //使用事件发生时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    //设置并行度为1
    env.setParallelism(1)
    //设置一个端口作为数据源
    val Stream: DataStream[String] = env.socketTextStream("192.168.**.**",7777)
	//将获取得数据转换成watersensor类型
    val dataStream: DataStream[WaterSensor] = Stream.map(data => {
    
    
      val strings: Array[String] = data.split(",")
      WaterSensor(strings(0), strings(1).toLong, strings(2).toDouble)
    })
	//先分区在process,自定义实现KeyedProcessFunction
    val value: DataStream[String] = dataStream.keyBy(_.id).process(new KPF)
	//输出数据
    value.print("data")

    env.execute()
  }
}
//实现自定义KeyeedProcessFunction
class KPF extends KeyedProcessFunction[String,WaterSensor,String]{
    
    
  //定义waterState状态,来保存当前接收到的WayerSensor
  var waterState:ValueState[WaterSensor]=_
  //定义currentState保存创建的定时器的时间值
  var currentState:ValueState[Long]=_
  //设置生命周期
  override def open(parameters: Configuration): Unit = {
    
    
    //初始化
    waterState=getRuntimeContext.getState(new ValueStateDescriptor[WaterSensor]("watersensor",classOf[WaterSensor]))
    currentState=getRuntimeContext.getState(new ValueStateDescriptor[Long]("current",classOf[Long]))
  }
	
  override def processElement(value: WaterSensor, ctx: KeyedProcessFunction[String, WaterSensor, String]#Context, out: Collector[String]): Unit = {
    
    
    //如果当前是第一次接收到数据,就直接传入watersensor状态值
    if(waterState.value()==null){
    
    
      println("初始化watersoner")
      waterState.update(value)
    }else{
    
    //否则判度当前获取得数据是否比上一次得温度值高
      //当传感器温度上升时,创建一个10秒后执行的定时器
      if(value.vc>waterSensor.value().vc && currentState.value()==0){
    
    
        //获取当前数据处理时间并加上10000毫秒作为定时器执行时间
        val timeTS:Long=ctx.timerService().currentProcessingTime()+10000L
        //创建定时器
        ctx.timerService().registerProcessingTimeTimer(timeTS)
        println("已创建定时器")
        //保存定时器执行时间
        currentState.update(timeTS)
        //当在10秒内读取到温度下降的值,解除定时器
      }else if(value.vc<waterState.value().vc){
    
    
        //根据定时器的时间,删除执行对应定时器
        ctx.timerService().deleteProcessingTimeTimer(currentState.value())
        //清空初始化
        currentState.clear()
      }

      //将每次传入的数据保存下来
      println("正在更新waterSensor")
      waterState.update(value)
    }
  }
  //实现opTime
  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, WaterSensor, String]#OnTimerContext, out: Collector[String]): Unit = {
    
    
    //当定时器发生时返回输出内容
    out.collect("警告!"+"传感器ID:"+waterState.value().id+"\t检测到温度持续上升!当前温度"+waterState.value().vc)
    currentState.clear()
  }
}

十三 Flink SQL/Table API

  • 万字长文!掌握Flink Table一篇就够了:点击这里

猜你喜欢

转载自blog.csdn.net/weixin_38468167/article/details/112548797