Flink--框架和分布式处理引擎----flink大全

Flink–框架和分布式处理引擎

目 录

  • Flink--框架和分布式处理引擎
    • 一、Flink概述
      • (一)理念/什么是flink:
      • (二)Flink特点
        • 1、事件驱动型(Event-driven)
        • 2、流与批的世界观
        • 3、分层api
      • (三)flink 和 sparkStreaming的区别
      • (四)flink 配置优先级 代码 脚本 配置文件
    • 二、Flink运行架构
      • (一)Flink 运行时的组件
        • 每个组件的职责如下
        • (1)作业管理器(JobManager)
        • (2)资源管理器(ResourceManager )
        • (3)任务管理器(TaskManager )
        • (4)分发器(Dispatcher)
      • (二)任务提交流程
      • (三)Flink 的并发执行能力由什么决定--------TM数量、slots的数量
        • (1)什么是并行度?
        • (2)什么是stream最大并行度?
      • (四)Flink 中的执行图--四层
        • (1)StreamGraph:
        • (2)JobGraph:
        • (3)ExecutionGraph :
        • (4)物理执行图:
      • (五)flink 算子之间传输数据的形式⭐
      • (六)满足 任务链(Operator Chains)的要求?
      • (七)任务链的好处?
    • 三、Flink 流处理API
      • (一)执行环境
        • (1)最常用的创建方式 -- getExecutionEnvironment
          • (1.1)getExecutionEnvironment
          • (1.2)代码
          • (1.3)并行度
        • (2)其他创建方式
          • (2.1)createLocalEnvironment --- 本地执行环境
          • (2.2) createRemoteEnvironment --- 集群执行环境
      • (二)flink 数据源source
        • (1)如何获取source
          • 从集合中读取数据-- env.fromCollection(List())
          • 从文件中读取数据
          • 以Kafka消息队列的数据作为来源
          • 自定义source
      • (三.1)transformation 转换算子
        • (1)keyBy
        • (2)滚动聚合算子(Rolling Aggregation)
        • (3)Reduce
        • (4)split
        • (5)select
        • (6)connect
        • **(6-补充)CoMap**/coFlapMap
        • (7)union
        • (8)切分流? 选择流? 链接流? 合并流?
          • (1)split 和 select
          • (2)Connect 与 Union 区别?
      • (三.2)flink 支持的数据类型?
        • (1)基础数据类型
        • (2)java和scala元组(Tuples)
        • (3)Scala的样例类(case class)
        • (4)Java简单对象(POJOs)
        • (5)其他(Arrays,Lists,Maps,Enums……)
      • (三.3)UDF函数--函数类、匿名函数、富函数
        • (1)函数类,匿名函数,匿名类
        • (2)什么是富函数?
      • (四)Flink对外输出-Sink
        • (1)Sink----Kafka
        • (2)Sink---JDBC
      • (五)flink 序列化?
    • 四、Flink中的window
      • (一)window是什么?
      • (二)window的类型
      • (三)window API
        • (1)窗口分配器(window assigner):确定数据属于哪个窗口
        • (2)Window
          • TimeWindow:
        • (2.1)Window------- TimeWindow
        • (2.2)Window------- CountWindow
        • (3)窗口函数 window function
        • (4)其他可选API
    • 五、时间语义和Wartermark
      • (一)时间语义
      • (二)引入事件时间 EventTime
      • (三)waterMark
        • (1)Watermark 产生背景:
        • (2)Watermark概述
        • (3)waterMark特点
        • (4)引入WaterMark
        • (5)自定义WaterMark
      • (四)flink如何处理迟到的数据?
      • (五)侧输出流是什么?
    • 六、ProcessFunction API(底层 API)
      • (一)keyedProcessFunction ⭐
      • (二)TimerService 和 定时器(Timers)
    • 七、状态编程 + 容错机制
      • (一)Flink状态
      • (二)状态一致性 / 正确性级别
        • (1)什么是状态一致性?
        • (2)一致性级别 [3]
        • (3)flink 状态管理包含哪些?
        • (4)端到端(end-to-end)状态一致性
        • (5)Flink ==具体==如何保证 exactly-once 呢?
      • (三)检查点checkpoint
        • (1)Flink 检查点算法的正式名称是**异步分界线快照**。
        • (2)检查点checkpoint和保存点savepoint的区别?
        • (3)Flink 的 checkpoint 机制对比 spark 有什么不同和优势?
      • (四)Flink+Kafka 如何实现端到端的 exactly-once
      • (五)状态后端 / Flink的状态存储方式
      • (六)Flink的重启策略
    • 八、CEP
      • (1)什么是CEP?
      • (2)cep的模式 ----- 处理事件的规则。
      • (3)近邻模式分为?

一、Flink概述

Flink的四大基石:checkpoint state time window

(一)理念/什么是flink:

分布式、高性能、随时可用以及准确的流处理应用程序打造的开源流处理框架

Flink是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态的计算。Flink被设计在所有常见的集群环境中运行,以内存执行速度和任意规模来执行计算。

  • 有状态的计算:会基于多个事件输出结果。

(二)Flink特点

1、事件驱动型(Event-driven)

事件驱动型应用是一类具有状态的应用。它从一个或多个事件流中提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。

【典型的是以Kafka为代表的消息队列,几乎都是事件驱动型】

PS:与之不同的是 sparkstreaming的微批次

2、流与批的世界观

批处理的特点是有界、持久、大量,非常适合需要访问全套数据才能完成的计算工作,一般用于离线统计

流处理的特点是无界、实时、无需针对整个数据集执行操作,而是通过系统传输的每个数据项进行操作一般用于实时统计

……的世界观 离线数据 实时数据
spark 一切都是由批次组成的 离线数据一个大批次 实时数据由一个一个无限的小批次组成的
flink 一切都是由流组成的 离线数据是有界限的流 【有界流】 实时数据是一个没有界限的流 【无界流】

什么是有界流 无界流?⭐

  • 有界流

    • 有界数据流有明确定义的开始和结束,可以在执行任何计算之前通过获取所有数据来处理有界流,处理有界流不需要有序获取,因为可以始终对有界数据集进行排序,有界流的处理也称为批处理
  • 无界流

    • 无界数据流有一个开始但是没有结束,它们不会在生成时终止并提供数据,必须连续处理无界流,也就是说必须在获取后立即处理 event。对于无界数据流我们无法等待所有数据都到达,因为输入是无界的,并且在任何时间点都不会完成。处理无界数据通常要求以特定顺序(例如事件发生的顺序)获取 event,以 便能够推断结果完整性
3、分层api

Flink--框架和分布式处理引擎----flink大全_第1张图片

Flink中提供了4种不同层次的API:

  • 低级API(stateful stream processing):提供了对时间和状态的细粒度控制,简洁性和易用性较差,主要应用在对一些复杂事件的处理逻辑上。
  • 核心API(datastream && dataset api):主要提供了针对流数据和离线数据的处理,对低级API进行了一些封装,提供了filter、sum、max、min等高级函数,简单且易用,所以在工作中应用比较广泛。
  • Table API:一般与DataSet或者DataStream紧密关联,
    • 首先通过一个DataSet或DataStream创建出一个Table;
    • 然后用类似于filter、join或者select关系型转化操作来转化为一个新的Table对象;
    • 最后将一个Table对象转回一个DataSet或DataStream。
    • 与SQL不同的是,Table API的查询不是一个指定的SQL字符串,而是调用指定的API方法。
  • SQL:Flink的SQL集成是基于Apache Calcite的,Apache Calcite实现了标准的SQL,使用起来比其他API更加灵活,因为可以直接使用SQL语句。Table API和SQL可以很容易地结合在一块使用,它们都返回Table对象。

大多数应用并不需要上述的底层抽象,而是针对核心 API(Core APIs) 进行编程,比如 DataStream API(有界或无界流数据)以及 DataSet API(有界数据集)

目前 Flink 作为批处理还不是主流,不如 Spark 成熟,所以 DataSet 使用的不是很多。

Flink Table API 和 Flink SQL 也并不完善

(三)flink 和 sparkStreaming的区别

(1)数据模型:
Spark采用RDD模型,spark Streaming的DStream是一组组小批数据RDD的集合
flink 基本数据模型是数据流,以及事件序列
(2)运行时架构
Spark是计算,将DAG划分为不同的stage,一个完成后才能计算下一个
flink是标准的执行模式,一个事件在一个节点处理完成后可以直接发给下一个节点处理

为什么使用 Flink 替代 Spark?

flink 的低延迟、高吞吐量和对流式数据应用场景更好的支持;另外,flink 可以很好地处理乱序数据,而且可以保证 exactly-once 的状态一致性。

(四)flink 配置优先级 代码 脚本 配置文件

二、Flink运行架构

(一)Flink 运行时的组件

Flink 运行时架构主要包括四个不同的组件,它们会在运行流处理应用程序时协同工作:

作业管理器(JobManager)

资源管理器(ResourceManager)

任务管理器(TaskManager)

分发器(Dispatcher)

因为 Flink 是用 Java 和 Scala 实现的,所以所有组件都会运行在 Java 虚拟机上。

每个组件的职责如下
(1)作业管理器(JobManager)

作业管理器作用控制一个应用程序执行的主进程,即–每个应用程序都会被一个不同的 JobManager 所控制执行

1、JobManager 会先接收到要执行的应用程序

JobManager 接收到的执行的应用程序包含:⭐⭐⭐

  • 作业图(JobGraph)
  • 逻辑数据流图(logical dataflow graph)
  • 打包了所有的类、库和其他资源的jar包

2、JobManager 会将作业图(JobGraph)转换为一个物理层面的数据流图,即执行图(ExcutionGraph)< 执行图包含了所有可以并发执行的任务 >

3、JobManager 会向资源管理器(ResourceManager)请求执行任务必要的资源,也就是任务管理器(TaskManager)上的插槽(slot)。

4、一旦它获取到了足够的资源,就会将执行图分发到真正运行它们的 TaskManager 上。

5、在运行过程中,JobManager 会负责所有需要中央协调的操作,比如说检查点(checkpoints)的协调。

(2)资源管理器(ResourceManager )

资源管理作用主要负责管理任务管理器(TaskManager)的插槽(slot),slot是 Flink 中 定义的处理资源单元。

Flink 为不同的环境和资源管理工具提供了不同资源管理器,比如 YARN、Mesos、K8s,以及 standalone 部署。

当 JobManager 申请插槽资源时,ResourceManager 会将有空闲插槽的 TaskManager 分配给 JobManager。

如果 ResourceManager 没有足够的插槽 来满足 JobManager 的请求,它还可以向资源提供平台发起会话,以提供启动 TaskManager 进程的容器。

另外,ResourceManager 还负责终止空闲的 TaskManager,释放计算资源。

(3)任务管理器(TaskManager )

Flink 中的工作进程。通常在 Flink 中会有多个 TaskManager 运行,每一个 TaskManager都包含了一定数量的插槽(slots)。插槽的数量限制了 TaskManager 能够执行的任务数量

启动之后,TaskManager 会向资源管理器注册它的插槽;

收到资源管理器的指令后, TaskManager 就会将一个或者多个插槽提供给 JobManager 调用。

JobManager 就可以向插槽(slot)分配任务(tasks)来执行了。

默认情况下,flink允许子任务共享slot,即使它们是不同的子任务。这样的结果是,一个slot可以保存作业的整个管道。提高资源的利用率

在执行过程中,一个 TaskManager 可以跟其它==运行同一应用程序==的 TaskManager 交换数据。

(4)分发器(Dispatcher)

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

(二)任务提交流程

Flink--框架和分布式处理引擎----flink大全_第2张图片

这是一个整体图。如果部署的集群环境不同(例如 YARN,Mesos,Kubernetes,standalone 等),其中一些步骤可以被省略,或是有些组件会运行在同一个 JVM 进程中。

例如:Flink 集群部署在yarn上

Flink--框架和分布式处理引擎----flink大全_第3张图片

Flink 任务提交后

1、Client 向 HDFS 上传 Flink 的 Jar 包和配置

2、向 Yarn ResourceManager 提交任务

3、ResourceManager 分配 Container 资源并通知对应的 NodeManager 启动 ApplicationMaster,ApplicationMaster 启动后加载 Flink 的 Jar 包 和配置构建环境,然后启动 JobManager

4、ApplicationMaster 向 ResourceManager 申 请 资 源 启 动 TaskManager

5、ResourceManager 分配 Container 资 源 后 , 由 ApplicationMaster 通 知 资 源 所 在 节 点 的 NodeManager 启 动 TaskManager , NodeManager 加载 Flink 的 Jar 包和配置构建环境并启动 TaskManager,TaskManager 启动后向 JobManager 发送心跳包,并等待 JobManager 向其分配任务。

(三)Flink 的并发执行能力由什么决定--------TM数量、slots的数量

(1)什么是并行度?

一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism)。一个程序中,不同的算子可能具有不同的并行度。

TaskManager中的slot的数量决定了flink的最大并发能力,但执行任务不一定都用到。

任务的最大并行度代表一个流程序需要的slot数量

(2)什么是stream最大并行度?

​ 一般情况下,一个stream的并行度,可以认为就是其所有算子中最大的并行度。

(四)Flink 中的执行图–四层

Flink 中的执行图可以分成四层

StreamGraph -> JobGraph -> ExecutionGraph -> 物理执行图

(1)StreamGraph:

是根据用户通过 Stream API 编写的代码生成的最初的图。用来表示程序的拓扑结构。

(2)JobGraph:

StreamGraph 经过优化后生成了 JobGraph,提交给 JobManager 的数据结构。主要的优化为,将多个符合条件的节点 chain 在一起作为一个节点,这样可以减少数据在节点之间流动所需要的序列化/反序列化/传输消耗。

(3)ExecutionGraph :

JobManager 根 据 JobGraph 生 成 ExecutionGraph 。 ExecutionGraph 是 JobGraph 的并行化版本,是调度层最核心的数据结构。

(4)物理执行图:

JobManager 根据 ExecutionGraph 对 Job 进行调度后,在各个 TaskManager 上部署 Task 后形成的“图”,并不是一个具体的数据结构。

(五)flink 算子之间传输数据的形式⭐

  1. one-to-one [forwording]
  2. Redistributing

stream在算子之间传输数据的形式有以上两种,但具体是哪一种还取决于 ------- 算子的种类

  • one-to-one [forwording]:stream维护着分区及元素的顺序

    • 下一个算子和上一个算子的子任务生产的元素的个数、顺序相同

    • map、filter、flatMap……

    • 类似spark的 窄依赖

    • 例如:source 和 map operator 之间

      • map 算子的子任务看到的元素的个数以及顺序跟 source 算子的子任务生产的元素的个数、顺序相同
  • redistributing:stream的分区会发生改变

    • 每一个算子的子任务依据所选择的transformation发送数据到不同的目标任务
    • 类似于spark中的 宽依赖/shuffle
    • 例如:map()跟 keyBy/window 之间 或者 keyBy/window 跟 sink之间
      • keyBy() 基于 hashCode 重分区、broadcast 和 rebalance 会随机重新分区,这些算子都会引起 redistribute 过程

补充:如果并行度相同且one-to-one数据传输,那么可以把多个算子合并成一个任务

(六)满足 任务链(Operator Chains)的要求?

相同并行度的 one-to-one 操作,Flink 这样相连的算子链接在一起形成一个 task, 原来的算子成为里面的一部分。

简而言之:如果并行度相同且是one-to-one数据传输,那么可以把多个算子合并成一个任务

程序会默认将符合条件的算子进行chain。

但会出现这样一种情况:任务链组合之后,数据传输简单,但任务耗费时间过长

如果有某个不想链接的算子,可以在代码里面进行设置,不允许链接。在算子后面.disableChaining() ,表示当前算子拒绝链接(前后都是)

例如:

val resDS = ds.flatMap(_.split(" "))
	  .filter(_.nonEmpty)
      .map((_,1)).disableChaining()
      .keyBy(0)
      .sum(1)

(七)任务链的好处?

将算子链接成 task 是非常有效的优化

它能减少线程之间的切换和基于缓存区的数据交换,在减少延时的同时提升吞吐量。链接的行为可以在编程 API 中指定。

三、Flink 流处理API

Flink--框架和分布式处理引擎----flink大全_第4张图片

(一)执行环境

创建数据源要基于执行环境,所以要先创建执行环境

(1)最常用的创建方式 – getExecutionEnvironment
(1.1)getExecutionEnvironment
  • 创建一个执行环境,表示当前执行程序的上下文。

  • getExecutionEnvironment 会根据查询运行的方式决定返回什么样的运行环境,是最常用的一种创建执行环境的方式。

  • 如果程序是独立调用的,则 此方法返回本地执行环境;

  • 如果从命令行客户端调用程序以提交到集群,则 此方法返回此集群的执行环境

(1.2)代码

批处理

val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment

流处理

val env = StreamExecutionEnvironment.getExecutionEnvironment
(1.3)并行度

如果没有设置并行度,会以 flink-conf.yaml 中的配置为准,默认是 1。

设置并行度

env.setParallelism(1)
(2)其他创建方式

下面两种方式均不如 getExecutionEnvironment 来的简便

(2.1)createLocalEnvironment — 本地执行环境

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

val env = StreamExecutionEnvironment.createLocalEnvironment(1) 
(2.2) createRemoteEnvironment — 集群执行环境

返回集群执行环境,将 Jar 提交到远程服务器。

需要在调用时指定 JobManager 的 IP 和端口号,并指定要在集群中运行的 Jar 包

 val env = ExecutionEnvironment.createRemoteEnvironment("jobmanage-hostname", 6123,"YYYY//wordcount.jar")

(二)flink 数据源source

(1)如何获取source
  1. 从集合中读取数据-- env.fromCollection(List())
    • val stream01 = env.fromCollection(List(数据1,数据2,数据3,数据4))
      
    • val stream1 = env.fromCollection(
          List(
      SensorReading("sensor_1", 1547718199, 35.80018327300259),
      SensorReading("sensor_6", 1547718201, 15.402984393403084),
      SensorReading("sensor_7", 1547718202, 6.720945201171228),
      SensorReading("sensor_10", 1547718205, 38.101067604893444)
      ))
      
  2. 从文件中读取数据
    • val stream02 = env.readTextFile("文件路径")
      
  3. 以Kafka消息队列的数据作为来源
    • 先导入Kafka连接器的依赖

    • <dependency>
      	<groupId>org.apache.flinkgroupId>
      	<artifactId>flink-connector-kafka-0.11_2.11artifactId>
      	<version>1.11.0version>
      dependency>
      
    • 代码使用,要使用 env.addSource(new FlinkKafkaConsumer[String] (topics , new SimpleStringSchema(), propertoes))

      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")
      
    • // new FlinkKafkaConsumer011是版本1.12以前的,1.12不能用了
      val stream03 = env.addSource(new FlinkKafkaConsumer011[String]("sensor", new
      SimpleStringSchema(), properties))
      
    • //1.12的版本是 new FlinkKafkaConsumer
      val ds:DataStream[String] = env.addSource(function = new FlinkKafkaConsumer[String](
                topics,
                new SimpleStringSchema(),
                properties
              ))
      
  4. 自定义source

传入 一个 SourceFunction

val stream4 = env.addSource( new MySensorSource())
class MySensorSource extends SourceFunction[SensorReading]{
    //重写方法
}

(三.1)transformation 转换算子

map | flatMap | KeyBy | 滚动聚合算子(Rolling Aggregation) | Reduce | Split | Select |

Connect | CoMap | Union |

(1)keyBy

DataStream → KeyedStream

逻辑地将一个流拆分成不相交的分区每个分区包含具有相同 key 的元素,在内部以 hash 的形式实现【基于key 的 hash code进行重分区 】

注意:

  • 同一个key只能在一个分区内处理一个分区内可以有不同key的数据
  • keyBy之后的keyedStream上的所有操作,针对的作用域都只是当前的key
(2)滚动聚合算子(Rolling Aggregation)

滚动聚合算子可以针对 KeyedStream 的每一个支流做聚合。DataStream没有聚合操作

  • sum()
  • min()
  • max()
  • minBy()
  • maxBy()
(3)Reduce

KeyedStream → DataStream

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

(4)split

DataStream → SplitStream:根据某些特征把一个 DataStream 拆分成 两个 或者 多个 DataStream

例如:高于40度视为高温,根据这个条件进行切分

val splitStream = dataStream.split( data =>{
    if(data.temp > 40)
    	seq("high_temperature")  //标签,选择流通过它选择相应的流
    else
    	seq("low_temperature")
})
(5)select

SplitStream→DataStream:从一个 SplitStream 中获取 一个或者多个 DataStream

例如:从splitSteam中选择流出来

val hignTemp = splitStream.select("high_temperature")
val lowTemp = splitStream.select("low_temperature")

//除了可以在切分流里选择单个的流,还可以一次选择多个流
val allTemp = splitStream.select("high_temperature","low_temperature")

//-----------------输出结果看看
highTemp.print("high:")
lowTemp.print("low:")
allTemp.print("all:")
(6)connect

DataStream,DataStream → ConnectedStreams

连接两个保持他们类型的数据流,两个数据流被 Connect 之后,只是被放在了一个同一个流中,内部依然保持各自的数据和形式不发生任何变化,两个流相互独立

    val connectedStreams: ConnectedStreams[SensorReading, SensorReading] = hignTemp.connect(lowTemp)

PS:connect 两个不同类型的流可以通过CoMap来变成一样类型的流。

(6-补充)CoMap/coFlapMap

ConnectedStreams → DataStream

作用于 ConnectedStreams 上,功能与 map 和 flatMap 一样,对 ConnectedStreams 中的每一个 Stream 分别进行 map 和 flatMap 处理。

真正写代码的时候,调用的是map() / flatMap() 方法,但是传参不同,传入coMapFunction。传入两个函数函数传入处理的数据与之前connect的流是一一对应的。

例如:

hignTemp.connect(lowTemp)

那么,第一函数对应hignTemp,第二个函数对应lowTemp

    val resultStream: DataStream[Product] = connectedStreams.map(
      highStream => (highStream.sensorId, highStream.timestamp,"high temp warning"),
      lowStream => (lowStream.sensorId, "normal")
    )

    resultStream.print("result")
/*
result> (sensor_6,1547718204,high temp warning)
result> (sensor_1,normal)
result> (sensor_3,1547718210,high temp warning)
result> (sensor_2,normal)
result> (sensor_3,1547718211,high temp warning)
result> (sensor_3,normal)
result> (sensor_3,1547718258,high temp warning)
result> (sensor_4,normal)
result> (sensor_5,normal)
result> (sensor_7,normal)
result> (sensor_3,normal)
result> (sensor_3,normal)
result> (sensor_3,normal)
……………………
*/

(7)union

DataStream → DataStream

两个或者两个以上的 DataStream 进行 union 操 作,产生一个包含所有 DataStream 元素的新 DataStream。

    val value: DataStream[SensorReading] = highStream.union(lowStream)
(8)切分流? 选择流? 链接流? 合并流?

split – select ,Connect – Union 成对出现

(1)split 和 select

split可以将一个DataStream 切分为两个/多个DataStream作为SplitStream

select可以从SplitStream里选择DataStream

(2)Connect 与 Union 区别?
  1. connect只能操作两个流,Union可以操作多个流
  2. connect可以连接类型不同的两个流,两个流之间相互独立,之后再通过 coMap/coFlatMap调整成一样的。Union要求两个流的类型必须一致
Connect Union
操作的流的数量 2 多个
操作的流的类型 不要求一致 必须一致

(三.2)flink 支持的数据类型?

Flink 支持 Java 和 Scala 中所有常见数据类型。

(1)基础数据类型

Flink 支持所有的 Java 和 Scala 基础数据类型,Int, Double, Long,

(2)java和scala元组(Tuples)
(3)Scala的样例类(case class)
(4)Java简单对象(POJOs)
(5)其他(Arrays,Lists,Maps,Enums……)

Flink 对 Java 和 Scala 中的一些特殊目的的类型也都是支持的,比如 Java 的 ArrayList,HashMap,Enum 等等

(三.3)UDF函数–函数类、匿名函数、富函数

(1)函数类,匿名函数,匿名类

Flink 暴露了所有 udf 函数的接口(实现方式为接口或者抽象类)。

例如 MapFunction, FilterFunction, ProcessFunction 等等。

----------------------------------------- 以FilterFunction为例 -------------------------------------------------------------------

要求:把 sensorId = “sensor_3” 的数据筛选出来

-------- 函数类 + 匿名函数

//可以自定义函数类,也可以自己直接在()写匿名函数
val value1: DataStream[SensorReading] = sensorStream.filter(new UDFFilter)
val value2: DataStream[SensorReading] = sensorStream.filter(_.sensorId == "sensor_3")

value1.print("new UDF")
value2.print("直接方法")
//自定义一个函数类 ,继承 FilterFunction
class UDFFilter extends FilterFunction[SensorReading] {
  override def filter(value: SensorReading): Boolean = {
    value.sensorId == "sensor_3"
  }

-------- 匿名类

val flinkTweets = sensorStream.filter(
	new RichFilterFunction[String] {
		override def filter(value: String): Boolean = {
			value.sensorId == "sensor_3"
		}
	}
)
(2)什么是富函数?

“富函数“是 DataStream API 提供的一个函数类的接口,所有 Flink 函数类都有其 Rich 版本
它与常规函数的不同在于,可以获取运行环境的上下文拥有一些生命周期方法,所以可以实现更复杂的功能。

典型的生命周期方法有:

  1. **open()**方法是 rich function 的初始化方法,当一个算子例如 map 或者 filter 被调用之前 open()会被调用。
  2. **close()**方法是生命周期中的最后一个调用的方法,做一些清理工作。
  3. **getRuntimeContext()**方法提供了函数的 RuntimeContext 的一些信息,例如函数执行的并行度,任务的名字,以及 state 状态
//自定义一个类 ,继承富函数RichFilterFunction
class UDFFilter1 extends RichFilterFunction[SensorReading]{

  //初始化  仅调用一次
  override def open(parameters: Configuration): Unit = super.open(parameters)

  //清理操作  整个函数关闭时调用
  override def close(): Unit = super.close()

  override def filter(value: SensorReading): Boolean = {
    value.sensorId == "sensor_3"
  }

(四)Flink对外输出-Sink

Flink 没有类似于 spark 中 foreach方法,可以迭代输出。

Flink的对外输出操作要利用Sink完成。

stream.addSink(new mySink(……))

官方提供了一部分的框架的 sink。除此以外,需要用户自定义实现 sink

(1)Sink----Kafka

pom.xml文件中要 插入依赖


        <dependency>
            <groupId>org.apache.flinkgroupId>
            <artifactId>flink-connector-kafka-0.11_2.11artifactId>
            <version>1.7.2version>
        dependency>

写完source和transformation。通过addSink()方法设置输出

这里输出到kafka,主函数中的代码如下

flink1.11及以下版本

dataStream.addSink(new FlinkKafkaProducer011[String]("c701:9092",
"test01", new SimpleStringSchema()))

或者:

    val properties = new Properties()
    properties.setProperty("bootstrap.servers", "c701:9092")
    properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")

    dataStream.addSink(new FlinkKafkaProducer011[String](
      "test01",
      new SimpleStringSchema(),
      properties
    ))

    dataStream.print()

    env.execute("kafka_sink_job")

flink1.12版本

    val properties = new Properties()
    properties.setProperty("bootstrap.servers", "c701:9092")
    properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    properties.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")

    dataStream.addSink(new FlinkKafkaProducer[String](
      "test01",
      new SimpleStringSchema(),
      properties
    ))

    dataStream.print()

    env.execute("kafka_sink_job")
(2)Sink—JDBC

pom.xml文件中要 插入依赖

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.20version>
        dependency>

写完source和transformation。通过addSink()方法设置输出

JDBC连接MySQL进行输出,

创建类继承RichSinkFunction方法,自定义外部sink

flink1.11及以下版本

class JDBCSink extends RichSinkFunction[SensorReading]{

  var connect:Connection = _
  var insertStmt:PreparedStatement = _
  var updateStmt:PreparedStatement = _

  //初始化,创建连接,和预编译语句
  override def open(parameters: Configuration): Unit = {
    super.open(parameters)
    connect = DriverManager.getConnection("jdbc:mysql://hadoop102:3306/flink","root","000000")
    insertStmt = connect.prepareStatement("INSERT INTO temperatures (sensorId,temp) VALUES(?,?)")
    updateStmt = connect.prepareStatement("UPDATE temperatures SET temp = ? WHERE sensorId = ?")
  }


  //执行sql
  override def invoke(value: SensorReading, context: SinkFunction.Context[_]): Unit = {
    updateStmt.setDouble(1,value.temp)
    updateStmt.setString(2,value.sensorId)
    updateStmt.execute()    //!!!!!!!!!!!!!!!!!如果没有写这句代码,不会执行update 的 sql语句

    // 如果update没有查到数据,那么执行插入语句
    if (updateStmt.getUpdateCount == 0){
      insertStmt.setString(1,value.sensorId)
      insertStmt.setDouble(2,value.temp)
      insertStmt.execute()   //!!!!!!!!!!!!!!!!!如果没有写这句代码,不会执行insert的sql语句
    }

  }

  //close
  override def close(): Unit = {
    insertStmt.close()
    updateStmt.close()
    connect.close()
  }
}

flink1.12版本:只有 invoke 方法上稍有不同,差别不大

class JDBCSink extends RichSinkFunction[SensorReading] {

  var connect: Connection = _
  var insertStmt: PreparedStatement = _
  var updateStmt: PreparedStatement = _

  //初始化,创建连接,和预编译语句
  override def open(parameters: Configuration): Unit = {
    super.open(parameters)
    connect = DriverManager.getConnection("jdbc:mysql://hadoop102:3306/flink", "root", "000000")
    insertStmt = connect.prepareStatement("INSERT INTO temperatures (sensorId,temp) VALUES(?,?)")
    updateStmt = connect.prepareStatement("UPDATE temperatures SET temp = ? WHERE sensorId = ?")
  }

  override def invoke(value: SensorReading, context: SinkFunction.Context): Unit = {
    updateStmt.setDouble(1, value.temp)
    updateStmt.setString(2, value.sensorId)
    updateStmt.execute() //!!!!!!!!!!!!!!!!!如果没有写这句代码,不会执行update 的 sql语句

    // 如果update没有查到数据,那么执行插入语句
    if (updateStmt.getUpdateCount == 0) {
      insertStmt.setString(1, value.sensorId)
      insertStmt.setDouble(2, value.temp)
      insertStmt.execute() //!!!!!!!!!!!!!!!!!如果没有写这句代码,不会执行insert的sql语句
    }
  }

  //close
  override def close(): Unit = {
    insertStmt.close()
    updateStmt.close()
    connect.close()
  }
}

主函数中的代码如下

import java.sql.{Connection, DriverManager, PreparedStatement}

import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.functions.sink.{RichSinkFunction, SinkFunction}
import org.apache.flink.streaming.api.scala._

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

    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    val dataStream: DataStream[String] = env.readTextFile("D:\\maven-poject\\flinkmaven\\src\\main\\resources\\sensor.txt")

    val stream = dataStream.map(
      data => {
        val split = data.split(",")
        SensorReading(split(0),split(1).trim.toLong,split(2).trim.toDouble)
      }    )

    stream.addSink(new JDBCSink())

    env.execute("JDBCSink")
  }
}

其他sink之后补充

(五)flink 序列化?

Flink有自己的序列化器和反序列化器

flink 摒弃了 Java 原生的序列化方法,以独特的方式处理数据类型和序列化,包含自己的类型描述符,泛型类型提取和类型序列化框架。
TypeInformation 是所有类型描述符的基类。它揭示了该类型的一些基本属性,并且可以生成序列化器。

TypeInformation 支持以下几种类型:

  • BasicTypeInfo: 任意 Java 基本类型或 String 类型
  • BasicArrayTypeInfo: 任意 Java 基本类型数组或 String 数组
  • WritableTypeInfo: 任意 Hadoop Writable 接口的实现类
  • TupleTypeInfo: 任意的 Flink Tuple 类型(支持 Tuple1 to Tuple25)。
    • Flink tuples 是固定长度固定类型的 Java Tuple 实现,不支持空值存储。
  • CaseClassTypeInfo: 任意的 Scala CaseClass(包括 Scala tuples)
  • PojoTypeInfo: 任意的 POJO (Java or Scala)
    • 例如,Java 对象的所有成员变量,要么是 public 修饰符定义,要么有 getter/setter 方法
  • GenericTypeInfo: 任意无法匹配之前几种类型的类

四、Flink中的window

(一)window是什么?

streaming 流式计算是一种被设计用于处理无限数据集数据处理引擎,【无限数据集是指一种不断增长的本质上无限的数据集】

window 是一种**切割无限数据为有限块进行处理的手段**。

Window 是无限数据流处理的核心,Window 将一个无限的 stream 拆分成有限大小的”buckets”桶,我们可以在这些桶上做计算操作

(二)window的类型

可以分为两类

  • CountWindow:根据指定的数据条数生成window(与时间无关)
    • 滚动窗口[较少使用]
    • 滑动窗口[较少使用]
  • TimeWindow:根据时间生成window
    • 滚动窗口(Tumbling Window):将数据依据固定的窗口长度对数据进行切片。

      • 特点:时间对齐,窗口长度固定,没有重叠。
      • 适用场景:适合做 BI 统计等(做每个时间段的聚合计算)
    • 滑动窗口(Sliding Window):滑动窗口由固定的窗口长度和滑动间隔组成。【如果滑动间隔参数小于窗口大小的话,窗口重叠,元素会被分配到多个窗口中。】

      • 特点:时间对齐,窗口长度固定,可以有重叠。
      • 适用场景:对最近一个时间段内的统计
    • 会话窗口(Session Window):由一系列事件组合一个指定时间长度的timeout间隙组成,一段时间没有接收到新数据就会生成新的窗口。指定session gap

      • 特点:时间无对齐。

(三)window API

window()必须在keyBy()之后才能调用

不用keyBy()方法,那么使用windowAll()方法:windowAll不能并行,尽量少用

如图:

Flink--框架和分布式处理引擎----flink大全_第5张图片

Flink--框架和分布式处理引擎----flink大全_第6张图片

PS:窗口:左闭右开

(1)窗口分配器(window assigner):确定数据属于哪个窗口

window () 方法接收的输入参数是一个 Window Assigner

Window Assigner负责将每条输入的数据分发到正确的window中

Flink 提供了通用的Window Assigner

  • 滚动窗口(tumbling window)
  • 滑动窗口(sliding window)
  • 会话窗口(session window)
  • 全局窗口(global window)
(2)Window
TimeWindow:

滚动事件时间窗口:

.window(TumblingEventTimeWindows.of(Time.seconds(15)))

滑动事件时间窗口:

.window(SlidingEventTimeWindows.of(Time.seconds(15),Time.seconds(10)))

滚动处理时间窗口:

.window(TumblingProcessingTimeWindows.of(Time.seconds(15)))

滑动处理时间窗口:

.window(SlidingProcessingTimeWindows.of(Time.seconds(15),Time.seconds(10)))

会话窗口:

.window(EventTimeSessionWindows.withGap(Time.seconds(10)))

	val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    //flink 1.12 之前默认使用时 处理时间
    //设置事件时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    //source
    val ds: DataStream[String] = env.readTextFile("E:\\idea\\workspaceIdea\\flinkmaven\\src\\main\\resources\\sensor.txt")

    val ds01: DataStream[SensorReading] = ds.map(line => {
      val spited = line.split(",")
      SensorReading(spited(0), spited(1).trim.toLong, spited(2).trim.toDouble)
    })
	.assignAscendingTimestamps(_.timestamp * 1000)  //数据没有乱序。指定时间戳数据,flink不知道哪个数据是时间戳,需要指定



	val value: DataStream[(String, Double)] = ds01.map(sensor => (sensor.sensorId, sensor.temp))  //(String,Double)
      .keyBy(_._1)
//      .window(TumblingEventTimeWindows.of(Time.seconds(15)))
//      .window(SlidingEventTimeWindows.of(Time.seconds(15),Time.seconds(10)))
//      .window(TumblingProcessingTimeWindows.of(Time.seconds(15)))
//      .window(SlidingProcessingTimeWindows.of(Time.seconds(15),Time.seconds(10)))
      .window(EventTimeSessionWindows.withGap(Time.seconds(10)))
      .reduce((t1, t2) => (t1._1, t1._2.min(t2._2)))

    value.print("min temp")

提供了两个简单的api供使用:TimeWindow 和 CountWindow

(2.1)Window------- TimeWindow

TimeWindow 是将指定时间范围内的所有数据组成一个 window一次对一个 window 里面的所有数据进行计算

TimeWindow可以是

  • ***滚动时间窗口(tumbling time window)***:.timeWindow(Time.seconds(15))

  • ***滑动时间窗口(sliding time window)***:.timeWindow(Time.seconds(10),Time.seconds(5))

    • 滚动窗口和滑动窗口的函数名一致,只是参数不一样,滑动窗口多了一个参数— sliding_size
  • ***会话窗口(session window)***:.window(EventTimeSessionWindows.withGap(Time.seconds(10)))

    • 还有ProcessingTimeSessionWindows等
    • 不用TimeWindow
val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)

    //flink 1.12 之前默认使用时 处理时间
    //设置事件时间
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)


    //source
    val ds: DataStream[String] = env.readTextFile("E:\\idea\\workspaceIdea\\flinkmaven\\src\\main\\resources\\sensor.txt")

    val ds01: DataStream[SensorReading] = ds.map(line => {
      val spited = line.split(",")
      SensorReading(spited(0), spited(1).trim.toLong, spited(2).trim.toDouble)
    })

    //需求:统计15s内 ,每个sensor的最低温度
    //import org.apache.flink.streaming.api.windowing.time.Time
   
   	val result = ds01.map(sensor => (sensor.sensorId, sensor.temp))
      .keyBy(_._1)
//      .timeWindow(Time.seconds(15)) //滚动窗口
//      .timeWindow(Time.seconds(10),Time.seconds(5))  //滑动窗口
		.window(EventTimeSessionWindows.withGap(Time.seconds(10)))  //会话窗口
      .reduce((tuple01, tuple02) => (tuple01._1, tuple01._2.min(tuple02._2)))

PS:时间间隔可以通过 Time.milliseconds(x),Time.seconds(x),Time.minutes(x)等其中的一个来指定。

(2.2)Window------- CountWindow

根据窗口中相同 key 元素的数量来触发执行,执行时只计算元素数量达到窗口大小的 key 对应的结果。

注意:CountWindow 的 window_size 指的是相同 Key 的元素的个数,不是输入的所有元素的总数。

默认的 CountWindow 是一个滚动窗口

  • 滚动窗口(tumbling count window)
    • 默认的 CountWindow 是一个滚动窗口,只需要指定窗口大小即可,当元素数量达到窗口大小时,就会触发窗口的执行。
  • 滑动窗口(sliding count window)
    • 滑动窗口和滚动窗口的函数名是完全一致的,只是在传参数时需要传入两个参数,一个是 window_size,一个是sliding_size
    • 代码中的 sliding_size 设置为了5,也就是说,每收到5个相同 key 的数据就计算一次,每一次计算的 window 范围是 10 个元素

    val result = ds01.map(sensor => (sensor.sensorId, sensor.temp))
      .keyBy(_._1)
//      .countWindow(2) // 滚动窗口
      .countWindow(10,5)  // 滑动窗口:数据滑动,有重叠
      .reduce((tuple01, tuple02) => (tuple01._1, tuple01._2.min(tuple02._2)))

    result.print("min temp")
(3)窗口函数 window function

window function 定义了要对窗口中收集的数据做的计算操作

主要可分为两类:

  • 增量聚合函数(incremental aggregation functions)
    • 每条数据到来就进行计算,保持一个简单的状态。
    • 典型的增量聚合函数有 ReduceFunction, AggregateFunction
  • 全窗口函数/全量聚合函数(full window functions)
    • 先把窗口所有数据收集起来,等到计算的时候会遍历所有数据。
    • ProcessWindowFunction 就是一个全窗口函数。
(4)其他可选API
  • .trigger() —触发器
    • 定义window什么时候关闭,触发计算并输出结果
  • .evitor() ---- 移除器
    • 定义移除某些数据的逻辑
  • .allowedLateness() ---- 允许处理迟到的数据
  • .sideOutputLateData() ---- 将迟到的数据放入侧输出流
  • .getSideOutput() ---- 获取侧输出流

五、时间语义和Wartermark

(一)时间语义

在 Flink 的流式处理中,会涉及到时间的不同概念:

Event Time:是事件创建的时间。它通常由事件中的时间戳描述,例如采集的 日志数据中,每一条日志都会记录自己的生成时间,Flink 通过时间戳分配器访问事件时间戳。**应用场景:**实际应用最常见的时间语义

Ingestion Time:是数据进入 Flink 的时间。 **应用场景:**没有事件时间的情况下,或者对实时性要求超高的情况下使用

Processing Time:是每一个执行基于时间操作的算子的本地系统时间,与机器相关,默认的时间属性就是 Processing Time。**应用场景:**存在多个 Source Operator 的情况下,每个 Source Operator 可以使用自己本地系统时钟指派 Ingestion Time。后续基于时间相关的各种操作, 都会使用数据记录中的 Ingestion Time。

Flink--框架和分布式处理引擎----flink大全_第7张图片

不同的时间语义有不同的应用场合

在 Flink 的流式处理中,绝大部分的业务都会使用 eventTime

一般只在 eventTime 无法使用时,才会被迫使用 ProcessingTime 或者 IngestionTime。

(二)引入事件时间 EventTime

flink 1.12 之前默认使用是 处理时间 Processing Time
如果要使用, 需要设置事件时间

env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

此外,flink不知道哪个数据是时间戳,需要指定(必须指定,数据源里的数据没有时间戳的话,就只能使用 Processing Time 了)。

如果数据没有乱序,那么

.assignAscendingTimestamps(_.timestamp * 1000)  //数据没有乱序。指定时间戳数据,flink不知道哪个数据是时间戳,需要指定

如果数据存在乱序,那么

.assignTimestampsAndWatermarks(          WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3))
            .withTimestampAssigner(new SerializableTimestampAssigner[SensorReading] {
              override def extractTimestamp(t: SensorReading, l: Long): Long = t.timestamp * 1000L
            }))

(三)waterMark

(1)Watermark 产生背景:

流处理从事件产生,到流经 source,再到 operator,中间是有一个过程和时间的

大部分情况下,流到 operator 的数据都是按照事件产生的时间顺序来的

但是可能由于网络、分布式等原因,导致乱序的产生,即 Flink 接收到的事件的先后顺序不是严格按照事件的 Event Time 顺序排列的。

我们想要处理延迟的数据,需要等一等它。我们不能明确数据是否全部到位,又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发 window 去进行计算,这个特别的机制,就是 Watermark。

我们使用 Watermark 处理乱序,准确来说是 Watermark+window

(2)Watermark概述

Watermark 是衡量 ==事件时间(Event Time)==进展的机制,可以设定延迟触发

Watermark 用于处理乱序,准确来说是 Watermark+window

数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据都已经到达了。因此,window 的执行也是由 Watermark 触发的。

Watermark 用来让程序自己平衡延迟和结果正确性

Watermark 可以理解成一个延迟触发机制,我们可以设置 Watermark 的延时时长 t,每次系统会校验已经到达的数据中最大的 maxEventTime,然后认定 eventTime 小于 maxEventTime - t 的所有数据都已经到达,如果有窗口的停止时间等于 maxEventTime – t,那么这个窗口被触发执行。

Watermark 就是触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中。只要没有达到水位那么不管现实中的时间推进了多久都不会触发关窗

Watermark 就等于当前所有到达数据中的 maxEventTime - 延迟时长,也就是说,Watermark 是 由数据携带的

(3)waterMark特点
  • waterMark是一条特殊的数据记录
  • waterMark必须单调递增,以保证任务的事件时间时钟在向前推进,而不是后退
  • waterMark与数据的时间戳相关
(4)引入WaterMark
def main(args: Array[String]): Unit = {
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    env.setParallelism(1)
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //指定为事件时间

    //source
    val ds: DataStream[String] = env.socketTextStream("localhost","99999")
    val ds01: DataStream[SensorReading] =  ds.map(line => {
        val spited = line.split(",")
        SensorReading(spited(0), spited(1).trim.toLong, spited(2).trim.toDouble)
      })
    // 引入WaterMark
        .assignTimestampsAndWatermarks(          WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(3))
            .withTimestampAssigner(new SerializableTimestampAssigner[SensorReading] {
              override def extractTimestamp(t: SensorReading, l: Long): Long = t.timestamp * 1000L
            }))
(5)自定义WaterMark

Flink 暴露了 TimestampAssigner 接口供我们实现,使我们可以自定义如何从事件数据中抽取时间戳。

  • AssignerWithPeriodicWatermarks :周期性生成watermark,系统会周期性的将 watermark 插入到流中(水位线也是一种特殊的事件!)。默认周期是 200 毫秒

    • env.getConfig.setAutoWatermarkInterval(5000)
      
    • 自定义

    • class PeriodicAssigner extends AssignerWithPeriodicWatermarks[SensorRead] {}
      
  • AssignerWithPunctuatedWatermarks :间断无规律地生成watermark,根据需要对每条数据进行筛选和处理。

    • class PunctuatedAssigner extends AssignerWithPunctuatedWatermarks[SensorRead] {}
      

AssignerWithPeriodicWatermarks产生 watermark 的逻辑:

每隔 5 秒钟,Flink 会调用 AssignerWithPeriodicWatermarks 的 getCurrentWatermark()方法。如果方法返回一个 时间戳大于之前水位的时间戳,新的 watermark 会被插入到流中。这个检查保证了水位线是单调递增的。如果方法返回的时间戳小于等于之前水位的时间戳,则不会产生新的 watermark

一种简单的特殊情况是,如果我们事先得知数据流的时间戳是单调递增的,即没有乱序,那我们可以使用 assignAscendingTimestamps,这个方法会直接使用数据的时间戳生成 watermark。

(四)flink如何处理迟到的数据?

​ 设置允许延迟的时间:通过allowedLateness(lateness: Time)设置
​ 保存延迟数据:通过sideOutputLateData(outputTag: OutputTag[T])保存,将迟到的数据放入侧输出流
​ 获取延迟数据:通过DataStream.getSideOutput(tag: OutputTag[X])获取

(五)侧输出流是什么?

​ flink中的侧输出就是将数据流进行分割,而不对流进行复制的一种分流机制,
​ flink的侧输出的另一个作用就是对延时迟到的数据进行处理,这样就可以不必丢弃迟到的数据。

六、ProcessFunction API(底层 API)

ProcessFunction API ---- 分层API的最底层

转换算子无法访问事件的时间戳信息和水位线信息的。

为了获取此类信息,DataStream API 提供了一系列的 Low-Level 转换算子,可以访问时间戳、watermark 以及注册定时事件。还可以输出特定的一些事件,例如超时事件等。

Process Function 用来构建事件驱动的应用以及实现自定义的业务逻辑(使用之前的 window 函数和转换算子无法实现)。

ProcessFunction可以看作是一个具有keyed statetimers访问权的 FlatMapFunction
通过RuntimeContext访问keyed state
计时器允许应用程序对处理时间和事件时间中的更改作出响应。
对processElemet(…)函数的每次调用都获得一个Context对象,该对象可以访问元素的event time timestamp和TimerService

TimerService可用于为将来的event/process time瞬间注册回调。
当到达计时器的特定时间时,将调用onTimer(…)方法。在该调用期间,所有状态都再次限定在创建计时器时使用的键的范围内,从而允许计时器操作键控状态

Flink 提供了 8 个 Process Function:

[ 所有的 Process Function 都继承自 RichFunction 接口,所以都有 open()、close()和 getRuntimeContext()等方法。]

  1. ProcessFunction
  2. KeyedProcessFunction
  3. CoProcessFunction
  4. ProcessJoinFunction
  5. BroadcastProcessFunction
  6. KeyedBroadcastProcessFunction
  7. ProcessWindowFunction
  8. ProcessAllWindowFunction

(一)keyedProcessFunction ⭐

KeyedProcessFunction 用来操作 KeyedStream

KeyedProcessFunction 会处理流 的每一个元素,输出为 0 个、1 个或者多个元素。

所有的 Process Function 都继承自 RichFunction 接口,所以都有 open()、close()和 getRuntimeContext()等方法。

KeyedProcessFunction[KEY, IN, OUT]额外提供了两个方法

(1)processElement(v: IN, ctx: Context, out: Collector[OUT])

  • ​ 流中的每一个元素都会调用这个方法,调用结果将会放在 Collector 数据类型中输出

  • Context 可以访问元素的时间戳,元素的 key,以及 TimerService 时间服务。

  • ​ Context 还可以将结果输出到别的流(side outputs)。

(2)onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])

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

定时器 timer 只能在 keyed streams 上面使用

(二)TimerService 和 定时器(Timers)

Context 和 OnTimerContext 所持有的 TimerService 对象拥有以下方法:

  • currentProcessingTime(): Long ---------- 返回当前处理时间

  • currentWatermark(): Long ---------- 返回当前 watermark 的时间戳

  • registerProcessingTimeTimer(timestamp: Long): Unit ----------- 会注册当前 key 的 processing time 的定时器。当 processing time 到达定时时间时,触发 timer。

  • registerEventTimeTimer(timestamp: Long): Unit --------- 会注册当前 key 的 event time 定时器,当水位线大于等于定时器注册的时间时,触发定时器执行回调函数(onTimer)。

  • deleteProcessingTimeTimer(timestamp: Long): Unit ------------ 删除之前注册处理时间定时器。如果没有这个时间戳的定时器,则不执行。

  • deleteEventTimeTimer(timestamp: Long): Unit ------------ 删除之前注册的事件时间定时 器,如果没有此时间戳的定时器,则不执行。

    当定时器 timer 触发时,会执行回调函数 onTimer()。

注意:定时器 timer 只能在 keyed Streams上面使用

七、状态编程 + 容错机制

状态编程基于 运行环境的上下文

流式计算分为无状态有状态两种情况。

  • 无状态的计算观察每个独立事件,并根据最后一个事件输出结果

  • 有状态的计算则会基于多个事件输出结果。

    • 所有类型的窗口
    • 所有用于复杂事件处理的状态机
    • 流与流之间的所有关联操作,以及流与静态表或动态表之间的关联操作

无状态流处理和有状态流处理的主要区别:
无状态流处理分别接收每条数据记录,然后根据最新输入的数据生成输出数据。
有状态流处理会维护状态(根据每条输入记录进行更新),并基于最新输入的记录和当前的状态值生成输出记录。

(一)Flink状态

Flink state 总的来说,有两种状态

  • 算子状态(operator state)
    • 作用范围:算子任务
    • 整个 Operator 只对应一个 State
    • 同一并行任务所处理的所有数据都可以访问到相同的状态,状态对于同一任务而言是共享的。算子状态不能由 相同或不同算子的另一个任务访问。
    • Flink为 operator state 提供了3种基本数据结构
      • 列表状态( LIst state):将状态表示为一组数据的列表
      • 联合列表状态( Union list state):将状态表示为数据的列表【它与LIst state的区别在于:在发生故障时,或者从保存点(savepoint)启动应用程序时如何恢复。】
      • 广播状态(Broadcast state):如果一个算子有多项任务,而它的每项任务状态又都相同,那么这种特殊情况最适合应 用广播状态
  • 键控状态(keyed state)
    • 作用范围:根据输入数据流中定义的键key来维护和访问
    • keyedStream上的每一个key都对应一个state
    • Flink 为每个键值维护一个状态实例,并将具有相同键的所有数据,都分区到同一个算子任务中,这个任务会维护和处理这个 key 对应的状态。当任务处理一条数据时,它会自动将状态的访问范围限定为当前数据的 key。因此,具有相同 key 的所有数据都会访问相同的状态
    • Flink的 Keyed state 支持以下数据类型
      • ValueState[T]:保存单个的值,值的类型为 T。
        • get 操作: ValueState.value()
        • set 操作: ValueState.update(value: T)
      • ListState[T]:保存一个列表,列表里的元素的数据类型为 T。
        • ListState.add(value: T)
        • ListState.addAll(values: java.util.List[T])
        • ListState.get()返回 Iterable[T]
        • ListState.update(values: java.util.List[T])
      • MapState[K, V]:保存 Key-Value 对。
        • MapState.get(key: K)
        • MapState.put(key: K, value: V)
        • MapState.contains(key: K)
        • MapState.remove(key: K)
      • ReducingState[T]
      • AggregatingState[I, O]

PS:State.clear()是清空操作。

(二)状态一致性 / 正确性级别

(1)什么是状态一致性?

​ 有状态的流处理,内部每个算子任务都可以有自己的状态。

​ 对于流处理器来说状态一致性就是计算结果要保证准确,一条数据不应该丢失,也不应该重复计算。

​ 在遇到故障时可以恢复,恢复以后的重新计算,结果应该也是完全正确的

(2)一致性级别 [3]
  • at-most-once:没有正确性保障。故障发生之后,计数结果可能丢失
  • at-least-once:计数程序在发生故障后可能多算,但是绝不会少算
  • exactly-once:是系统保证在发生故障后得到的计数结果与正确值一致

Flink 的一个重大价值在于,它既保证了 exactly-once,也具有低延迟和高吞吐的处理能力。从根本上说,Flink 通过使自身满足所有需求来避免了二者之间的权衡

(3)flink 状态管理包含哪些?

状态一致性故障处理 以及 高效存储和访问,以便开发人员可以专注于应用程序的逻辑。

(4)端到端(end-to-end)状态一致性

真实应用中,流处理应用包含 ① 流处理器 ② 数据源(例如 Kafka) ③ 输出到持久化系统。

端到端的一致性保证,意味着结果的正确性贯穿了整个流处理应用的始终

每一个组件都保证了它自己的一致性,整个端到端的一致性级别取决于所有组件中一致性最弱的组件

具体可以划分如下

  • 内部保证 —— 依赖 checkpoint
  • source 端 —— 需要外部源可重设数据的读取位置
  • sink 端 —— 需要保证从故障恢复时,数据不会重复写入外部系统
    • sink端有两种具体的实现方式:
      • 幂等(Idempotent)写入**:一个操作可重复执行多次,但只导致一次结果更改**,后面再重复执行不起作用
      • 事务性(Transactional)写入**:**构建事物来写入外部系统,构建的事务对应着 checkpoint,等到 checkpoint 真正完成的时候,才把所有对应的结果写入 sink 系统中
        • 实现方式:
        • 预写日志(WAL):
        • 两阶段提交(2PC:Two-Phase-Commit):每个算子执行完成,会进行“预提交”,直到执行完 sink 操作,会发起“确认提交”,如果执行失败,预提交会放弃掉
          • 对于每个checkpoint, sink 任务会启动一个事务,并将接下来所有接收的数据添加到事务里然后将这些数据写入外部sink系统,但不提交它们–这时只是预提交
          • 当它收到checkpoint完成的通知时,它才正式提交事务,实现结果的真正写入
          • 这种方式真正实现了exactly-once,它需要一个提供事务支持的外部sink系统。
          • Flink 提供了TwoPhaseCommitSinkFunction接口。

不同 Source 和 Sink 的一致性保证可以用下表说明:
Flink--框架和分布式处理引擎----flink大全_第8张图片

(5)Flink 具体如何保证 exactly-once 呢?

​ Flink通过实现**两阶段提交**和==状态保存==来实现端到端的一致性语义。 分为以下几个步骤:

  1. ​ 开始事务(beginTransaction)创建一个临时文件夹,来写,把数据写入到这个文件夹里面
  2. ​ 预提交(preCommit)将内存中缓存的数据写入文件并关闭
  3. ​ 正式提交(commit)将之前写完的临时文件放入目标目录下。这代表着最终的数据会有一些延迟
  4. ​ 丢弃(abort)丢弃临时文件
  5. ​ 若失败发生在预提交成功后,正式提交前。可以根据状态来提交预提交的数据,也可删除预提交的数据。

(三)检查点checkpoint

Flink 检查点的核心作用是确保状态正确,即使遇到程序中断,也要正确

(1)Flink 检查点算法的正式名称是异步分界线快照

Flink--框架和分布式处理引擎----flink大全_第9张图片

检查点是 Flink 最有价值的创新之一,因为它使 Flink 可以保证 exactly-once, 并且不需要牺牲性能。

如何从检查点恢复状态?
在执行流应用程序期间,Flink 会定期保存状态的一致性检查点
如果发生故障,Flink 将会使用最近的检查点来一致恢复应用程序的状态,并重新启动处理流程
遇到故障之后,

  • 第一步就是重启应用

  • 第二步是从checkpoint中读取状态,将状态重置
    从检查点重新启动应用程序后,其内部状态与检查点完成时的状态完全相同

  • 第三步:开始消费并处理检查点到发生故障之间的所有数据

  • 这种检查点的保存和恢复机制可以为应用程序状态提供"精确一次"(exactly-once)的一致性,因为所有算子都会保存检查点并恢复其所有状态,这样一来所有的输入流就都会被重置到检查点完成时的位置

(2)检查点checkpoint和保存点savepoint的区别?

checkpoint 的侧重点是“容错”,即Flink作业意外失败并重启之后,能够直接从早先打下的checkpoint恢复运行,
且不影响作业逻辑的准确性。
savepoint 的侧重点是“维护”,即Flink作业需要在人工干预下手动重启、升级、迁移或A/B测试时,
先将状态整体写入可靠存储,维护完毕之后再从savepoint恢复现场。

savepoint是“通过checkpoint机制”创建的,所以savepoint本质上是特殊的checkpoint。

(3)Flink 的 checkpoint 机制对比 spark 有什么不同和优势?

spark streaming 的 checkpoint 仅仅是针对 driver 的故障恢复做了数据和元数据的 checkpoint

flink 的 checkpoint 机制要复杂了很多,它采用的是 轻量级的分布式快照实现了每个算子的快照,及流动中的数据的快照

(四)Flink+Kafka 如何实现端到端的 exactly-once

对于 Flink + Kafka 的数据管道系统(Kafka 进、Kafka 出)而言,各组件怎样保证 exactly-once 语义?

简洁版:

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

详细分析版:

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

  • 第一条数据来了之后,开启一个 kafka 的事务(transaction),正常写入 kafka 分区日志但标记为未提交,这就是“预提交”

  • 当 checkpoint 启动时,JobManager 会将检查点分界线(barrier)注入数据流; barrier 会在算子间传递下去。每个算子会对当前的状态做个快照,保存到状态后端,并通知JobManager 。

  • 对于 source 任务而言, 就会把当前的 offset 作为状态保存起来。下次从 checkpoint 恢复时,source 任务可 以重新提交偏移量,从上次保存的位置开始重新消费数据

  • 每个内部的 transform 任务遇到 barrier 时,都会把状态存到 checkpoint

  • sink 任务首先把数据写入外部 kafka,这些数据都属于预提交的事务(还不能被消费);当遇到 barrier 时,把状态保存到状态后端,并开启新的预提交事务。

  • 当所有算子任务的快照完成,也就是这次的 checkpoint 完成时,JobManager 会向所有任务发通知,确认这次 checkpoint 完成

  • 当 sink 任务收到确认通知,就会正式提交之前的事务,kafka 中未确认的数据就改为“已确认”,数据就真正可以被消费了。

PS:如果宕机需要通过 StateBackend 进行恢复,只能恢复所有确认提交的操作

(五)状态后端 / Flink的状态存储方式

状态的存储、访问以及维护,由一个可插入的组件决定,这个组件就叫做状态后端。

作用:本地的状态管理,以及将检查点状态写入远程存储

每传入一条数据,有状态的算子任务都会读取和更新状态

由于有效的状态访问对于处理数据的低延迟至关重要,因此每个并行任务都会在本地维护其状态,以确保快速的状态访问

env.setStateBackend(
    new MemoryStateBackend(11212,false)new FsStateBackend("hdfs://")new RocksDBStateBackend(""))

Flink 在做计算的过程中经常需要存储中间状态,来避免数据丢失和状态恢复。

选择的状态存储策略不同,会影响状态持久化如何和 checkpoint 交互。

Flink提供下表3种状态存储方式。

状态存储于 checkpoint存储于
MemoryStateBackend 测试用 将键控状态作为内存中的对象进行管理,存储在 TaskManager 的 JVM 堆上 JobManager 的内存
FsStateBackend 一般情况用 本地状态存在 TaskManager 的 JVM 堆上 远程的持久化文件系统(FileSystem)上
RocksDBStateBackend 超大状态用 将所有状态序列化后,存入本地的 RocksDB 中存储(内存+磁盘) 本地/HDFS

PS:RocksDB 的支持并不直接包含在 flink 中,需要引入依赖。对于状态state的读写效率要低一些

//状态后端设置
    //----MemoryStateBackend:state存内存,checkpoint存内存     -=====  开发不用,用于测试
    //将键控状态作为内存中的对象进行管理,存储在 TaskManager 的 JVM 堆上;将 checkpoint 存储在 JobManager 的内存中。
    env.setStateBackend(new MemoryStateBackend(11212,false))

    //----fsStateBackend:state存内存【存在 TaskManager 的 JVM堆】,checkpoint存FS(本地/HDFS)  ===== 一般情况 用
    val fsStateBackend: FsStateBackend = new FsStateBackend("hdfs://")
    env.setStateBackend(fsStateBackend)

    //----rocksDBStateBackend:state存RocksDB(内存+磁盘)【将所有状态序列化后,存入本地的 RocksDB 中存储】,checkpoint存FS(本地/HDFS) ==== 超大状态使用,对于状态state的读写效率要低一些
    val rocksDBStateBackend = new RocksDBStateBackend("")
    env.setStateBackend(rocksDBStateBackend)

(六)Flink的重启策略

1、配置了Checkpoint的情况下不做任务配置:默认是无限重启并自动恢复,可以解决小问题,但是可能会隐藏真正的bug

2、无重启策略

3、固定延迟重启策略–开发中常用

4、失败率重启策略–开发中偶尔使用

  //===========配置重启策略:
    import org.apache.flink.api.common.restartstrategy.RestartStrategies
    //1.配置了Checkpoint的情况下不做任务配置:默认是无限重启并自动恢复,可以解决小问题,但是可能会隐藏真正的bug

    //2.单独配置无重启策略
    env.setRestartStrategy(RestartStrategies.noRestart())

    //3.固定延迟重启--开发中常用
    env.setRestartStrategy(RestartStrategies.fixedDelayRestart(
      3,  // 最多重启3次数
      10000L// 重启时间间隔
    ))
            //上面的设置表示:如果job失败,重启3次, 每次间隔10s

    //4.失败率重启--开发中偶尔使用
    //设置表示:如果1分钟内job失败不超过三次,自动重启,每次重启间隔3s (如果1分钟内程序失败达到3次,则程序退出)
    import org.apache.flink.api.common.time.Time
    env.setRestartStrategy(RestartStrategies.failureRateRestart(  //重启失败比率
      3, // 每个测量阶段内最大失败次数
      Time.minutes(1), //失败率测量的时间间隔
      Time.seconds(3) // 两次连续重启的时间间隔

八、CEP

简要介绍,详细的在另一篇文档

(1)什么是CEP?

一个或多个由简单事件构成的事件流通过一定的**规则匹配**,然后输出用户想得的数据,满足规则的复杂事件。

(2)cep的模式 ----- 处理事件的规则。

Flink CEP提供了Pattern API,用于对输入流数据进行复杂事件规则定义,用来提取符合规则的事件序列

模式有:个体模式、组合模式/模式序列、模式组

(3)近邻模式分为?

严格近邻
宽松近邻
非确定性宽松近邻
nager 的内存中。
env.setStateBackend(new MemoryStateBackend(11212,false))

你可能感兴趣的:(flink,大数据,flink)