2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51

时间煮雨
@R星校长

大数据技术之Flink

第一章 初识Flink

  在当前数据量激增的时代,各种业务场景都有大量的业务数据产生,对于这些不断产生的数据应该如何进行有效的处理,成为当下大多数公司所面临的问题。目前比较流行的大数据处理引擎Apache Spark,基本上已经取代了MapReduce成为当前大数据处理的标准。但对实时数据处理来说,Apache Spark的Spark-Streaming还有性能改进的空间。对于Spark-Streaming的流计算本质上还是批(微批)计算,Apache Flink就是近年来在开源社区不断发展的技术中的能够同时支持高吞吐、低延迟、高性能的纯实时的分布式处理框架。

1. Flink是什么?

  1) Flink 的发展历史

  在2010年至2014年间,由柏林工业大学、柏林洪堡大学和哈索普拉特纳研究所联合发起名为“Stratosphere:Information Management on the Cloud”研究项目,该项目在当时的社区逐渐具有了一定的社区知名度。2014年4月,Stratosphere代码被贡献给Apache软件基金会,成为Apache基金会孵化器项目。初期参与该项目的核心成员均是Stratosphere曾经的核心成员,之后团队的大部分创始成员离开学校,共同创办了一家名叫Data Artisans的公司,其主要业务便是将Stratosphere,也就是之后的Flink实现商业化。在项目孵化期间,项目Stratosphere改名为Flink。Flink在德语中是快速和灵敏的意思,用来体现流式数据处理器速度快和灵活性强等特点,同时使用棕红色松鼠图案作为Flink项目的Logo,也是为了突出松鼠灵活快速的特点,由此,Flink正式进入社区开发者的视线。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第1张图片
  2014年12月,该项目成为Apache软件基金会顶级项目,从2015年9月发布第一个稳定版本0.9,到目前为止已经发布到1.11的版本,更多的社区开发成员逐步加入,现在Flink在全球范围内拥有350多位开发人员,不断有新的特性发布。同时在全球范围内,越来越多的公司开始使用Flink,在国内比较出名的互联网公司如阿里巴巴、美团、滴滴等,都在大规模使用Flink作为企业的分布式大数据处理引擎。

  2) Flink的定义

  Apache Flink 是一个框架和分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算。Flink 能在所有常见集群环境中运行,并能以内存速度和任意规模进行计算。
  Apache Flink is a framework and distributed processing engine for stateful computations over unbounded and bounded data streams. Flink has been designed to run in all common cluster environments, perform computations at in-memory speed and at any scale.

  3) 有界流和无界流

  任何类型的数据都可以形成一种事件流。信用卡交易、传感器测量、机器日志、网站或移动应用程序上的用户交互记录,所有这些数据都形成一种流。
  无界流: 有定义流的开始,但没有定义流的结束。它们会无休止地产生数据。无界流的数据必须持续处理,即数据被摄取后需要立刻处理。我们不能等到所有数据都到达再处理,因为输入是无限的,在任何时候输入都不会完成。处理无界数据通常要求以特定顺序摄取事件,例如事件发生的顺序,以便能够推断结果的完整性。
  有界流: 有定义流的开始,也有定义流的结束。有界流可以在摄取所有数据后再进行计算。有界流所有数据可以被排序,所以并不需要有序摄取。有界流处理通常被称为批处理。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第2张图片
  Apache Flink 擅长处理无界和有界数据集 精确的时间控制和状态化使得 Flink 的运行时(runtime)能够运行任何处理无界流的应用。有界流则由一些专为固定大小数据集特殊设计的算法和数据结构进行内部处理,产生了出色的性能。

  4) 有状态的计算架构

  数据产生的本质,其实是一条条真实存在的事件按照时间顺序源源不断的产生,我们很难在数据产生的过程中进行计算并直接产生统计结果,因为这不仅对系统有非常高的要求,还必须要满足高性能、高吞吐、低延时等众多目标。而有状态流计算架构(如图所示)的提出,从一定程度上满足了企业的这种需求,企业基于实时的流式数据,维护所有计算过程的状态,所谓状态就是计算过程中产生的中间计算结果,每次计算新的数据进入到流式系统中都是基于中间状态结果的基础上进行运算,最终产生正确的统计结果。基于有状态计算的方式最大的优势是不需要将原始数据重新从外部存储中拿出来,从而进行全量计算,因为这种计算方式的代价可能是非常高的。从另一个角度讲,用户无须通过调度和协调各种批量计算工具,从数据仓库中获取数据统计结果,然后再落地存储,这些操作全部都可以基于流式计算完成,可以极大地减轻系统对其他框架的依赖,减少数据计算过程中的时间损耗以及硬件存储。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第3张图片

2. 为什么要使用 Flink

  可以看出有状态流计算将会逐步成为企业作为构建数据平台的架构模式,而目前从社区来看,能够满足的只有Apache Flink。Flink通过实现Google Dataflow流式计算模型实现了高吞吐、低延迟、高性能兼具实时流式计算框架。同时Flink支持高度容错的状态管理,防止状态在计算过程中因为系统异常而出现丢失,Flink周期性地通过分布式快照技术Checkpoints实现状态的持久化维护,使得即使在系统停机或者异常的情况下都能计算出正确的结果。
  自 2019 年 1 月起,阿里巴巴逐步将内部维护的 Blink 回馈给 Flink 开源社区,目前贡献代码数量已超过 100 万行。国内包括腾讯、百度、字节跳动等公司,国外包括 Uber、Lyft、Netflix 等公司都是 Flink 的使用者。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第4张图片

3. Flink 的应用场景

  在实际生产的过程中,大量数据在不断地产生,例如金融交易数据、互联网订单数据、GPS定位数据、传感器信号、移动终端产生的数据、通信信号数据等,以及我们熟悉的网络流量监控、服务器产生的日志数据,这些数据最大的共同点就是实时从不同的数据源中产生,然后再传输到下游的分析系统。针对这些数据类型主要包括实时智能推荐、复杂事件处理、实时欺诈检测、实时数仓与ETL类型、流数据分析类型、实时报表类型等实时业务场景,而Flink对于这些类型的场景都有着非常好的支持。

  (一)实时智能推荐

  智能推荐会根据用户历史的购买行为,通过推荐算法训练模型,预测用户未来可能会购买的物品。对个人来说,推荐系统起着信息过滤的作用,对Web/App服务端来说,推荐系统起着满足用户个性化需求,提升用户满意度的作用。推荐系统本身也在飞速发展,除了算法越来越完善,对时延的要求也越来越苛刻和实时化。利用Flink流计算帮助用户构建更加实时的智能推荐系统,对用户行为指标进行实时计算,对模型进行实时更新,对用户指标进行实时预测,并将预测的信息推送给Wep/App端,帮助用户获取想要的商品信息,另一方面也帮助企业提升销售额,创造更大的商业价值。

  (二)复杂事件处理

  对于复杂事件处理,比较常见的案例主要集中于工业领域,例如对车载传感器、机械设备等实时故障检测,这些业务类型通常数据量都非常大,且对数据处理的时效性要求非常高。通过利用Flink提供的CEP(复杂事件处理)进行事件模式的抽取,同时应用Flink的Sql进行事件数据的转换,在流式系统中构建实时规则引擎,一旦事件触发报警规则,便立即将告警结果传输至下游通知系统,从而实现对设备故障快速预警监测,车辆状态监控等目的。

  (三)实时欺诈检测

  在金融领域的业务中,常常出现各种类型的欺诈行为,例如信用卡欺诈、信贷申请欺诈等,而如何保证用户和公司的资金安全,是来近年来许多金融公司及银行共同面对的挑战。随着不法分子欺诈手段的不断升级,传统的反欺诈手段已经不足以解决目前所面临的问题。以往可能需要几个小时才能通过交易数据计算出用户的行为指标,然后通过规则判别出具有欺诈行为嫌疑的用户,再进行案件调查处理,在这种情况下资金可能早已被不法分子转移,从而给企业和用户造成大量的经济损失。而运用Flink流式计算技术能够在毫秒内就完成对欺诈判断行为指标的计算,然后实时对交易流水进行规则判断或者模型预测,这样一旦检测出交易中存在欺诈嫌疑,则直接对交易进行实时拦截,避免因为处理不及时而导致的经济损失。

  (四)实时数仓与ETL

  结合离线数仓,通过利用流计算诸多优势和SQL灵活的加工能力,对流式数据进行实时清洗、归并、结构化处理,为离线数仓进行补充和优化。另一方面结合实时数据ETL处理能力,利用有状态流式计算技术,可以尽可能降低企业由于在离线数据计算过程中调度逻辑的复杂度,高效快速地处理企业需要的统计结果,帮助企业更好地应用实时数据所分析出来的结果。

  (五)流数据分析

  实时计算各类数据指标,并利用实时结果及时调整在线系统相关策略,在各类内容投放、无线智能推送领域有大量的应用。流式计算技术将数据分析场景实时化,帮助企业做到实时化分析Web应用或者App应用的各项指标,包括App版本分布情况、Crash检测和分布等,同时提供多维度用户行为分析,支持日志自主分析,助力开发者实现基于大数据技术的精细化运营、提升产品质量和体验、增强用户黏性。

  (六)实时报表分析

  实时报表分析是近年来很多公司采用的报表统计方案之一,其中最主要的应用便是实时大屏展示。利用流式计算实时得出的结果直接被推送到前端应用,实时显示出重要指标的变换情况。最典型的案例便是淘宝的双十一活动,每年双十一购物节,除疯狂购物外,最引人注目的就是天猫双十一大屏不停跳跃的成交总额。在整个计算链路中包括从天猫交易下单购买到数据采集、数据计算、数据校验,最终落到双十一大屏上展现的全链路时间压缩在5秒以内,顶峰计算性能高达数三十万笔订单/秒,通过多条链路流计算备份确保万无一失。而在其他行业,企业也在构建自己的实时报表系统,让企业能够依托于自身的业务数据,快速提取出更多的数据价值,从而更好地服务于企业运行过程中。

4. Flink 的特点和优势

1) Flink 的具体优势和特点有以下几点

 (一)同时支持高吞吐、低延迟、高性能

  Flink是目前开源社区中唯一一套集高吞吐、低延迟、高性能三者于一身的分布式流式数据处理框架。像Apache Spark也只能兼顾高吞吐和高性能特性,主要因为在Spark Streaming流式计算中无法做到低延迟保障;而流式计算框架Apache Storm只能支持低延迟和高性能特性,但是无法满足高吞吐的要求。而满足高吞吐、低延迟、高性能这三个目标对分布式流式计算框架来说是非常重要的。

 (二)支持事件时间(Event Time)概念

  在流式计算领域中,窗口计算的地位举足轻重,但目前大多数框架窗口计算采用的都是系统时间(Process Time),也是事件传输到计算框架处理时,系统主机的当前时间。Flink能够支持基于事件时间(Event Time)语义进行窗口计算,也就是使用事件产生的时间,这种基于事件驱动的机制使得事件即使乱序到达,流系统也能够计算出精确的结果,保持了事件原本产生时的时序性,尽可能避免网络传输或硬件系统的影响。

 (三)支持有状态计算

  Flink在1.4版本中实现了状态管理,所谓状态就是在流式计算过程中将算子的中间结果数据保存在内存或者文件系统中,等下一个事件进入算子后可以从之前的状态中获取中间结果中计算当前的结果,从而无须每次都基于全部的原始数据来统计结果,这种方式极大地提升了系统的性能,并降低了数据计算过程的资源消耗。对于数据量大且运算逻辑非常复杂的流式计算场景,有状态计算发挥了非常重要的作用。

 (四)支持高度灵活的窗口(Window)操作

  在流处理应用中,数据是连续不断的,需要通过窗口的方式对流数据进行一定范围的聚合计算,例如统计在过去的1分钟内有多少用户点击某一网页,在这种情况下,我们必须定义一个窗口,用来收集最近一分钟内的数据,并对这个窗口内的数据进行再计算。Flink将窗口划分为基于Time、Count、Session,以及Data-driven等类型的窗口操作,窗口可以用灵活的触发条件定制化来达到对复杂的流传输模式的支持,用户可以定义不同的窗口触发机制来满足不同的需求。

 (五)基于轻量级分布式快照(CheckPoint)实现的容错

  Flink能够分布式运行在上千个节点上,将一个大型计算任务的流程拆解成小的计算过程,然后将tesk分布到并行节点上进行处理。在任务执行过程中,能够自动发现事件处理过程中的错误而导致数据不一致的问题,比如:节点宕机、网路传输问题,或是由于用户因为升级或修复问题而导致计算服务重启等。在这些情况下,通过基于分布式快照技术的Checkpoints,将执行过程中的状态信息进行持久化存储,一旦任务出现异常停止,Flink就能够从Checkpoints中进行任务的自动恢复,以确保数据在处理过程中的一致性(Exactly-Once)。

 (六)基于JVM实现独立的内存管理

  内存管理是所有计算框架需要重点考虑的部分,尤其对于计算量比较大的计算场景,数据在内存中该如何进行管理显得至关重要。针对内存管理,Flink实现了自身管理内存的机制,尽可能减少JVM GC对系统的影响。另外,Flink通过序列化/反序列化方法将所有的数据对象转换成二进制在内存中存储,降低数据存储的大小的同时,能够更加有效地对内存空间进行利用,降低GC带来的性能下降或任务异常的风险,因此Flink较其他分布式处理的框架会显得更加稳定,不会因为JVM GC等问题而影响整个应用的运行。

 (七)Save Points(保存点)

  对于7*24小时运行的流式应用,数据源源不断地接入,在一段时间内应用的终止有可能导致数据的丢失或者计算结果的不准确,例如进行集群版本的升级、停机运维操作等操作。值得一提的是,Flink通过Save Points技术将任务执行的快照保存在存储介质上,当任务重启的时候可以直接从事先保存的Save Points恢复原有的计算状态,使得任务继续按照停机之前的状态运行,Save Points技术可以让用户更好地管理和运维实时流式应用。

2) 流式计算框架的对比

  Storm是比较早的流式计算框架,后来又出现了Spark Streaming和Trident,现在又出现了Flink这种优秀的实时计算框架,那么这几种计算框架到底有什么区别呢?

产品 模型 API 保证次数 容错机制 状态管理 延时 吞吐量
Strom Native(数据进入立即处理) 组合式(基础API) At-least-once(至少一次) ACK机制
Trident Mico-Batching(划分小批次处理) 组合式 Exactly-once(仅一次) ACK机制 基于每次操作都有一个状态 中等 中等
SparkStreaming Mico-Batching(划分小批次处理) 声明式(有封装好的高级API) Exactly-once 基于RDD做checkpoint 基于DStream 中等
Flink Native 声明式(有封装好的高级API) Exactly-once Flink checkpoint 基于操作
  • 模型:Storm和Flink是真正的一条一条处理数据;而Trident(Storm的封装框架)和Spark Streaming其实都是小批处理,一次处理一批数据(小批量)。
  • API:Storm和Trident都使用基础API进行开发,比如实现一个简单的sum求和操作;而Spark Streaming和Flink中都提供封装后的高阶函数,可以直接来使用,非常方便。
  • 保证次数:在数据处理方面,Storm可以实现至少处理一次,但不能保证仅处理一次,这样就会导致数据重复处理问题,所以针对计数类的需求,可能会产生一些误差;Trident通过事务可以保证对数据实现仅一次的处理,Spark Streaming和Flink也是如此。
  • 容错机制:Storm和Trident可以通过ACK机制实现数据的容错机制,而Spark Streaming和Flink可以通过CheckPoint机制实现容错机制。
  • 状态管理:Storm中没有实现状态管理,Spark Streaming实现了基于DStream的状态管理,而Trident和Flink实现了基于操作的状态管理。
  • 延时:表示数据处理的延时情况,因此Storm和Flink接收到一条数据就处理一条数据,其数据处理的延时性是很低的;而Trident和Spark Streaming都是小型批处理,它们数据处理的延时性相对会偏高。
  • 吞吐量:Storm的吞吐量其实也不低,只是相对于其他几个框架而言较低;Trident属于中等;而Spark Streaming和Flink的吞吐量是比较高的。
    2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第5张图片

第二章 Flink快速入门

1. Flink的开发环境

 Flink课程选择的是Apache Flink 1.9.1 版本,是目前最新的稳定版本,并且兼容性比较好。下载地址:
 https://flink.apache.org/zh/downloads.html

1) 开发工具

  先说明一下开发工具的问题。官方建议使用IntelliJ IDEA,因为它默认集成了Scala和Maven环境,使用更加方便,当然使用Eclipse也是可以的。我们这门课使用IDEA。开发Flink程序时,可以使用Java、Python或者Scala语言,本课程全部使用Scala,因为使用Scala实现函数式编程会比较简洁。学生可以在课后自己补充JAVA代码。

2) 配置依赖

  开发 Flink 应用程序需要最低限度的 API 依赖。最低的依赖库包括:flink-scala和flink-streaming-scala。大多数应用需要依赖特定的连接器或其他类库,例如 Kafka的连接器、TableAPI、CEP库等。这些不是 Flink 核心依赖的一部分,因此必须作为依赖项手动添加到应用程序中。

<dependency>
            <groupId>org.apache.flinkgroupId>
            <artifactId>flink-scala_2.11artifactId>
            <version>1.9.1version>
        dependency>
        <dependency>
            <groupId>org.apache.flinkgroupId>
            <artifactId>flink-streaming-scala_2.11artifactId>
            <version>1.9.1version>
        dependency>

    <build>
        <plugins>
            
            <plugin>
                <groupId>net.alchim31.mavengroupId>
                <artifactId>scala-maven-pluginartifactId>
                <version>3.4.6version>
                <executions>
                    <execution>
                        
                        <goals>
                            <goal>testCompilegoal>
                        goals>
                    execution>
                executions>
            plugin>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-assembly-pluginartifactId>
                <version>3.0.0version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependenciesdescriptorRef>
                    descriptorRefs>
                configuration>
                <executions>
                    <execution>
                        <id>make-assemblyid>
                        <phase>packagephase>
                        <goals>
                            <goal>singlegoal>
                        goals>
                    execution>
                executions>
            plugin>
        plugins>
    build>

2. 第一个 Flink 流处理(Streaming)案例

  创建项目,并且修改源代码目录为scala2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第6张图片2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第7张图片
案例需求:采用Netcat 数据源发送数据,使用Flink统计每个单词的数量。
注意: Flink流式处理数据时,需要导入隐式转换:org.apache.flink.streaming.api.scala._

package com.bjsxt.flink

import org.apache.flink.streaming.api.scala.{
     DataStream, StreamExecutionEnvironment}

//基于流计算的WordCount案例
object StreamWordCount {
     

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

    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._

    //读取数据
    val stream: DataStream[String] = streamEnv.socketTextStream("mynode5",8888)

    //转换计算
    val result: DataStream[(String, Int)] = stream.flatMap(_.split(","))
      .map((_, 1))
      .keyBy(0)
      .sum(1)

    //打印结果到控制台
    result.print()

    //启动流式处理,如果没有该行代码上面的程序不会运行
    streamEnv.execute("wordcount")
  }
}

 在Linux系统中使用nc命令发送数据测试

 nc  -lk  8888

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第8张图片

3. 第一个Flink批处理(Batch)案例

需求:读取本地数据文件,统计文件中每个单词出现的次数。
根据需求,很明显是有界流(批计算),所以采用另外一个上下文环境:ExecutionEnvironment

package com.bjsxt.flink

import java.net.URL

import org.apache.flink.api.scala.ExecutionEnvironment

object BatchWordCount {
     

  def main(args: Array[String]): Unit = {
     
    //初始化flink的环境
    val env: ExecutionEnvironment = ExecutionEnvironment.getExecutionEnvironment

    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.api.scala._

    //读取数据
    val dataURL = getClass.getResource("/wc.txt")//wc.txt文件在main目录下的resources中
    val data: DataSet[String] = env.readTextFile(dataURL.getPath)

    //计算
    val result: AggregateDataSet[(String, Int)] = data.flatMap(_.split(" "))
      .map((_, 1))
      .groupBy(0)  //其中0代表元组中的下标,“0”下标代表:单词
      .sum(1)      //其中1代表元组中的下标,“1”下标代表:单词出现的次数

    //打印结果
    result.print()
  }
}

第三章 Flink 的安装和部署

  Flink的安装和部署主要分为本地(单机)模式和集群模式,其中本地模式只需直接解压就可以使用,不以修改任何参数,一般在做一些简单测试的时候使用。本地模式在我们的课程里面不再赘述。集群模式包含:

  • Standalone。
  • Flink on Yarn。
  • Mesos。
  • Docker。
  • Kubernetes。
  • AWS。
  • Goole Compute Engine。

目前在企业中使用最多的是Flink on Yarn模式。我们的课程中讲Standalone和Flink on Yarn这两种模式。

1. 集群基本架构

 Flink整个系统主要由两个组件组成,分别为JobManager和TaskManager,Flink架构也遵循Master-Slave架构设计原则,JobManager为Master节点,TaskManager为Worker(Slave)节点。所有组件之间的通信都是借助于Akka Framework,包括任务的状态以及Checkpoint触发等信息。
1) Client客户端

  客户端负责将任务提交到集群,与JobManager构建Akka连接,然后将任务提交到JobManager,通过和JobManager之间进行交互获取任务执行状态。客户端提交任务可以采用CLI方式或者通过使用Flink WebUI提交,也可以在应用程序中指定JobManager的RPC网络端口构建ExecutionEnvironment提交Flink应用。

2) JobManager

  JobManager负责整个Flink集群任务的调度以及资源的管理,从客户端中获取提交的应用,然后根据集群中TaskManager上TaskSlot的使用情况,为提交的应用分配相应的TaskSlots资源并命令TaskManger启动从客户端中获取的应用。JobManager相当于整个集群的Master节点,且整个集群中有且仅有一个活跃的JobManager,负责整个集群的任务管理和资源管理。JobManager和TaskManager之间通过Actor System进行通信,获取任务执行的情况并通过Actor System将应用的任务执行情况发送给客户端。同时在任务执行过程中,Flink JobManager会触发Checkpoints操作,每个TaskManager节点收到Checkpoint触发指令后,完成Checkpoint操作,所有的Checkpoint协调过程都是在Flink JobManager中完成。当任务完成后,Flink会将任务执行的信息反馈给客户端,并且释放掉TaskManager中的资源以供下一次提交任务使用。

3) TaskManager

  TaskManager相当于整个集群的Slave节点,负责具体的任务执行和对应任务在每个节点上的资源申请与管理。客户端通过将编写好的Flink应用编译打包,提交到JobManager,然后JobManager会根据已经注册在JobManager中TaskManager的资源情况,将任务分配给有资源的TaskManager节点,然后启动并运行任务。TaskManager从JobManager接收需要部署的任务,然后使用Slot资源启动Task,建立数据接入的网络连接,接收数据并开始数据处理。同时TaskManager之间的数据交互都是通过数据流的方式进行的。
  可以看出,Flink的任务运行其实是采用多线程的方式,这和MapReduce多JVM进程的方式有很大的区别Fink能够极大提高CPU使用效率,在多个任务和Task之间通过TaskSlot方式共享系统资源,每个TaskManager中通过管理多个TaskSlot资源池进行对资源进行有效管理。

2. Standalone集群安装和部署

  Standalone是Flink的独立部署模式,它不依赖其他平台。在使用这种模式搭建Flink集群之前,需要先规划集群机器信息。在这里为了搭建一个标准的Flink集群,这里准备3台Linux机器,如图下所示。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第9张图片
1) 解压 Flink 的压缩包2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第10张图片
2) 修改配置文件

  ① 进入到conf目录下,编辑flink-conf.yaml配置文件:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第11张图片
  其中:taskmanager.numberOfTaskSlot 参数默认值为1,修改成3。表示数每一个TaskManager上有3个Slot。

  ② 编辑conf/slaves配置文件2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第12张图片
3) 分发给另外两台服务器在这里插入图片描述
4) 启动 Flink 集群服务2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第13张图片

5) 访问 WebUI2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第14张图片
6) 通过命令提交 job 到集群

  ① 把上一章节中第一个 Flink 流处理案例代码打包,并上传2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第15张图片
  ② 执行命令: 在执行命令之前先确保 nc -lk 8888 是否启动在这里插入图片描述
其中-d选项表示提交job之后,客户端结束并退出。之后输入测试数据2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第16张图片
③ 查看job执行结果在这里插入图片描述
2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第17张图片
然后去 hadoop101 的 TaskManager 上查看最后的结果:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第18张图片
7) 通过 WebUI 提交 job 到集群2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第19张图片
注意:通过webui上传的jar包会默认放在web.tmpdir目录下,这个目录在/tmp/flink-web-UUID组成,可以在jobManager的webui中查看,每次集群重启后这个目录会被删除重建,可以修改这个目录保存之前上传的jar包。

8) 配置文件参数说明

 下面针对 flink-conf.yaml 文件中的几个重要参数进行分析:

  • jobmanager.heap.size:JobManager节点可用的内存大小。
  • taskmanager.heap.size:TaskManager节点可用的内存大小。
  • taskmanager.numberOfTaskSlots:每台机器可用的Slot数量。
  • parallelism.default:默认情况下Flink任务的并行度。

 上面参数中所说的 Slot 和 parallelism 的区别:

  • Slot是静态的概念,是指TaskManager具有的并发执行能力。
  • parallelism是动态的概念,是指程序运行时实际使用的并发能力。
  • 设置合适的parallelism能提高运算效率。

3. Flink提交到Yarn

 Flink on Yarn模式的原理是依靠YARN来调度Flink任务,目前在企业中使用较多。这种模式的好处是可以充分利用集群资源,提高集群机器的利用率,并且只需要1套Hadoop集群,就可以执行MapReduce和Spark任务,还可以执行Flink任务等,操作非常方便,不需要维护多套集群,运维方面也很轻松。Flink on Yarn模式需要依赖Hadoop集群,并且Hadoop的版本需要是2.2及以上。我们的课程里面选择的Hadoop版本是2.7.5。

 Flink On Yarn的内部实现原理:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第20张图片

  • 当启动一个新的Flink YARN Client会话时,客户端首先会检查所请求的资源(容器和内存)是否可用。之后,它会上传Flink配置和JAR文件到HDFS。
  • 客户端的下一步是请求一个YARN容器启动ApplicationMaster。JobManager和ApplicationMaster(AM)运行在同一个容器中,一旦它们成功地启动了,AM就能够知道JobManager的地址,它会为TaskManager生成一个新的Flink配置文件(这样它才能连上JobManager),该文件也同样会被上传到HDFS。另外,AM容器还提供了Flink的Web界面服务。Flink用来提供服务的端口是由用户和应用程序ID作为偏移配置的,这使得用户能够并行执行多个YARN会话。
  • 之后,AM开始为Flink的TaskManager分配容器(Container),从HDFS下载JAR文件和修改过的配置文件。一旦这些步骤完成了,Flink就安装完成并准备接受任务了。

 Flink on Yarn模式在使用的时候又可以分为两种:

  • 第1种模式(Session-Cluster):是在YARN中提前初始化一个Flink集群(称为Flink yarn-session),开辟指定的资源,以后的Flink任务都提交到这里。这个Flink集群会常驻在YARN集群中,除非手工停止(yarn application -kill id),当手动停止yarn application对应的id时,运行在当前application上的所有flink任务都会被kill。这种方式创建的Flink集群会独占资源,不管有没有Flink任务在执行,YARN上面的其他任务都无法使用这些资源。

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第21张图片

  • 第2种模式(Per-Job-Cluster):每次提交Flink任务都会创建一个新的Flink集群,每个Flink任务之间相互独立、互不影响,管理方便。任务执行完成之后创建的Flink集群也会消失,不会额外占用资源,按需使用,这使资源利用率达到最大,在工作中推荐使用这种模式。当杀掉一个当前yarn flink任务时,不会影响其他flink任务执行。

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第22张图片
注意:Flink on Yarn 还需要以下先决条件:

  • 配置Hadoop的环境变量
  • 关闭yarn的虚拟内存检查。

在每台 nodemanager 节点上 $HADOOP_HOME/etc/hadoop/yarn-site.xml中配置如下配置:

<property>
 <name>yarn.nodemanager.vmem-check-enabledname>
   <value>falsevalue>
 property>
  • 下载Flink提交到Hadoop的连接器(jar包),并把jar拷贝到Flink的lib目录下
    注意:可以将这个jar包拷贝到所有的flink节点上lib目录下,也可以只是拷贝到对应的提交任务的flink节点上。

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第23张图片

1) Session-Cluster 模式(yarn-session)

 ① 先启动Hadoop集群,然后通过命令启动一个Flink的yarn-session集群:

  bin/yarn-session.sh  -n 3 -s 3 -nm bjsxt  -d

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第24张图片
其中yarn-session.sh后面支持多个参数。下面针对一些常见的参数进行讲解:

  • -n,–container 表示分配容器的数量(也就是TaskManager的数量)。目前版本yarn-session.sh提交任务中,经过测试发现,-n参数是不起作用的。
  • -D 动态属性。
  • -d,–detached在后台独立运行。
  • -jm,–jobManagerMemory :设置JobManager的内存,单位是MB。
  • -nm,–name:在YARN上为一个自定义的应用设置一个名字。
  • -q,–query:显示YARN中可用的资源(内存、cpu核数)。
  • -qu,–queue :指定YARN队列。
  • -s,–slots :每个TaskManager使用的Slot数量。
  • -tm,–taskManagerMemory :每个TaskManager的内存,单位是MB。
  • -z,–zookeeperNamespace :针对HA模式在ZooKeeper上创建NameSpace。
  • -id,–applicationId :指定YARN集群上的任务ID,附着到一个后台独立运行的yarn session中。

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第25张图片

 ② 查看WebUI: 由于还没有提交Flink job,所以都是0。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第26张图片
这个时候注意查看本地文件系统中有一个临时文件。有了这个文件可以提交 job 到 Yarn2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第27张图片
 ③ 提交Job : 由于有了之前的配置,所以自动会提交到Yarn中。

  bin/flink run -c com.bjsxt.flink.StreamWordCount /home/Flink-Demo-1.0-SNAPSHOT.jar

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第28张图片2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第29张图片
 注意:如果删除目录/tmp/.yarn-properties-root文件,那么再按照以上命令提交任务,将会是寻找Standalone模式中的jobManager节点提交,如果想要重新提交到当前yarn-session中可以使用-yid命令指定对应的yarn application的id,命令如下:

./flink run -yid  application_1598346048136_0002  -c com.lw.scala.myflink.streaming.example.FlinkReadSocketData  /root/test/MyFlink-1.0-SNAPSHOT-jar-with-dependencies.jar

 至此第一种模式全部完成。

2) Pre-Job-Cluster 模式(yarn-cluster

 这种模式下不需要先启动yarn-session。所以我们可以把前面启动的yarn-session集群先停止,停止的命令是:

yarn application -kill application_1576832892572_0002
//其中 application_1576832892572_0002 是ID

 确保Hadoop集群是健康的情况下直接提交Job命令:

bin/flink run -m yarn-cluster -yn 3 -ys 3 -ynm bjsxt02 -c com.bjsxt.flink.StreamWordCount /home/Flink-Demo-1.0-SNAPSHOT.jar

 可以看到一个全新的yarn-session2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第30张图片
任务提交参数讲解:相对于Yarn-Session参数而言,只是前面加了y。

  • -yn,–container 表示分配容器的数量,也就是TaskManager的数量。目前版本yarn-cluster提交任务中,经过测试发现,-yn参数是不起作用的。
  • -d,–detached:设置在后台运行。
  • -yjm,–jobManagerMemory:设置JobManager的内存,单位是MB。
  • -ytm,–taskManagerMemory:设置每个TaskManager的内存,单位是MB。
  • -ynm,–name:给当前Flink application在Yarn上指定名称。
  • -yq,–query:显示yarn中可用的资源(内存、cpu核数)
  • -yqu,–queue :指定yarn资源队列
  • -ys,–slots :每个TaskManager使用的Slot数量。
  • -yz,–zookeeperNamespace:针对HA模式在Zookeeper上创建NameSpace
  • -yid,–applicationID : 指定Yarn集群上的任务ID,附着到一个后台独立运行的Yarn Session中。

4. Flink的HA

 默认情况下,每个Flink集群只有一个JobManager,这将导致单点故障(SPOF),如果这个JobManager挂了,则不能提交新的任务,并且运行中的程序也会失败。使用JobManager HA,集群可以从JobManager故障中恢复,从而避免单点故障。用户可以在Standalone或Flink on Yarn集群模式下配置Flink集群HA(高可用性)。
 Standalone模式下,JobManager的高可用性的基本思想是,任何时候都有一个Alive JobManager和多个Standby JobManager。Standby JobManager可以在Alive JobManager挂掉的情况下接管集群成为Alive JobManager,这样避免了单点故障,一旦某一个Standby JobManager接管集群,程序就可以继续运行。Standby JobManagers和Alive JobManager实例之间没有明确区别,每个JobManager都可以成为Alive或Standby。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第31张图片
1) Flink Standalone集群的HA安装和配置(目前测试1.9版本有bug)

  实现HA还需要依赖ZooKeeper和HDFS,因此要有一个ZooKeeper集群和Hadoop集群,首先启动Zookeeper集群和HDFS集群。我们的课程中分配3台JobManager,如下表:

hadoop101 hadoop102 hadoop103
JobManager JobManager JobManager
TaskManager TaskManager TaskManager

  ① 修改配置文件conf/masters2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第32张图片
  ② 修改配置文件conf/flink-conf.yaml

#要启用高可用,设置修改为zookeeper
high-availability: zookeeper
#Zookeeper的主机名和端口信息,多个参数之间用逗号隔开
high-availability.zookeeper.quorum: hadoop103:2181,hadoop101:2181,hadoop102:2181
# 建议指定HDFS的全路径。如果某个Flink节点没有配置HDFS的话,不指定HDFS的全路径则无法识到,storageDir存储了恢复一个JobManager所需的所有元数据。这里如果指定hdfs路径需要在每台节点上配置hadoop的依赖包flink-shaded-hadoop-2-uber-2.7.5-10.0.jar。
high-availability.storageDir: hdfs://mycluster/flink/ha

  ③ 把修改的配置文件拷贝其他服务器中

[root@hadoop101 ]# scp -r ./flink-xxx root@hadoop102:`pwd`
[root@hadoop101 ]# scp -r ./flink-xxx root@hadoop103:`pwd`

  ④ 启动集群在这里插入图片描述
版本问题:目前使用Flink1.7.1版本测试没有问题,使用Flink1.9版本存在HA界面不能自动跳转到对应的Alive JobManager的现象。

2) Flink On Yarn HA 安装和配置

  正常基于Yarn提交Flink程序,无论是使用yarn-session模式还是yarn-cluster模式,基于yarn运行后的application 只要kill 掉对应的Flink 集群进程“YarnSessionClusterEntrypoint”后,基于Yarn的Flink任务就失败了,不会自动进行重试,所以基于Yarn运行Flink任务,也有必要搭建HA,这里同样还是需要借助zookeeper来完成,步骤如下:

  ① 修改所有Hadoop节点的yarn-site.xml

  将所有Hadoop节点的yarn-site.xml中的提交应用程序最大尝试次数调大,这里默认是2次,也可以不调。

#在每台hadoop节点yarn-site.xml中设置提交应用程序的最大尝试次数,建议不低于4,这里重试指的是ApplicationMaster
<property>
  <name>yarn.resourcemanager.am.max-attemptsname>
  <value>4value>
property>

  ② 启动Hadoop集群
  启动zookeeper,启动Hadoop集群。
  ③ 修改Flink对应flink-conf.yaml配置
  配置对应的conf下的flink-conf.yaml,配置内容如下:

#配置依赖zookeeper模式进行HA搭建
high-availability: zookeeper
#配置JobManager原数据存储路径
high-availability.storageDir: hdfs://mycluster/flink/yarnha/
#配置zookeeper集群节点
high-availability.zookeeper.quorum: hadoop101:2181,hadoop102:2181,hadoop103:2181
#向yarn提交一个application重试的次数,也可以不设置。
yarn.application-attempts: 10

  ④ 启动yarn-session.sh 测试HA: yarn-session.sh -n 2 ,也可以直接提交Job
启动之后,可以登录yarn中对应的flink webui,如下图示:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第33张图片
  点击对应的Tracking UI,进入Flink 集群UI:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第34张图片
  查看对应的JobManager在哪台节点上启动:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第35张图片
进入对应的节点,kill掉对应的“YarnSessionClusterEntrypoint”进程。然后进入到Yarn中观察“applicationxxxx_0001”job信息:在这里插入图片描述
点击job ID,发现会有对应的重试信息:在这里插入图片描述
点击对应的“Tracking UI”进入到Flink 集群UI,查看新的JobManager节点由原来的hadoop103变成了hadoop101,说明HA起作用。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第36张图片

5. Flink并行度和Slot

  Flink中每一个worker(TaskManager)都是一个JVM进程,它可能会在独立的线程(Solt)上执行一个或多个subtask。Flink的每个TaskManager为集群提供Solt。Solt的数量通常与每个TaskManager节点的可用CPU内核数成比例,一般情况下Slot的数量就是每个节点的CPU的核数。
  Slot的数量由集群中flink-conf.yaml配置文件中设置taskmanager.numberOfTaskSlots的值为3,这个值的大小建议和节点CPU的数量保持一致。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第37张图片
一个任务的并行度设置可以从4个层面指定:

  • Operator Level(算子层面)。
  • Execution Environment Level(执行环境层面)。
  • Client Level(客户端层面)。
  • System Level(系统层面)。

这些并行度的优先级为Operator Level>Execution Environment Level>Client Level>System Level。

1) 并行度设置之 Operator Level

 Operator、Source和Sink目的地的并行度可以通过调用setParallelism()方法来指定2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第38张图片
2) 行度设置之 Execution Environment Level

  任务的默认并行度可以通过调用setParallelism()方法指定。为了以并行度3来执行所有的Operator、Source和Sink,可以通过如下方式设置执行环境的并行度2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第39张图片
3) 并行度设置之 Client Level

  并行度还可以在客户端提交Job到Flink时设定。对于CLI客户端,可以通过-p参数指定并行度。在这里插入图片描述
4) 并行度设置之 System Level

  在系统级可以通过设置flink-conf.yaml文件中的parallelism.default属性来指定所有执行环境的默认并行度。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第40张图片
5) 并行度案例分析

Flink集群中有3个TaskManager节点,每个TaskManager的Slot数量为32021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第41张图片2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第42张图片

第四章 Flink 常用 API 详解

  Flink 根据抽象程度分层,提供了三种不同的 API和库。每一种 API 在简洁性和表达力上有着不同的侧重,并且针对不同的应用场景。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第43张图片

  • ProcessFunction 是 Flink 所提供最底层接口。ProcessFunction 可以处理一或两条输入数据流中的单个事件或者归入一个特定窗口内的多个事件。它提供了对于时间和状态的细粒度控制。开发者可以在其中任意地修改状态,也能够注册定时器用以在未来的某一时刻触发回调函数。因此,你可以利用 ProcessFunction 实现许多有状态的事件驱动应用所需要的基于单个事件的复杂业务逻辑。
  • DataStream API 为许多通用的流处理操作提供了处理原语。这些操作包括窗口、逐条记录的转换操作,在处理事件时进行外部数据库查询等。DataStream API 支持 Java 和 Scala 语言,预先定义了例如map()、reduce()、aggregate() 等函数。你可以通过扩展实现预定义接口或使用 Java、Scala 的 lambda 表达式实现自定义的函数。
  • SQL & Table API:Flink 支持两种关系型的 API,Table API 和 SQL。这两个 API 都是批处理和流处理统一的 API,这意味着在无边界的实时数据流和有边界的历史记录数据流上,关系型 API 会以相同的语义执行查询,并产生相同的结果。Table API 和 SQL 借助了 Apache Calcite 来进行查询的解析,校验以及优化。它们可以与 DataStream 和 DataSet API 无缝集成,并支持用户自定义的标量函数,聚合函数以及表值函数。
    另外Flink 具有数个适用于常见数据处理应用场景的扩展库。
  • 复杂事件处理(CEP):模式检测是事件流处理中的一个非常常见的用例。Flink 的 CEP 库提供了 API,使用户能够以例如正则表达式或状态机的方式指定事件模式。CEP 库与 Flink 的 DataStream API 集成,以便在 DataStream 上评估模式。CEP 库的应用包括网络入侵检测,业务流程监控和欺诈检测。
  • DataSet API:DataSet API 是 Flink 用于批处理应用程序的核心 API。DataSet API 所提供的基础算子包括map、reduce、(outer) join、co-group、iterate等。所有算子都有相应的算法和数据结构支持,对内存中的序列化数据进行操作。如果数据大小超过预留内存,则过量数据将存储到磁盘。Flink 的 DataSet API 的数据处理算法借鉴了传统数据库算法的实现,例如混合散列连接(hybrid hash-join)和外部归并排序(external merge-sort)。
  • Gelly: Gelly 是一个可扩展的图形处理和分析库。Gelly 是在 DataSet API 之上实现的,并与 DataSet API 集成。因此,它能够受益于其可扩展且健壮的操作符。Gelly 提供了内置算法,如 label propagation、triangle enumeration 和 page rank 算法,也提供了一个简化自定义图算法实现的 Graph API。

1. DataStream 的编程模型

DataStream 的编程模型包括四个部分:Environment,DataSource,Transformation,Sink。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第44张图片

2. Flink 的 DataSource 数据源

  1) 基于文件的 Source

  读取本地文件系统的数据,前面的案例已经讲过了。本课程主要讲基于HDFS文件系统的Source。首先需要配置Hadoop的依赖

>
    >org.apache.hadoop>
    >hadoop-client>
    >2.7.5>
>

 读取HDFS上的文件:

object FileSource {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv = StreamExecutionEnvironment.getExecutionEnvironment

    streamEnv.setParallelism(1)

    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._

    //读取数据

    val stream = streamEnv.readTextFile("hdfs://mycluster/wc.txt")

    //转换计算
    val result: DataStream[(String, Int)] = stream.flatMap(_.split(","))
      .map((_, 1))
      .keyBy(0)
      .sum(1)

    //打印结果到控制台
    result.print()

    //启动流式处理,如果没有该行代码上面的程序不会运行
    streamEnv.execute("wordcount")
  }
}

2) 基于集合的 Source

/**
 * 通信基站日志数据
 *
 * @param sid 基站ID
 * @param callOut 主叫号码
 * @param callIn 被叫号码
 * @param callType 通话类型eg:呼叫失败(fail),占线(busy),拒接(barring),接通(success):
 * @param callTime 呼叫时间戳,精确到毫秒
 * @Param duration 通话时长 单位:秒
 */
case class StationLog(sid:String,callOut:String,callIn:String,callType:String,callTime:Long,duration:Long)

object CollectionSource {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv = StreamExecutionEnvironment.getExecutionEnvironment

    streamEnv.setParallelism(1)

    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._

    //读取数据
    var dataStream =streamEnv.fromCollection(Array(
      new StationLog("001","186","189","busy",1577071519462L,0),
      new StationLog("002","186","188","busy",1577071520462L,0),
      new StationLog("003","183","188","busy",1577071521462L,0),
      new StationLog("004","186","188","success",1577071522462L,32)
    ))

    dataStream.print()

    streamEnv.execute()
  }
}

3) 基于 KafkaSource
  首先需要配置Kafka连接器的依赖,另外更多的连接器可以查看官网:https://ci.apache.org/projects/flink/flink-docs-release-1.9/zh/dev/connectors/

>
    >org.apache.flink>
    >flink-connector-kafka-版本_2.11>
    >1.9.1>
>

  ① 第一种:读取Kafka中的普通数据(String)

object KafkaSourceWithoutKey {
     
  def main(args: Array[String]): Unit = {
     
    import org.apache.flink.streaming.api.scala._
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    //组织配置项
    val props = new Properties()
  props.setProperty("bootstrap.servers","mynode1:9092,mynode2:9092,mynode3:9092")
    props.setProperty("key.deserializer",classOf[StringDeserializer].getName)
    props.setProperty("value.deserializer",classOf[StringDeserializer].getName)
    props.setProperty("group.id","ft1_group")
//    props.setProperty("auto.offset.reset","latest")//也可以不设置,默认是 flinkKafkaConsumer.setStartFromGroupOffsets(),设置了也不会起作用
    //读取Kafka中的数据
    val lines: DataStream[String] = env.addSource(new FlinkKafkaConsumer[String]("ft1",new SimpleStringSchema(),props))
    lines.print()

    //触发执行
    env.execute()
  }
}

  ② 第二种:读取Kafka中的KeyValue数据

object KafkaSourceWithKey {
     
  def main(args: Array[String]): Unit = {
     
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    val props = new Properties()
    props.setProperty("bootstrap.servers","mynode1:9092,mynode2:9092,mynode3:9092")
    props.setProperty("key.serializer",classOf[StringDeserializer].getName)
    props.setProperty("value.serializer",classOf[StringDeserializer].getName)
    props.setProperty("group.id","ft2_group")
//    props.setProperty("auto.offset.reset","latest") 设置不设置无所谓,因为可以对FlinkKafkaConsumer设置 从什么位置读取数据

   val flinkKafkaConsumer =  new FlinkKafkaConsumer[(Int, String)]("ft2", new KafkaDeserializationSchema[(Int, String)] {
     
      override def isEndOfStream(t: (Int, String)): Boolean = false //是否流结束

      override def deserialize(consumerRecord: ConsumerRecord[Array[Byte], Array[Byte]]): (Int, String) = {
     
        var key = "0"
        var value = "null"
        if(consumerRecord.key() != null){
     
          key = new String(consumerRecord.key(), "UTF-8")
        }
        if(consumerRecord.value() != null){
     
          value = new String(consumerRecord.value, "UTF-8")
        }
        (key.toInt, value)
      }
     
      //设置返回的二元组类型 ,createTuple2TypeInformation 需要导入隐式转换
      override def getProducedType: TypeInformation[(Int, String)] = {
     
        createTuple2TypeInformation(createTypeInformation[Int], createTypeInformation[String])
      }
    }, props)

    //设置读取Kafka中的数据从最后开始,默认设置为 setStartFromGroupOffsets
    val infos: DataStream[(Int, String)] = env.addSource(flinkKafkaConsumer.setStartFromLatest())

    //打印结果
    infos.print()

    //触发执行
    env.execute()

  }
}

注意:

  • 默认在kafka consoler中向topic中写入数据时,数据是没有key的,如果想要在console中写入的数据有key,可以使用下面命令:
./kafka-console-producer.sh --broker-list mynode1:9092,mynode2:9092,mynode3:9092 --topic ft2 --property  parse.key=true --property key.separator='|'
parse.key=true: 生产数据带有key
key.separator=’|’:指定分隔符,默认分隔符是\t

console中消费数据也可以打印出key,命令如下:

./kafka-console-consumer.sh --bootstrap-server mynode1:9092,mynode2:9092,mynode3:9092 --topic ft2 --property print.key=true
print.key=true:打印key
  • Flink读取Kafka数据确定开始位置有以下几种设置方式:
    flinkKafkaConsumer.setStartFromEarliest()
    从topic的最早offset位置开始处理数据,如果kafka中保存有消费者组的消费位置将被忽略。
    flinkKafkaConsumer.setStartFromLatest()
    从topic的最新offset位置开始处理数据,如果kafka中保存有消费者组的消费位置将被忽略。
    flinkKafkaConsumer.setStartFromTimestamp(…)
    从指定的时间戳(毫秒)开始消费数据,Kafka中每个分区中数据大于等于设置的时间戳的数据位置将被当做开始消费的位置。如果kafka中保存有消费者组的消费位置将被忽略。
    flinkKafkaConsumer.setStartFromGroupOffsets()
    默认的设置。根据代码中设置的group.id设置的消费者组,去kafka中或者zookeeper中找到对应的消费者offset位置消费数据。如果没有找到对应的消费者组的位置,那么将按照auto.offset.reset设置的策略读取offset。
  • 关于Flink消费Kafka中数据offset问题
     Flink提供了消费kafka数据的offset如何提交给Kafka或者zookeeper(kafka0.8之前)的配置。注意,Flink并不依赖提交给Kafka或者zookeeper中的offset来保证容错。提交的offset只是为了外部来查询监视kafka数据消费的情况。
     配置offset的提交方式取决于是否为job设置开启checkpoint。可以使用env.enableCheckpointing(5000)来设置开启checkpoint。
      关闭checkpoint:
      如果禁用了checkpoint,那么offset位置的提交取决于Flink读取kafka客户端的配置,enable.auto.commit ( auto.commit.enable【Kafka 0.8】)配置是否开启自动提交offset, auto.commit.interval.ms决定自动提交offset的周期。
      开启checkpoint:
      如果开启了checkpoint,那么当checkpoint保存状态完成后,将checkpoint中保存的offset位置提交到kafka。这样保证了Kafka中保存的offset和checkpoint中保存的offset一致,可以通过配置setCommitOffsetsOnCheckpoints(boolean)来配置是否将checkpoint中的offset提交到kafka中(默认是true)。如果使用这种方式,那么properties中配置的kafka offset自动提交参数enable.auto.commit和周期提交参数auto.commit.interval.ms参数将被忽略。

4) 自定义 Source

 当然也可以自定义数据源,有两种方式实现:

  • 通过实现SourceFunction接口来自定义无并行度(也就是并行度只能为1)的Source。
  • 通过实现ParallelSourceFunction 接口或者继承RichParallelSourceFunction 来自定义有并行度的数据源。

 实现SourceFunction 接口实现无并行度的自定义Source:

class MyDefinedSource extends SourceFunction[StationLog]{
     
  //是否生成数据的标志
  var flag = true

  /**
    * 主要方法:启动一个Source,大部分情况下都需要在run方法中实现一个循环产生数据
    * 这里计划每次产生10条基站数据
    */
  override def run(ctx: SourceFunction.SourceContext[StationLog]): Unit = {
     
    val random = new Random()
    val callType = Array[String]("fail","success","busy","barring")
    while(flag){
     
      1.to(10).map(i=>{
     
        StationLog("sid_"+random.nextInt(10),"1811234%04d".format(random.nextInt(8)),
          "1915678%04d".format(random.nextInt(8)),callType(random.nextInt(4)),System.currentTimeMillis(),random.nextInt(50))
      }).foreach(sl =>{
     
        ctx.collect(sl)
      })
      //每次生成10条数据就休息 5s
      Thread.sleep(5000)
    }

  }

  //当取消对应的Flink任务时被调用
  override def cancel(): Unit = flag = false
}

object DefinedNoParalleSource {
     
  def main(args: Array[String]): Unit = {
     
    import org.apache.flink.streaming.api.scala._
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val stationLogInfos: DataStream[StationLog] = env.addSource(new MyDefinedSource())
    stationLogInfos.print()
    env.execute()
  }
}

 实现 ParallelSourceFunction 接口实现有并行度的自定义Source:

class MyDefinedParallelSouce extends ParallelSourceFunction[StationLog]{
     
  //是否生成数据的标志
  var flag = true

  /**
    * 主要方法:启动一个Source,大部分情况下都需要在run方法中实现一个循环产生数据
    * 这里计划每次产生10条基站数据
    */
  override def run(ctx: SourceFunction.SourceContext[StationLog]): Unit = {
     
    val random = new Random()
    val callType = Array[String]("fail","success","busy","barring")
    while(flag){
     
      1.to(10).map(i=>{
     
        StationLog("sid_"+random.nextInt(10)+"_"+Thread.currentThread().getName,"181%04d".format(random.nextInt(8)),
          "191%04d".format(random.nextInt(8)),callType(random.nextInt(4)),System.currentTimeMillis(),random.nextInt(50))
      }).foreach(sl =>{
     
        ctx.collect(sl)
      })
      //每次生成10条数据就休息 5s
      Thread.sleep(5000)
    }

  }

  //当取消对应的Flink任务时被调用
  override def cancel(): Unit = flag = false
}

object DefinedParalleSource {
     
  def main(args: Array[String]): Unit = {
     
    import org.apache.flink.streaming.api.scala._
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val stationInfos: DataStream[StationLog] = env.addSource(new MyDefinedParallelSouce())
    stationInfos.print()

    env.execute()
  }
}

3. Flink 的 Sink 数据目标

  Flink 针对 DataStream 提供了大量的已经实现的数据目标(Sink),包括文件、Kafka、Redis、HDFS、Elasticsearch等等。

1) 基于 HDFSSink

  首先配置支持Hadoop FileSystem的连接器依赖。

>
    >org.apache.flink>
    >flink-connector-filesystem_2.11>
    >1.9.1>
>

  Streaming File Sink能把数据写入HDFS中,还可以支持分桶写入,每一个分桶就对应HDFS中的一个目录。默认按照小时来分桶,在一个桶内部,会进一步将输出基于滚动策略切分成更小的文件。这有助于防止桶文件变得过大。滚动策略也是可以配置的,默认 策略会根据文件大小和超时时间来滚动文件,超时时间是指没有新数据写入部分文件(part file)的时间。

  将Flink 处理后的数据写入HDFS目录中,需注意:

  • Flink数据写入HDFS中会以 “yyyy-MM-dd–HH” 的时间格式给每个目录命名,每个目录也叫一个桶。默认每小时产生一个桶,目录下包含了一些文件, 每个 sink 的并发实例都会创建一个属于自己的部分文件,当这些文件太大的时候,sink 会根据设置产生新的部分文件。当一个桶不再活跃时,打开的部分文件会刷盘并且关闭(即:将sink数据写入磁盘,并关闭文件),当再次写入数据会创建新的文件。
  • 生成新的桶目录检查周期是 withBucketCheckInterval(1000) 默认是一分钟。
  • 在桶内生成新的文件规则:
      withInactivityInterval : 桶不活跃的间隔时长,如果一个桶最近一段时间都没有写入,那么这个桶被认为是不活跃的,sink 默认会每分钟检查不活跃的桶、关闭那些超过一分钟没有数据写入的桶。【即:桶内的当下文件如果一分钟没有写入数据就会自动关闭,再次写入数据时,生成新的文件】
      withMaxPartSize :设置文件多大后生成新的文件,默认128M。
      withRolloverInterval :每隔多长时间生成一个新的文件,默认1分钟
      上述三个条件满足任意一个条件都会生成新的文件。
object HDFSSink {
     
  def main(args: Array[String]): Unit = {
     
    import org.apache.flink.streaming.api.scala._
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val infos: DataStream[StationLog] = env.addSource(new MyDefinedSource())

    /**
      *  设置 文件滚动规则
      *   withInactivityInterval :桶不活跃的间隔时长,如果一个桶最近一段时间都没有写入,那么这个桶被认为是不活跃的,sink 默认会每分钟检查不活跃的桶、关闭那些超过一分钟没有数据写入的桶。
      *   withMaxPartSize : 设置文件多大后生成新的文件,默认128M。
      *   withRolloverInterval :每隔多长时间生成一个新的文件,默认1分钟
      */
    val policy: DefaultRollingPolicy[StationLog, String] = DefaultRollingPolicy.create()
      .withInactivityInterval(5000) //桶不活跃的间隔时长
      .withMaxPartSize(1024*1024*128) //设置128M 生成一个文件,默认128M
      .withRolloverInterval(1000*60) //每隔多长时间生成一个文件,默认1分钟
      .build[StationLog, String]()

    //创建一个HDFS sink ,注意,这里需要一直后面 .追加方法,不然设置的参数没有设置到对应的hdfsSink中
    val hdfsSink = StreamingFileSink.forRowFormat[StationLog](
      new Path("hdfs://mycluster/flinkresult"),
      new SimpleStringEncoder[StationLog]("UTF-8"))
      .withBucketCheckInterval(1000)//检查分桶的间隔时间,默认每分钟检查桶(目录),是否生成新的桶目录,默认是每小时生成一个桶。
      .withRollingPolicy(policy) //设置文件滚动策略
      .build()

    //将DataStream写入HDFS Sink
    infos.addSink(hdfsSink)
    env.execute()

  }
}

2) 基于 RedisSink

  Flink除了内置的连接器外,还有一些额外的连接器通过 Apache Bahir 发布,包括:

  • Apache ActiveMQ (source/sink)
  • Apache Flume (sink)
  • Redis (sink)
  • Akka (sink)
  • Netty (source)

  这里我用Redis来举例,首先需要配置Redis连接器的依赖:

>
    >org.apache.bahir>
    >flink-connector-redis_2.11>
    >1.0>
>

接下来我们可以把 WordCount 的结果写入 Redis 中:

 object RedisSink {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv= StreamExecutionEnvironment.getExecutionEnvironment
    streamEnv.setParallelism(1)
    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._
    //读取数据
    val stream = streamEnv.socketTextStream("hadoop101",8888)

    //转换计算
    val result = stream.flatMap(_.split(","))
      .map((_, 1))
      .keyBy(0)
      .sum(1)

    //连接redis的配置
    val config = new  FlinkJedisPoolConfig.Builder().setDatabase(1).setHost("hadoop101").setPort(6379).build()
    
    //写入redis
    result.addSink(new RedisSink[(String, Int)](config,new RedisMapper[(String, Int)] {
     
      override def getCommandDescription = new RedisCommandDescription(RedisCommand.HSET,"t_wc") //t_wc是表名

      override def getKeyFromData(data: (String, Int)) = {
     
        data._1 //单词
      }

      override def getValueFromData(data: (String, Int)) = {
     
        data._2+"" //单词出现的次数
      }
    }))
    
    streamEnv.execute()
  }
}

3) 基于 KafkaSink

  由于前面有的课程已经讲过Flink的Kafka连接器,所以还是一样需要配置Kafka连接器的依赖配置,接下我们还是把WordCout的结果写入Kafka:

 object KafkaSink {
     //新版本的代码

  def main(args: Array[String]): Unit = {
     
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    streamEnv.setParallelism(1) //默认情况下每个任务的并行度为1

    import org.apache.flink.streaming.api.scala._
    //读取netcat流中数据 (实时流)
    val stream1: DataStream[String] = streamEnv.socketTextStream("hadoop101",8888)


    //转换计算
    val result = stream1.flatMap(_.split(","))
      .map((_, 1))
      .keyBy(0)
      .sum(1)

    //Kafka生产者的配置
    val props = new Properties()
    props.setProperty("bootstrap.servers","hadoop101:9092,hadoop102:9092,hadoop103:9092")
    props.setProperty("key.serializer",classOf[ByteArraySerializer].getName)
    props.setProperty("value.serializer",classOf[ByteArraySerializer].getName)

    //数据写入Kafka,并且是KeyValue格式的数据
    result.addSink(new FlinkKafkaProducer[(String, Int)]("t_topic",new KafkaSerializationSchema[(String,Int)]{
     
      override def serialize(element: (String, Int), timestamp: lang.Long) = {
     
        new ProducerRecord("t_topic",element._1.getBytes,(element._2+"").getBytes())
      }
    },props,FlinkKafkaProducer.Semantic.EXACTLY_ONCE)) //EXACTLY_ONCE 精确一次

    streamEnv.execute()
  }
}

4) 自定义的 Sink

  当然你可以自己定义Sink,有两种实现方式:1、实现SinkFunction接口。2、实现RichSinkFunction类。后者增加了生命周期的管理功能。比如需要在Sink初始化的时候创建连接对象,则最好使用第二种。
  案例需求:把StationLog对象写入Mysql数据库中。

>
        >mysql>
        >mysql-connector-java>
        >5.1.47>
>
//从自定义的Source中读取StationLog数据,通过Flink写入Mysql数据库
object CustomJdbcSink {
     

  //自定义一个Sink写入Mysql
  class MyCustomSink extends RichSinkFunction[StationLog]{
     
    var conn:Connection =_
    var pst :PreparedStatement =_
    //生命周期管理,在Sink初始化的时候调用
    override def open(parameters: Configuration): Unit = {
     
      conn=DriverManager.getConnection("jdbc:mysql://localhost/test","root","123123")
      pst=conn. ("insert into t_station_log (sid,call_out,call_in,call_type,call_time,duration) values (?,?,?,?,?,?)")
    }

    //把StationLog 写入到表t_station_log,循环调用方法,输出一条数据调用一次
    override def invoke(value: StationLog, context: SinkFunction.Context[_]): Unit = {
     
      pst.setString(1,value.sid)
      pst.setString(2,value.callOut)
      pst.setString(3,value.callIn)
      pst.setString(4,value.callType)
      pst.setLong(5,value.callTime)
      pst.setLong(6,value.duration)
      pst.executeUpdate()

    }

    override def close(): Unit = {
     
      pst.close()
      conn.close()
    }
  }

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv = StreamExecutionEnvironment.getExecutionEnvironment
    streamEnv.setParallelism(1)

    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._

    val data: DataStream[StationLog] = streamEnv.addSource(new MyCustomerSource)

    //数据写入msyql
    data.addSink(new MyCustomSink)

    streamEnv.execute()
  }
}

4. DataStream 转换算子

  即通过从一个或多个DataStream生成新的DataStream的过程被称为Transformation操作。在转换过程中,每种操作类型被定义为不同的Operator,Flink程序能够将多个Transformation组成一个DataFlow的拓扑。

1) Map [DataStream->DataStream]

  调用用户定义的MapFunction对DataStream[T]数据进行处理,形成新的Data-Stream[T],其中数据格式可能会发生变化,常用作对数据集内数据的清洗和转换。例如将输入数据集中的每个数值全部加1处理,并且将数据输出到下游数据集。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第45张图片
2) FlatMap [DataStream->DataStream]

  该算子主要应用处理输入一个元素产生一个或者多个元素的计算场景,比较常见的是在经典例子WordCount中,将每一行的文本数据切割,生成单词序列如在图所示,对于输入DataStream[String]通过FlatMap函数进行处理,字符串数字按逗号切割,然后形成新的整数数据集。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第46张图片

 val resultStream[String] = dataStream.flatMap {
      str => str.split(" ") }

3) Filter [DataStream->DataStream]

  该算子将按照条件对输入数据集进行筛选操作,将符合条件的数据集输出,将不符合条件的数据过滤掉。如下图所示将输入数据集中偶数过滤出来,奇数从数据集中去除。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第47张图片

//通过通配符
val filter:DataStream[Int] = dataStream.filter {
      _ % 2 == 0 }
//或者指定运算表达式
val filter:DataStream[Int] = dataStream.filter {
      x => x % 2 == 0 }

4) KeyBy [DataStream->KeyedStream]

  Flink中不是K,V格式的编程模型,当遇到按照某个key进行分组情况时,需要根据keyBy来指定虚拟的key。该算子根据指定的Key将输入的DataStream[T]数据格式转换为KeyedStream[T],也就是在数据集中执行Partition操作,将相同的Key值的数据放置在相同的分区中。如下图所示,将白色方块和灰色方块通过颜色的Key值重新分区,将数据集分为具有灰色方块的数据集合。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第48张图片
  将数据集中第一个参数作为Key,对数据集进行KeyBy函数操作,形成根据id分区的KeyedStream数据集。其中keyBy方法输入为DataStream[T]数据集。
  注意:在DataSet Flink的批处理数据中,对数据进行分组使用group By。

 val dataStream = env.fromElements((1, 5),(2, 2),(2, 4),(1, 3))
//指定第一个字段为分区Key
val keyedStream: KeyedStream[(String,Int), Tuple] = dataStream.keyBy(0)

5) Reduce [KeyedStream->DataStream]

  该算子和MapReduce中Reduce原理基本一致,主要目的是将输入的KeyedStream通过传入的用户自定义的ReduceFunction滚动地进行数据聚合处理,其中定义的ReduceFunciton必须满足运算结合律和交换律。
  案例:对传入keyedStream数据集中相同的key值的数据独立进行求和运算,得到每个key所对应的求和值。

val dataStream = env.fromElements(("a", 3), ("d", 4), ("c", 2), ("c",5), ("a", 5))
  //指定第一个字段为分区Key
val keyedStream: KeyedStream[(String,Int), Tuple] = dataStream.keyBy(0)
/滚动对第二个字段进行reduce相加求和
val reduceStream = keyedStream.reduce {
      (t1, t2) =>
    (t1._1, t1._2 + t2._2)
}

6) Aggregations[KeyedStream->DataStream]

  Aggregations是KeyedDataStream接口提供的聚合算子,根据指定的字段进行聚合操作,滚动地产生一系列数据聚合结果。其实是将Reduce算子中的函数进行了封装,封装的聚合操作有sum、min、minBy、max、maxBy等,这样就不需要用户自己定义Reduce函数。
  如下代码所示,指定数据集中第一个字段作为key,用第二个字段作为累加字段,然后滚动地对第二个字段的数值进行累加并输出。

//指定第一个字段为分区Key
val keyedStream: KeyedStream[(Int, Int), Tuple] = dataStream.keyBy(0)
//对第二个字段进行sum统计
val sumStream: DataStream[(Int, Int)] = keyedStream.sum(1)
//输出计算结果
sumStream.print()

7) Union[DataStream ->DataStream]

  Union算子主要是将两个或者多个输入的数据集合并成一个数据集,需要保证两个数据集的格式一致,输出的数据集的格式和输入的数据集格式保持一致,如图所示,将灰色方块数据集和黑色方块数据集合并成一个大的数据集。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第49张图片

//创建不同的数据集
val dataStream1: DataStream[(String, Int)] = env.fromElements(("a", 3), ("d", 4), ("c", 2), ("c", 5), ("a", 5))
val dataStream2: DataStream[(String, Int)] = env.fromElements(("d", 1), ("s", 2), ("a", 4), ("e", 5), ("a", 6))
val dataStream3: DataStream[(String, Int)] = env.fromElements(("a", 2), ("d", 1), ("s", 2), ("c", 3), ("b", 1))
//合并两个DataStream数据集
val unionStream = dataStream1.union(dataStream_02)
//合并多个DataStream数据集
val allUnionStream = dataStream1.union(dataStream2, dataStream3)

8) Connect,CoMap,CoFlatMap[DataStream ->ConnectedStream->DataStream]

  Connect算子主要是为了合并两种或者多种不同数据类型的数据集,合并后会保留原来数据集的数据类型。例如:dataStream1数据集为(String, Int)元祖类型,dataStream2数据集为Int类型,通过connect连接算子将两个不同数据类型的流结合在一起,形成格式为ConnectedStreams的数据集,其内部数据为[(String, Int), Int]的混合数据类型,保留了两个原始数据集的数据类型。

//创建不同数据类型的数据集
val dataStream1: DataStream[(String, Int)] = env.fromElements(("a", 3), ("d", 4), ("c", 2), ("c", 5), ("a", 5))
val dataStream2: DataStream[Int] = env.fromElements(1, 2, 4, 5, 6)
//连接两个DataStream数据集
val connectedStream: ConnectedStreams[(String, Int), Int] = dataStream1.connect(dataStream2)

  需要注意的是,对于ConnectedStreams类型的数据集不能直接进行类似Print()的操作,需要再转换成DataStream类型数据集,在Flink中ConnectedStreams提供的map()方法和flatMap()

object ConnectTransformation {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv = StreamExecutionEnvironment.getExecutionEnvironment
    streamEnv.setParallelism(1)

    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._
    //创建不同数据类型的数据集
    val dataStream1: DataStream[(String, Int)] = streamEnv.fromElements(("a", 3), ("d", 4), ("c", 2), ("c", 5), ("a", 5))
    val dataStream2: DataStream[Int] = streamEnv.fromElements(1, 2, 4, 5, 6)
    //连接两个DataStream数据集
    val connectedStream: ConnectedStreams[(String, Int), Int] = dataStream1.connect(dataStream2)

   //coMap函数处理
    val result: DataStream[(Any, Int)] = connectedStream.map(
      //第一个处理函数
      t1 => {
     
        (t1._1, t1._2)
      },
      //第二个处理函数
      t2 => {
     
        (t2, 0)
      }
    )
    result.print()
    streamEnv.execute()
  }
}

  注意 Union 和 Connect 的区别:

  • Union之前两个流的类型必须是一样,Connect可以不一样,在之后的coMap中再去调整成为一样的。
  • Connect只能操作两个流,Union可以操作多个。

9) Splitselect [DataStream->SplitStream->DataStream]

  Split算子是将一个DataStream数据集按照条件进行拆分,形成两个数据集的过程,也是union算子的逆向实现。每个接入的数据都会被路由到一个或者多个输出数据集中。如下图所示,将输入数据集根据颜色切分成两个数据集。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第50张图片
  在使用split函数中,需要定义split函数中的切分逻辑,通过调用split函数,然后指定条件判断函数,如下面的代码所示:将根据第二个字段的奇偶性将数据集标记出来,如果是偶数则标记为even,如果是奇数则标记为odd,然后通过集合将标记返回,最终生成格式SplitStream的数据集。

//创建数据集
val dataStream1: DataStream[(String, Int)] = env.fromElements(("a", 3), ("d", 4), ("c", 2), ("c", 5), ("a", 5))
//合并两个DataStream数据集
val splitedStream: SplitStream[(String, Int)] = dataStream1.split(t => if (t._2 % 2 == 0) Seq("even") else Seq("odd"))

  split函数本身只是对输入数据集进行标记,并没有将数据集真正的实现切分,因此需要借助Select函数根据标记将数据切分成不同的数据集。如下代码所示,通过调用SplitStream数据集的select()方法,传入前面已经标记好的标签信息,然后将符合条件的数据筛选出来,形成新的数据集。

//筛选出偶数数据集
val evenStream: DataStream[(String, Int)] = splitedStream.select("even")
//筛选出奇数数据集
val oddStream: DataStream[(String, Int)] = splitedStream.select("odd")
//筛选出奇数和偶数数据集
val allStream: DataStream[(String, Int)] = splitedStream.select("even", "odd")

5. 函数类和富函数类

  前面学过的所有算子几乎都可以自定义一个函数类、富函数类作为参数。因为Flink暴露了者两种函数类的接口,常见的函数接口有:

  • MapFunction
  • FlatMapFunction
  • ReduceFunction
  • 。。。。。

  富函数接口它其他常规函数接口的不同在于:可以获取运行环境的上下文,在上下文环境中可以管理状态(State在下一章节中提到),并拥有一些生命周期方法,所以可以实现更复杂的功能。富函数的接口有:

  • RichMapFunction
  • RichFlatMapFunction
  • RichFilterFunction
  • 。。。。。

 1)普通函数类举例:按照指定的时间格式输出每个通话的拨号时间和结束时间。数据如下:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第51张图片

//按照指定的时间格式输出每个通话的拨号时间和结束时间
object FunctionClassTransformation {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    streamEnv.setParallelism(1)
    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._
    //读取文件数据
    val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })
    //定义时间输出格式
    val format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    //过滤那些通话成功的
    data.filter(_.callType.equals("success"))
      .map(new CallMapFunction(format))
      .print()

    streamEnv.execute()
  }

  //自定义的函数类
  class CallMapFunction(format: SimpleDateFormat) extends MapFunction[StationLog,String]{
     
    override def map(t: StationLog): String = {
     
      var strartTime=t.callTime;
      var endTime =t.callTime + t.duration*1000
      "主叫号码:"+t.callOut +",被叫号码:"+t.callIn+",呼叫起始时间:"+format.format(new Date(strartTime))+",呼叫结束时间:"+format.format(new Date(endTime))
    }
  }
}

运行结果:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第52张图片
 2)富函数类举例:把呼叫成功的通话信息转化成真实的用户姓名,通话用户对应的用户表(在Mysql数据中)为:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第53张图片
  由于需要从数据库中查询数据,就需要创建连接,创建连接的代码必须写在生命周期的open方法中。所以需要使用富函数类。
  Rich Function有一个生命周期的概念。典型的生命周期方法有:

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

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    streamEnv.setParallelism(1)
    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._
    //读取文件数据
    val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })
    //过滤那些通话成功的
    data.filter(_.callType.equals("success"))
      .map(new CallRichMapFunction())
      .print()

    streamEnv.execute()
  }
  //自定义的富函数类
  class CallRichMapFunction() extends RichMapFunction[StationLog,StationLog]{
     
    var conn:Connection =_
    var pst :PreparedStatement =_
    //生命周期管理,初始化的时候创建数据连接
    override def open(parameters: Configuration): Unit = {
     
      conn=DriverManager.getConnection("jdbc:mysql://localhost/test","root","123123")
      pst=conn.prepareStatement("select name from t_phone where phone_number=?")
    }

    override def map(in: StationLog): StationLog = {
     
      //查询主叫用户的名字
      pst.setString(1,in.callOut)
      val set1: ResultSet = pst.executeQuery()
      if(set1.next()){
     
        in.callOut=set1.getString(1)
      }
      //查询被叫用户的名字
      pst.setString(1,in.callIn)
      val set2: ResultSet = pst.executeQuery()
      if(set2.next()){
     
        in.callIn=set2.getString(1)
      }
      in
    }
    //关闭连接
    override def close(): Unit = {
     
      pst.close()
      conn.close()
    }
  }
}

6. 底层 ProcessFunctionAPI

  ProcessFunction是一个低层次的流处理操作,允许返回所有Stream的基础构建模块:

  • 访问Event本身数据(比如:Event的时间,Event的当前Key等)
  • 管理每个key的状态State(仅在Keyed Stream中)
  • 管理定时器Timer(包括:注册定时器,删除定时器等)

  总而言之,ProcessFunction是Flink最底层的API,也是功能最强大的。
  例如:监控每一个手机,如果在5秒内呼叫它的通话都是失败的,发出警告信息。注意:这个案例中会用到状态编程,请同学们只要知道状态的意思,不需要掌握。后面的章节中会详细讲解State编程。

/**
 * 监控每一个手机号,如果在5秒内呼叫它的通话都是失败的,发出警告信息
 * 在5秒中内只要有一个呼叫不是fail则不用警告
 */
object TestProcessFunction {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    streamEnv.setParallelism(1)
    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._
    //读取文件数据
    val data = streamEnv.socketTextStream("hadoop101",8888)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })

    //处理数据
    data.keyBy(_.callOut)
      .process(new MonitorCallFail())
      .print()

    streamEnv.execute()
  }
  //监控逻辑
  class MonitorCallFail() extends KeyedProcessFunction[String,StationLog,String]{
     
    //使用一个状态记录时间
    lazy val timeState :ValueState[Long] =getRuntimeContext.getState(new ValueStateDescriptor[Long]("time",classOf[Long]))

    override def processElement(value: StationLog, ctx: KeyedProcessFunction[String, StationLog, String]#Context, out: Collector[String]): Unit = {
     
      //从状态中取得时间
      var time =timeState.value()
      if(value.callType.equals("fail")&& time==0){
      //表示第一次发现呼叫当前手机号是失败的
        //获取当前时间,并注册定时器
        var nowTime=ctx.timerService().currentProcessingTime()
        var onTime=nowTime + 5000L //5秒后触发
        ctx.timerService().registerProcessingTimeTimer(onTime)
        timeState.update(onTime)
      }
      if(!value.callType.equals("fail") &&  time!=0){
     //表示有呼叫成功了,可以取消触发器
        ctx.timerService().deleteProcessingTimeTimer(time)
        timeState.clear()
      }
    }

    //时间到了,执行触发器,发出告警
    override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, StationLog, String]#OnTimerContext, out: Collector[String]): Unit = {
     
      var warnStr="触发时间:"+timestamp+" 手机号:"+ctx.getCurrentKey
      out.collect(warnStr)
      timeState.clear()
    }
  }
}

7. 侧输出流 Side Output

  在flink处理数据流时,我们经常会遇到这样的情况:在处理一个数据源时,往往需要将该源中的不同类型的数据做分割处理,如果使用 filter算子对数据源进行筛选分割的话,势必会造成数据流的多次复制,造成不必要的性能浪费;flink中的侧输出就是将数据流进行分割,而不对流进行复制的一种分流机制。flink的侧输出的另一个作用就是对延时迟到的数据进行处理,这样就可以不必丢弃迟到的数据。在后面的章节中会讲到!
  案例:根据基站的日志,请把呼叫成功的Stream(主流)和不成功的Stream(侧流)分别输出。

  /**
 * 把呼叫成功的Stream(主流)和不成功的Stream(侧流)分别输出。
 */
object TestSideOutputStream {
     
  //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
  import org.apache.flink.streaming.api.scala._
  //侧输出流首先需要定义一个流的标签
  var notSuccessTag= new OutputTag[StationLog]("not_success")

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    streamEnv.setParallelism(1)

    //读取文件数据
    val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })

    val mainStream: DataStream[StationLog] = data.process(new CreateSideOutputStream(notSuccessTag))
    //得到侧流
    val sideOutput: DataStream[StationLog] = mainStream.getSideOutput(notSuccessTag)

    mainStream.print("main")
    sideOutput.print("sideoutput")

    streamEnv.execute()
  }
  class CreateSideOutputStream(tag: OutputTag[StationLog]) extends ProcessFunction[StationLog,StationLog]{
     
    override def processElement(value: StationLog, ctx: ProcessFunction[StationLog, StationLog]#Context, out: Collector[StationLog]): Unit = {
     
      if(value.callType.equals("success")){
     //输出主流
        out.collect(value)
      }else{
     //输出侧流
        ctx.output(tag,value)
      }
    }
  }
}

第五章 Flink State 管理与恢复

  Flink是一个默认就有状态的分析引擎,前面的WordCount案例可以做到单词的数量的累加,其实是因为在内存中保证了每个单词的出现的次数,这些数据其实就是状态数据。但是如果一个Task在处理过程中挂掉了,那么它在内存中的状态都会丢失,所有的数据都需要重新计算。从容错和消息处理的语义(At -least-once和Exactly-once)上来说,Flink引入了State和CheckPoint。

  • State一般指一个具体的Task/Operator的状态,State数据默认保存在Java的堆内存中。
  • CheckPoint(可以理解为CheckPoint是把State数据持久化存储了)则表示了一个Flink Job在一个特定时刻的一份全局状态快照,即包含了所有Task/Operator的状态。

1. 常用 State

  Flink 有两种常见的 State 类型,分别是:

  • keyed State(键控状态)
  • Operator State(算子状态)

1) Keyed State(键控状态)

  Keyed State:顾名思义就是基于KeyedStream上的状态,这个状态是跟特定的Key绑定的。KeyedStream流上的每一个Key,都对应一个State。Flink针对Keyed State提供了以下可以保存State的数据结构:

  • ValueState: 保存一个可以更新和检索的值(如上所述,每个值都对应到当前的输入数据的 key,因此算子接收到的每个 key 都可能对应一个值)。 这个值可以通过 update(T) 进行更新,通过 T value() 进行获取值。
  • ListState: 保存一个元素的列表。可以往这个列表中追加数据,并在当前的列表上进行检索。可以通过 add(T) 或者 addAll(List) 进行添加元素,通过 Iterable get() 获得整个列表。还可以通过 update(List) 覆盖当前的列表。
  • ReducingState: 保存一个单值,表示添加到状态的所有值的聚合。接口与 ListState 类似,但使用 add(T) 增加元素,会使用提供的 ReduceFunction 进行聚合。
  • AggregatingState: 保留一个单值,表示添加到状态的所有值的聚合。和 ReducingState 相反的是, 聚合类型可能与 添加到状态的元素的类型不同。 接口与 ListState 类似,但使用 add(IN) 添加的元素会用指定的 AggregateFunction 进行聚合。
  • FoldingState: 保留一个单值,表示添加到状态的所有值的聚合。 与 ReducingState 相反,聚合类型可能与添加到状态的元素类型不同。 接口与 ListState 类似,但使用add(T)添加的元素会用指定的 FoldFunction 折叠成聚合值。
  • MapState: 维护了一个映射列表。 你可以添加键值对到状态中,也可以获得反映当前所有映射的迭代器。使用 put(UK,UV) 或者 putAll(Map) 添加映射。 使用 get(UK) 检索特定 key。 使用 entries(),keys() 和 values() 分别检索映射、键和值的可迭代视图。

2) Operator State(算子状态)

  Operator State与Key无关,而是与Operator绑定,整个Operator只对应一个State。比如:Flink中的Kafka Connector就使用了Operator State,它会在每个Connector实例中,保存该实例消费Topic的所有(partition, offset)映射。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第54张图片
3) Keyed State 案例

  案例需求:计算每个手机的呼叫间隔时间,单位是毫秒。–使用ValueState

/**
 * 统计每个手机的呼叫间隔时间,并输出
 */
object StateExampleWithMap {
     
  def main(args: Array[String]): Unit = {
     
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos: DataStream[String] = env.socketTextStream("mynode5",9999)
    val keyStream: KeyedStream[StationLog, String] = infos.map(line => {
     
      val arr = line.split(",")
      StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
    }).keyBy(_.callOut)

    /**
      * IN :Type of the input elements.
      * OUT :Type of the returned elements.
      */
    val result: DataStream[String] = keyStream.map(new RichMapFunction[StationLog, String] {
     

      //从上下文环境中获取一个状态来保存对应的主叫呼出时间
      private lazy val ts: ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("timeState", classOf[Long]))

      override def map(value: StationLog): String = {
     
        val phoneNum = value.callOut
        val preCallOutTime = ts.value()
        val currentCallOutTime = value.callTime
        ts.update(currentCallOutTime)
        s"主叫号码:$phoneNum,上次主叫时间:$preCallOutTime,本次主叫时间:$currentCallOutTime,两次间隔:${currentCallOutTime - preCallOutTime}ms"
      }
    })
    result.print()
    env.execute()
  }
}

  还有第二种简单的方法:调用flatMapWithState算子

object FlatMapWithStateTest {
     
  def main(args: Array[String]): Unit = {
     
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos: DataStream[String] = env.socketTextStream("mynode5",9999)
    val keyStream: KeyedStream[StationLog, String] = infos.map(line => {
     
      val arr = line.split(",")
      StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
    }).keyBy(_.callOut)

    /**
      * (T, Option[S]) => (TraversableOnce[R], Option[S])
      *  T:输入数据的类型
      *  S:每个key保存在状态中数据类型
      *  R:通过算子输出的数据类型
      */
    val result: DataStream[String] = keyStream.flatMapWithState((sl: StationLog, option: Option[Long]) => {
     
      (Array[String](s"主叫号码:${sl.callOut},上次呼出时间:${option.getOrElse(0L)}," +
        s"本次呼出时间:${sl.callTime},两次差值:${sl.callTime - option.getOrElse(0L)}"), Option(sl.callTime))
    })
    result.print()

    env.execute()
  }

}

 当然了mapWithState算子也可以

object MapWithStateTest {
     
  def main(args: Array[String]): Unit = {
     
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos: DataStream[String] = env.socketTextStream("mynode5",9999)
    val keyStream: KeyedStream[StationLog, String] = infos.map(line => {
     
      val arr = line.split(",")
      StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
    }).keyBy(_.callOut)

    /**
      * mapWithState 传入的匿名函数:(T, Option[S]) => (R, Option[S])
      *   T: 代表传入的数据类型
      *   S :代表状态中的数据类型
      *   R: 代表通过mapWithState输出返回的数据类型
      *   S : 代表状态中的数据类型
      *   注意:(R,S)类型需要在mapWithState方法后指定
      */
    val result: DataStream[String] = keyStream.mapWithState[String,Long]((sl: StationLog, option: Option[Long]) => {
     
      val phoneNum = sl.callOut
      val preCallTime = option.getOrElse(0L)
      val currentCallTime = sl.callTime
      val diff = currentCallTime - preCallTime
      (s"主叫号码:${phoneNum},上次呼出时间:${preCallTime},本次呼出时间:${currentCallTime},两次相差时长:${diff}ms", Option(currentCallTime))
    })

    result.print()

    env.execute()
  }
}

  案例需求:统计20s内主叫号码所有两次通话的呼叫间隔。–使用ListState

/**
  *  通过process算子实现  ,状态使用ListState
  *
  *  案例需求:统计每个主机号码在20内所有的通话间隔时长
  */
object StateExampleWithProcess {
     
  def main(args: Array[String]): Unit = {
     
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos: DataStream[String] = env.socketTextStream("mynode5",9999)
    val keyStream: KeyedStream[StationLog, String] = infos.map(line => {
     
      val arr = line.split(",")
      StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
    }).keyBy(_.callOut)

    /**
      * K : Type of the key.
      * I : Type of the input elements.
      * O : Type of the output elements.
      */
    val result :DataStream[String] = keyStream.process(new KeyedProcessFunction[String, StationLog, String] {
     
      //首先获取一个ListState 来存放20s 内每个主叫号码呼叫时间
      private lazy val listState: ListState[Long] = getRuntimeContext.getListState(new ListStateDescriptor[Long]("listState", classOf[Long]))

      override def processElement(value: StationLog, ctx: KeyedProcessFunction[String, StationLog, String]#Context, out: Collector[String]): Unit = {
     
        //获取当前主叫号码的存储的状态值
        val stateValueList = IteratorUtils.toList(listState.get().iterator())
        val currentProcessTime: Long = ctx.timerService().currentProcessingTime() //获取当前数据处理时间
        if (stateValueList.size() == 0) {
     //状态中没有数据,第一次呼叫
          listState.add(currentProcessTime) //向状态中加入当前处理时间
          ctx.timerService().registerProcessingTimeTimer(currentProcessTime + 20 * 1000) //设置定时器
        }
        if (stateValueList.size() != 0) {
     
          listState.add(currentProcessTime) //向状态中加入当前处理时间
        }
      }

      //定时器触发时执行onTimer
      override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[String, StationLog, String]#OnTimerContext, out: Collector[String]): Unit = {
     
        val phoneNum = ctx.getCurrentKey

        //循环找出状态中的两次间隔,并放入字符串中
        val builder = new StringBuilder()
        var preTime = 0L
        val iterator: util.Iterator[Long] = listState.get().iterator()
        while(iterator.hasNext){
     
          val elem = iterator.next()
          if (preTime != 0) {
     
            builder.append(s"【pthoneNum = $phoneNum,perTime = ${preTime},currentTime = ${elem},间隔:${elem - preTime}ms】->")
          }
          preTime = elem
        }
        //将状态重置
        listState.clear()
        out.collect(builder.toString().substring(0, builder.toString().length - 2))

      }
    })
    result.print()

    env.execute()
  }

}

2. CheckPoint

  当程序出现问题需要恢复Sate数据的时候,只有程序提供支持才可以实现State的容错。State的容错需要依靠CheckPoint机制,这样才可以保证Exactly-once这种语义,但是注意,它只能保证Flink系统内的Exactly-once,比如Flink内置支持的算子。针对Source和Sink组件,如果想要保证Exactly-once的话,则这些组件本身应支持这种语义。

1) CheckPoint 原理

  Flink中基于异步轻量级的分布式快照技术提供了Checkpoints容错机制,分布式快照可以将同一时间点Task/Operator的状态数据全局统一快照处理,包括前面提到的Keyed State和Operator State。Flink会在输入的数据集上间隔性地生成checkpoint barrier,通过栅栏(barrier)将间隔时间段内的数据划分到相应的checkpoint中。如下图:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第55张图片2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第56张图片
从检查点(CheckPoint)恢复如下图:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第57张图片2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第58张图片2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第59张图片2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第60张图片
2) CheckPoint 参数和设置

  默认情况下Flink不开启检查点的,用户需要在程序中通过调用方法配置和开启检查点,另外还可以调整其他相关参数:

  • Checkpoint开启和时间间隔指定:

  开启检查点并且指定检查点时间间隔为1000ms,根据实际情况自行选择,如果状态比较大,则建议适当增加该值。

  streamEnv.enableCheckpointing(1000);
  • exactly-once和at-least-once语义选择:

  选择exactly-once语义保证整个应用内端到端的数据一致性,这种情况比较适合于数据要求比较高,不允许出现丢数据或者数据重复,与此同时,Flink的性能也相对较弱,而at-least-once语义更适合于时廷和吞吐量要求非常高但对数据的一致性要求不高的场景。
  如果在Flink内部exactly-once语义涉及到barrier对齐,如果at-least-once语义就是barrier不对齐,只有Flink内部多个流(多个并行度时)才会涉及到barrier对齐问题。
如下通过setCheckpointingMode()方法来设定语义模式,默认情况下使用的是exactly-once模式。

streamEnv.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)//或者
streamEnv.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.AT_LEAST_ONCE)
  • Checkpoint超时时间:

  超时时间指定了每次Checkpoint执行过程中的上限时间范围,一旦Checkpoint执行时间超过该阈值,Flink将会中断Checkpoint过程,并按照超时处理。该指标可以通过setCheckpointTimeout方法设定,默认为10分钟。

  streamEnv.getCheckpointConfig.setCheckpointTimeout(50000)
  • 检查点之间最小时间间隔:

  该参数主要目的是设定两个Checkpoint之间的最小时间间隔,防止出现例如状态数据过大而导致Checkpoint执行时间过长,从而导致Checkpoint积压过多,最终Flink应用密集地触发Checkpoint操作,会占用了大量计算资源而影响到整个应用的性能。

streamEnv.getCheckpointConfig.setMinPauseBetweenCheckpoints(600)
  • 最大并行执行的检查点数量:

  通过setMaxConcurrentCheckpoints()方法设定能够最大同时执行的 Checkpoint数量。在默认情况下只有一个检查点可以运行,根据用户指定的数量可以同时触发多个Checkpoint,进而提升Checkpoint整体的效率。

streamEnv.getCheckpointConfig.setMaxConcurrentCheckpoints(1)
  • 是否删除Checkpoint中保存的数据:

  设置为RETAIN_ON_CANCELLATION:表示一旦Flink处理程序被cancel后,会保留CheckPoint数据,以便根据实际需要恢复到指定的CheckPoint。
  设置为DELETE_ON_CANCELLATION:表示一旦Flink处理程序被cancel后,会删除CheckPoint数据,只有Job执行失败的时候才会保存CheckPoint。(默认)

//删除  streamEnv.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.DELETE_ON_CANCELLATION)
//保留  streamEnv.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)
  • TolerableCheckpointFailureNumber:

  checkpoint在执行过程中如果出现失败设置可以容忍的检查的失败数,超过这个数量则系统自动关闭和停止任务。

streamEnv.getCheckpointConfig.setTolerableCheckpointFailureNumber(1)

3. 保存机制 StateBackend(状态后端)

  默认情况下,State会保存在TaskManager的内存中,CheckPoint会存储在JobManager的内存中。State和CheckPoint的存储位置取决于StateBackend的配置。Flink一共提供了3种StateBackend。包括基于内存的MemoryStateBackend、基于文件系统的FsStateBackend,以及基于RockDB作为存储介质的RocksDBState-Backend。

1) MemoryStateBackend

  基于内存的状态管理具有非常快速和高效的特点,但也具有非常多的限制,最主要的就是内存的容量限制,一旦存储的状态数据过多就会导致系统内存溢出等问题,从而影响整个应用的正常运行。同时如果机器出现问题,整个主机内存中的状态数据都会丢失,进而无法恢复任务中的状态数据。因此从数据安全的角度建议用户尽可能地避免在生产环境中使用MemoryStateBackend。

streamEnv.setStateBackend(new MemoryStateBackend(10*1024*1024))

2) FsStateBackend

  和MemoryStateBackend有所不同,FsStateBackend是基于文件系统的一种状态管理器,这里的文件系统可以是本地文件系统,也可以是HDFS分布式文件系统。FsStateBackend更适合任务状态非常大的情况,例如应用中含有时间范围非常长的窗口计算,或Key/value State状态数据量非常大的场景。

streamEnv.setStateBackend(new FsStateBackend("hdfs://hadoop101:9000/checkpoint/cp1"))

3) RocksDBStateBackend

  RocksDBStateBackend是Flink中内置的第三方状态管理器,和前面的状态管理器不同,RocksDBStateBackend需要单独引入相关的依赖包到工程中。

 >
  >org.apache.flink>
  >flink-statebackend-rocksdb_2.11>
  >1.9.1>
>

  RocksDBStateBackend采用异步的方式进行状态数据的Snapshot,任务中的状态数据首先被写入本地RockDB中(RockDB是一个高效、高性能的数据库引擎,可以直接使用内存、也可以使用硬盘或者HDFS,支持不同的压缩算法),这样在RockDB仅会存储正在进行计算的热数据,而需要进行CheckPoint的时候,会把本地的数据直接复制到远端的FileSystem中。
  与FsStateBackend相比,RocksDBStateBackend在性能上要比FsStateBackend高一些,主要是因为借助于RocksDB在本地存储了最新热数据,然后通过异步的方式再同步到文件系统中,但RocksDBStateBackend和MemoryStateBackend相比性能就会较弱一些。RocksDB克服了State受内存限制的缺点,同时又能够持久化到远端文件系统中,推荐在生产中使用。

  streamEnv.setStateBackend(
new RocksDBStateBackend ("hdfs://hadoop101:9000/checkpoint/cp2"))

4) 全局配置 StateBackend

  以上的代码都是单job配置状态后端,也可以全局配置状态后端,需要修改flink-conf.yaml配置文件:

  state.backend: filesystem

  其中:
  filesystem 表示使用 FsStateBackend,
  jobmanager 表示使用 MemoryStateBackend
  rocksdb 表示使用 RocksDBStateBackend。

  state.checkpoints.dir: hdfs://hadoop101:9000/checkpoints

  默认情况下,如果设置了CheckPoint选项,则Flink只保留最近成功生成的1个CheckPoint,而当Flink程序失败时,可以通过最近的CheckPoint来进行恢复。但是,如果希望保留多个CheckPoint,并能够根据实际需要选择其中一个进行恢复,就会更加灵活。添加如下配置,指定最多可以保存的CheckPoint的个数。

  state.checkpoints.num-retained: 2

4. Checkpoint案例

  案例:设置HDFS文件系统的状态后端,取消Job之后再次恢复Job。

  object CheckpointOnFsBackend {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    streamEnv.enableCheckpointing(5000)
    streamEnv.setStateBackend(new FsStateBackend("hdfs://hadoop101:9000/checkpoint/cp1"))

    streamEnv.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
    streamEnv.getCheckpointConfig.setCheckpointTimeout(50000)

    streamEnv.getCheckpointConfig.setMaxConcurrentCheckpoints(1)

    streamEnv.getCheckpointConfig.enableExternalizedCheckpoints(ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)

    streamEnv.getCheckpointConfig.setTolerableCheckpointFailureNumber(1)

    streamEnv.setParallelism(1)

    import org.apache.flink.streaming.api.scala._
    //读取数据得到DataStream
    val stream = streamEnv.socketTextStream("hadoop101",8888)

    stream.flatMap(_.split(" ")).map((_,1)).keyBy(0).sum(1).print()


    streamEnv.execute("wc") //启动流计算
  }
}
  • 打包在服务器上执行

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第61张图片

  • 查看执行结果,可以看出flink出现四次

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第62张图片

  • 取消Job,可以看到Job已经停止

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第63张图片

  • 查看HDFS目录上的状态文件

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第64张图片

  • 通过命令启动Job

[root@hadoop101 bin]# ./flink run -d -s hdfs://hadoop101:9000/checkpoint/cp1/b38e35788eecf3053d4a87d52e97d22d/chk-272 -c com.bjsxt.flink.state.CheckpointOnFsBackend /home/Flink-Demo-1.0-SNAPSHOT.jar
 注意:使用 -s 来指定checkpoint目录,需要指定到chk-xxx 目录。

  • 输入新数据,查看结果
    2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第65张图片

5. SavePoint

  Savepoints 是检查点的一种特殊实现,底层实现其实也是使用Checkpoints的机制。Savepoints是用户以手工命令的方式触发Checkpoint,并将结果持久化到指定的存储路径中,其主要目的是帮助用户在升级和维护集群过程中保存系统中的状态数据,避免因为停机运维或者升级应用等正常终止应用的操作而导致系统无法恢复到原有的计算状态的情况,从而无法实现从端到端的 Excatly-Once 语义保证。

1) 配置 Savepoints 的存储路径

  在flink-conf.yaml中配置SavePoint存储的位置,设置后,如果要创建指定Job的SavePoint,可以不用在手动执行命令时指定SavePoint的位置。

  state.savepoints.dir: hdfs:/hadoop101:9000/savepoints

2) 在代码中设置算子ID

  为了能够在作业的不同版本之间以及Flink的不同版本之间顺利升级,强烈推荐程序员通过手动给算子赋予ID,这些ID将用于确定每一个算子的状态范围。如果不手动给各算子指定ID,则会由Flink自动给每个算子生成一个ID。而这些自动生成的ID依赖于程序的结构,并且对代码的更改是很敏感的。因此,强烈建议用户手动设置ID。

  object TestSavepoints {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    streamEnv.setParallelism(1)

    import org.apache.flink.streaming.api.scala._
    //读取数据得到DataStream
    val stream: DataStream[String] = streamEnv.socketTextStream("hadoop101",8888)
        .uid("mySource-001")

    stream.flatMap(_.split(" "))
      .uid("flatMap-001")
      .map((_,1))
      .uid("map-001")
      .keyBy(0)
      .sum(1)
      .uid("sum-001")
      .print()

    streamEnv.execute("wc") //启动流计算
  }
}

3) 触发 SavePoint

//先启动Job
[root@hadoop101 bin]# ./flink run -c com.bjsxt.flink.state.TestSavepoints -d /home/Flink-Demo-1.0-SNAPSHOT.jar
//再取消Job ,触发SavePoint
[root@hadoop101 bin]# ./flink savepoint 6ecb8cfda5a5200016ca6b01260b94ce
[root@hadoop101 bin]# ./flink cancel 6ecb8cfda5a5200016ca6b01260b94ce

2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第66张图片2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第67张图片
注意:在yarn提交的Flink job触发 savepoint 时,需要指定yid ,命令如下:

./flink savepoint  flink-job-id  savepointpath  -yid  application_xxx_0001

注意:如果savepointpath在当前提交任务节点的flink-conf.yaml中配置了,就不需要再写上。

./flink savepoint  flink-job-id  savepointpath  -yid  application_xxx_0001
例如:
./flink savepoint  cd4192b02d9ce3127b0256525ec83b67 hdfs://mycluster/sv -yid application_1598581537108_0004

4)SavePoint启动Job

[root@hadoop101 bin]# ./flink run -s hdfs://hadoop101:9000/savepoints/savepoint-6ecb8c-e56ccb88576a -c com.bjsxt.flink.state.TestSavepoints -d /home/Flink-Demo-1.0-SNAPSHOT.jar

 也可以通过Web UI启动Job:2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第68张图片
注意:基于yarn提交的flink任务恢复时,按照正常的提交任务命令写即可。例如:
//从SavePoint中恢复任务提交到 yarn-cluster模式,命令如下:

[root@hadoop101 bin]# ./flink run -s hdfs://hadoop101:9000/savepoints/savepoint-6ecb8c-e56ccb88576a -c com.bjsxt.flink.state.TestSavepoints -d /home/Flink-Demo-1.0-SNAPSHOT.jar

第六章 Flink Window(窗口)详解

  Windows计算是流式计算中非常常用的数据计算方式之一,通过按照固定时间或长度将数据流切分成不同的窗口,然后对数据进行相应的聚合运算,从而得到一定时间范围内的统计结果。例如统计最近5分钟内某基站的呼叫数,此时基站的数据在不断地产生,但是通过5分钟的窗口将数据限定在固定时间范围内,就可以对该范围内的有界数据执行聚合处理,得出最近5分钟的基站的呼叫数量。

1. Window分类

1) Global WindowKeyed Window

  在运用窗口计算时,Flink根据上游数据集是否为KeyedStream类型,对应的Windows 也会有所不同。

  • Keyed Window:上游数据集如果是KeyedStream类型,则调用DataStream API的window()方法,数据会根据Key在不同的Task实例中并行分别计算,最后得出针对每个Key统计的结果。
/**
 * 读取socket数据,每5s统计窗口内每个单词出现的次数
 */
val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos: KeyedStream[(String, Int), Tuple] = env.socketTextStream("mynode5", 9999)
      .flatMap(_.split(" "))
      .map(word => {
     
        (word, 1)
      }).keyBy(0)

    /**
      *  1.可以使用window 对KeyedStream进行窗口设置
      *     TumblingProcessingTimeWindows.of(Time.seconds(5) :滚动窗口,使用数据进入到Flink开始处理的时间作为标准来统计窗口
      *     TumblingEventTimeWindows.of(Time.seconds(5) : 滚动窗口,使用事件产生时间作为标准来统计窗口,这个需要手动指定,后续讲。
      *
      *  2.也可以使用timeWindow 对KeyedStream进行窗口设置
      */
//    val windows: WindowedStream[(String, Int), Tuple, TimeWindow] = infos.window(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    val windows: WindowedStream[(String, Int), Tuple, TimeWindow] = infos.timeWindow(Time.seconds(5))
    val result: DataStream[(String, Int)] = windows.reduce((tp1, tp2)=>{
     (tp1._1,(tp1._2+tp2._2))})
    result.print()
    env.execute()
  • Global Window:如果是Non-Keyed类型,则调用 WindowsAll()方法,所有的数据都会在窗口算子中由到一个Task中计算,并得到全局统计结果。
/**
 * 读取Socket数据,每隔5s,统计下输入数据的总条数
 */   
 val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos : DataStream[(String,Int)] = env.socketTextStream("mynode5",9999)
      .map(line=>{
     (line,1)})

    /**
      *  1.可以使用windowAll 对没有key的DataStream数据进行窗口处理
      *     TumblingProcessingTimeWindows.of(Time.seconds(5) :滚动窗口,使用数据进入到Flink开始处理的时间作为标准来统计窗口
      *     TumblingEventTimeWindows.of(Time.seconds(5) : 滚动窗口,使用事件产生时间作为标准来统计窗口,这个需要手动指定,后续讲。
      *
      *  2.也可以使用 timeWindowAll 对没有key的DataStream数据进行窗口处理
      *     timeWindowAll(Time.second(5)) ,这里的时间是以当前系统时间戳时间为标准统计窗口
      */
//    val aws: AllWindowedStream[(String,Int), TimeWindow] = infos.windowAll(TumblingProcessingTimeWindows.of(Time.seconds(5)))
    val aws: AllWindowedStream[(String,Int), TimeWindow] = infos.timeWindowAll(Time.seconds(5))
    val result: DataStream[(String,Int)] = aws.sum(1)
    result.map(_._2).print()
    env.execute()

2) Time WindowCount Window

  基于业务数据的方面考虑,Flink又支持两种类型的窗口,一种是基于时间的窗口叫Time Window。还有一种基于输入数据数量的窗口叫Count Window

2. Time Window(时间窗口)

  根据不同的业务场景,Time Window也可以分为三种类型,分别是滚动窗口(Tumbling Window)、滑动窗口(Sliding Window)和会话窗口(Session Window)

1) 滚动窗口**(Tumbling Window)**

  滚动窗口是根据固定时间进行切分,且窗口和窗口之间的元素互不重叠。这种类型的窗口的最大特点是比较简单。只需要指定一个窗口长度(window size)。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第69张图片

    //每隔5秒统计每个基站的日志数量
    data.map(stationLog=>((stationLog.sid,1)))
      .keyBy(_._1)
      .timeWindow(Time.seconds(5)) 
      //.window(TumblingProcessingTimeWindows.of(Time.seconds(5))) 
      .sum(1) //聚合

  其中时间间隔可以是Time.milliseconds(x)、Time.seconds(x)、Time.minutes(x)、Time.hours(x)。

2) 滑动窗口(Sliding Window

  滑动窗口也是一种比较常见的窗口类型,其特点是在滚动窗口基础之上增加了窗口滑动时间(Slide Time),且允许窗口数据发生重叠。当Windows size固定之后,窗口并不像滚动窗口按照Windows Size向前移动,而是根据设定的Slide Time向前滑动。窗口之间的数据重叠大小根据Windows size和Slide time决定,当Slide time小于Windows size便会发生窗口重叠,Slide size大于Windows size就会出现窗口不连续,数据可能不能在任何一个窗口内计算,Slide size和Windows size相等时,Sliding Windows其实就是Tumbling Windows。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第70张图片

    //每隔3秒计算最近5秒内,每个基站的日志数量
    data.map(stationLog=>((stationLog.sid,1)))
      .keyBy(_._1)
      .timeWindow(Time.seconds(5),Time.seconds(3))
     //.window(TumblingProcessingTimeWindows.of(Time.seconds(5),Time.seconds(3)))
      .sum(1)    

3) 会话窗口(Session Window

  会话窗口(Session Windows)主要是将某段时间内活跃度较高的数据聚合成一个窗口进行计算,窗口的触发的条件是Session Gap,是指在规定的时间内如果没有数据活跃接入,则认为窗口结束,然后触发窗口计算结果。需要注意的是如果数据一直不间断地进入窗口,也会导致窗口始终不触发的情况。与滑动窗口、滚动窗口不同的是,Session Windows不需要有固定windows size和slide time,只需要定义session gap,来规定不活跃数据的时间上限即可。Session Windows 窗口类型比较适合非连续性数据处理或周期性产生数据场景。
  注意:SessionWindow本质上没有固定的起止时间点,如果对非KeyedStream 使用 SessionWindow ,那么会为所有数据创建一个窗口,当超过指定时间没有输入数据就会触发窗口执行,如果对KeyedStream使用SessionWindow ,那么会为每个key都会创建一个窗口,每个窗口中如果当前key没有数据继续输入,超过指定时间,也会触发当前key对应的窗口。
2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第71张图片

//案例:读取Socket数据,如果3s内没有输入当前单词,统计单词个数
//超过3s socket中每个单词如果没有输入数据,就会触发key对应的窗口 
val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._

    val infos = env.socketTextStream("mynode5", 9999)
      .flatMap(_.split(" "))
      .map((_, 1))
      .keyBy(0)

    /**
      * 这里只能使用window ,不能使用timeWindow,因为需要指定 ProcessingTimeSessionWindows.withGap(Time.seconds(3)),也可以指定EventTimeSessionWindows,需要设置事件时间
      * 注意:这里作用在KeyedStream上,会为每个key的数据都会创建一个窗口,如果当前key在最近3s内都没有继续输入,那么就会触发当期key的窗口
      */
    val ws: WindowedStream[(String, Int), Tuple, TimeWindow] = infos.window(ProcessingTimeSessionWindows.withGap(Time.seconds(3)))
    ws.sum(1).print()

    env.execute()

3. Count Window(数量窗口)

  Count Window是基于数量的窗口,根据固定的数量定义窗口的大小,例如每5000条数据形成一个窗口。CountWindow也有滚动窗口、滑动窗口等。
  案例:Flink监控socket输入基站通话数据,①.每当有5个成功通话时,将对应的成功通话时长累加输出。②.每当有3条成功通话数据时,输出最近5条成功通话总时长。

    val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos: DataStream[String] = env.socketTextStream("mynode5", 9999)
    val stationInfos: DataStream[StationLog] = infos.map(line => {
     
      val arr = line.split(",")
      StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
    })
    //过滤通话成功数据,并转换
    val transInfos: KeyedStream[(String, Long), Tuple] = stationInfos.filter(sl => {
     
      "success".equals(sl.callType)
      })
      .map(sl => {
     
        (sl.callType, sl.duration)
      })
      .keyBy(0)


    //数量窗口统计 : ①.每当有5个成功通话时,将对应的成功通话时长累加输出。
//    transInfos.countWindow(5).sum(1).print()

    //数量窗口统计 : ②.每当有3条成功通话数据时,输出最近 5 条成功通话总时长。
    transInfos.countWindow(5,3).sum(1).print()


    env.execute()

4. Window的API

  在以后的实际案例中Keyed Window使用最多,所以我们需要掌握Keyed Window的算子,在每个窗口算子中包含了Windows Assigner、Windows Trigger(窗口触发器)、Evictor(数据剔除器)、Lateness(时延设定)、Output Tag(输出标签)以及Windows Funciton等组成部分,其中Windows Assigner和Windows Funciton是所有窗口算子必须指定的属性,其余的属性都是根据实际情况选择指定。

stream.keyBy(...) // 是Keyed类型数据集
.window(...)  //指定窗口分配器类型
[.trigger(...)] //指定触发器类型(可选)
[.evictor(...)]    //指定evictor或者不指定(可选)
[.allowedLateness(...)]   //指定是否延迟处理数据(可选)
[.sideOutputLateData(...)] //指定Output Lag(可选)
.reduce/aggregate/fold/apply()/process   //指定窗口计算函数
[.getSideOutput(...)]    //根据Tag输出数据(可选) 
  • Windows Assigner:指定窗口的类型,定义如何将数据流分配到一个或多个窗口;
  • Windows Trigger:指定窗口触发的时机,定义窗口满足什么样的条件触发计算;
  • Evictor:用于数据剔除;
  • allowedLateness:标记是否处理迟到数据,当迟到数据到达窗口中是否触发计算;
  • Output Tag:标记输出标签,然后在通过getSideOutput将窗口中的数据根据标签输出;
  • Windows Funciton:定义窗口上数据处理的逻辑,例如对数据进行sum操作。

5. 窗口聚合函数

  如果定义了Window Assigner之后,下一步就可以定义窗口内数据的计算逻辑,这也就是Window Function的定义。Flink中提供了四种类型的Window Function,分别为ReduceFunction、AggregateFunction、ProcessWindowFunction以及WindowFunction(sum和max)等。
  以上类型的Window Fucntion按照计算原理的不同可以分为两大类:

  • 一类是增量聚合函数:对应有ReduceFunction、AggregateFunction;
  • 另一类是全量窗口函数,对应有ProcessWindowFunction、WindowFunction。

  增量聚合函数计算性能较高,占用存储空间少,主要因为基于中间状态的计算结果,窗口中只维护中间结果状态值,不需要缓存原始数据。而全量窗口函数使用的代价相对较高,性能比较弱,主要因为此时算子需要对所有属于该窗口的接入数据进行缓存,然后等到窗口触发的时候,对所有的原始数据进行汇总计算。

1) reduce - ReduceFunction

  ReduceFunction定义了对输入的两个相同类型的数据元素按照指定的计算方法进行聚合的逻辑,然后输出类型相同的一个结果元素。

//每隔5秒统计每个基站的日志数量
    data.map(stationLog=>((stationLog.sid,1)))
      .keyBy(_._1)
      .window(TumblingProcessTimeWindows.of(Time.seconds(5)))
      .reduce((v1,v2)=>(v1._1,v1._2+v2._2))

2) aggregate - AggregateFunction

  和ReduceFunction相似,AggregateFunction也是基于中间状态计算结果的增量计算函数,但AggregateFunction在窗口计算上更加通用。AggregateFunction接口相对ReduceFunction更加灵活,实现复杂度也相对较高。AggregateFunction接口中定义了四个需要复写的方法,其中add()定义数据的添加逻辑,getResult定义了根据accumulator计算结果的逻辑,merge方法定义合并accumulator的逻辑。

//每隔5秒计算最近10秒内,每个基站的日志数量
val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos: WindowedStream[StationLog, Tuple, TimeWindow] =
      env.socketTextStream("mynode5", 9999).map(line => {
     
      val arr = line.split(",")
      StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
    }).keyBy(0)
      .timeWindow(Time.seconds(10), Time.seconds(5))

    /**
      * 三种类型:
      *  IN  : 输入数据类型
      *  ACC : 累加器累加数据类型
      *  OUT : 聚合结果类型
      */
    val result: DataStream[(String, Long)] = infos.aggregate(new AggregateFunction[StationLog, (String, Long), (String, Long)] {
     
      //创建累加器
      override def createAccumulator(): (String, Long) = ("", 0L)

      //每个分区内累加器累加
      override def add(value: StationLog, accumulator: (String, Long)): (String, Long) = (value.sid, accumulator._2 + 1L)

      //获取最后结果
      override def getResult(accumulator: (String, Long)): (String, Long) = accumulator

      //不同的分区之间数据结果合并,这里如果针对的是KeyedStream 是不会执行的
      override def merge(a: (String, Long), b: (String, Long)): (String, Long) = (a._1, (a._2 + b._2))
    })

    result.print()

    env.execute()

  以上aggregate 方法除了可以跟上AggregateFunction,还可以再跟上WindowFunction获取更多的窗口和key的信息:

//每隔5秒计算最近10秒内,每个基站的日志数量
val env = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val infos: WindowedStream[StationLog, String, TimeWindow] =
      env.socketTextStream("mynode5", 9999).map(line => {
     
        val arr = line.split(",")
        StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
      }).keyBy(_.sid)
        .timeWindow(Time.seconds(10), Time.seconds(5))

    /**
      * AggregateFunction : AggregateFunction将处理结果传递给WindowFunction处理
      *     IN  : 输入数据类型
      *     ACC : 累加器累加数据类型
      *     OUT : 聚合结果类型
      * WindowFunction : 当AggregateFunction 执行完成之后执行
      *     IN  : 当AggregateFunction执行完成之后将数据结果输入,输入数据类型
      *     OUT :窗口输出数据类型
      *     KEY :Key的类型
      *     W   : 窗口类型 : 时间窗口或者全局窗口
      */
    val result: DataStream[(String, Long)] = infos.aggregate(new AggregateFunction[StationLog, Long, Long] {
     
      //创建累加器
      override def createAccumulator(): Long = 0L
      //在每个分区内,每个key的数据进行累加
      override def add(value: StationLog, accumulator: Long): Long = accumulator + 1L
      //在每个分区内,获取最终每个key的结果
      override def getResult(accumulator: Long): Long = accumulator
      //将不同分区的数据进行累加,这里如果针对的是keyedStream ,不会执行,非KeyedStream才会执行
      override def merge(a: Long, b: Long): Long = a + b
    }, new WindowFunction[Long, (String, Long), String, TimeWindow] {
     
      //window Function 中apply 在窗口最后执行,可以获取窗口类型及Key数据,返回自己想要的格式数据
      override def apply(key: String, window: TimeWindow, input: Iterable[Long], out: Collector[(String, Long)]): Unit = {
     
        val str = s"窗口起始时间:${window.getStart} - ${window.getEnd}"
        out.collect((key + "_" + str, input.iterator.next()))
      }
    })

    result.print()

    env.execute()

3) process - ProcessWindowFunction

  前面提到的ReduceFunction和AggregateFunction都是基于中间状态实现增量计算的窗口函数,虽然已经满足绝大多数场景,但在某些情况下,统计更复杂的指标可能需要依赖于窗口中所有的数据元素,或需要操作窗口中的状态数据和窗口元数据,这时就需要使用到ProcessWindowsFunction,ProcessWindowsFunction能够更加灵活地支持基于窗口全部数据元素的结果计算,例如对整个窗口数据排序取TopN,这样的需要就必须使用ProcessWindowFunction。
  案例:Flink读取Socket数据,每隔5s统计最近10s通话成功的最大时长。

  //案例:Flink读取Socket数据,每隔5s统计最近10s通话成功的最大时长。
 /**
      * 当时间到达5s后,触发窗口,调用process方法
      *  IN  : 输入数据类型
      *  OUT :输出数据类型
      *  KEY : Key值类型
      *  W   :窗口类型,这里是时间窗口
      */
    val result: DataStream[String] = infos.process(new ProcessWindowFunction[StationLog, String, String, TimeWindow] {
     
      override def process(key: String, context: Context, elements: Iterable[StationLog], out: Collector[String]): Unit = {
     
        var phoneNum = ""
        var duration = 0L
        for (elem <- elements.toList) {
     
          if (elem.duration > duration) {
     
            phoneNum = elem.callOut
            duration = elem.duration
          }
        }
        out.collect(s"主叫号码: $phoneNum,最大时长 : $duration")
      }
    })
    result.print()
    env.execute()

4) apply - WindowFunction

  WindowsFunction同样可以支持基于窗口全部数据元素的结果计算,用法与ProcessWindowFunction一样,可以针对窗口通过调用apply方法使用WindowFunction。
案例:Flink读取Socket数据,每隔5s统计最近10s通话成功的最大时长。

    //案例:Flink读取Socket数据,每隔5s统计最近10s通话成功的最大时长。
/**
      * IN : 输入数据类型
      * OUT  :输出数据类型
      * KEY  : key的类型
      * W : 窗口类型
      */
    val result: DataStream[String] = infos.apply(new WindowFunction[StationLog, String, String, TimeWindow] {
     
      override def apply(key: String, window: TimeWindow, input: Iterable[StationLog], out: Collector[String]): Unit = {
     
        var phoneNum = ""
        var duration = 0L
        for (elem <- input.toList) {
     
          if (elem.duration > duration) {
     
            phoneNum = elem.callOut
            duration = elem.duration
          }
        }
        out.collect(s"主叫号码: $phoneNum,最大时长 : $duration")
      }
    })

    result.print()
    env.execute()

第七章 Flink Time详解

  对于流式数据处理,最大的特点是数据上具有时间的属性特征,Flink根据时间产生的位置不同,将时间区分为三种时间语义,分别为事件生成时间(Event Time)、事件接入时间(Ingestion Time)和事件处理时间(Processing Time)。

  • Event Time:事件产生的时间,它通常由事件中的时间戳描述。
  • Ingestion Time:事件进入Flink的时间。
  • Processing Time:事件被处理时当前系统的时间。

1. 时间语义Time

  数据从终端产生,或者从系统中产生的过程中生成的时间为事件生成时间,当数据经过消息中间件传入到Flink系统中,在DataSource中接入的时候会生成事件接入时间,当数据在Flink系统中通过各个算子实例执行转换操作的过程中,算子实例所在系统的时间为数据处理时间。Flink已经支持这三种类型时间概念,用户能够根据需要选择时间类型作为对流式数据的依据,这种情况极大地增强了对事件数据处理的灵活性和准确性。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第72张图片
1) 设置时间语义

  在Flink中默认情况下使用是Process Time时间语义,如果用户选择使用Event Time或者Ingestion Time语义,则需要在创建的StreamExecutionEnvironment中调用setStreamTimeCharacteristic()方法设定系统的时间概念,如下代码使用TimeCharacteristic.EventTime作为系统的时间语义:

//设置使用EventTime
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
//设置使用IngestionTime
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)

  注意:但是上面的代码还没有指定具体的时间到底是什么值,所以后面还有代码需要设置!

2. WaterMark 水位线(解决数据乱序问题)

  在使用EventTime处理Stream数据的时候会遇到数据乱序的问题,流处理从Event(事件)产生,流经Source,再到Operator,这中间需要一定的时间。虽然大部分情况下,传输到Operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因而导致乱序的产生,特别是使用Kafka的时候,多个分区之间的数据无法保证有序。因此,在进行Window计算的时候,不能无限期地等下去,必须要有个机制来保证在特定的时间后,必须触发Window进行计算,这个特别的机制就是Watermark(水位线)。Watermark是用于处理乱序事件的。

1) Watermark原理

  在Flink的窗口处理过程中,如果确定全部数据到达,就可以对Window的所有数据做窗口计算操作(如汇总、分组等),如果数据没有全部到达,则继续等待该窗口中的数据全部到达才开始处理。这种情况下就需要用到水位线(WaterMarks)机制,它能够衡量数据处理进度(表达数据到达的完整性),保证事件数据(全部)到达Flink系统,或者在乱序及延迟到达时,也能够像预期一样计算出正确并且连续的结果。当任何Event进入到Flink系统时,会根据当前最大事件时间产生Watermarks时间戳。
  那么Flink是怎么计算Watermak的值呢?
  Watermark = 进入Flink的最大的事件时间(mxtEventTime)— 指定的延迟时间(t)
  那么有Watermark的Window是怎么触发窗口函数的呢?
  如果有窗口的停止时间小于等于【maxEventTime – t】(即:当时的warkmark),那么这个窗口被触发执行。
注意:Watermark本质可以理解成一个延迟触发机制。

  Watermark的使用存在三种情况:

  1. 本来有序的Stream中的Watermark

  如果数据元素的事件时间是有序的,Watermark时间戳会随着数据元素的事件时间按顺序生成,此时水位线的变化和事件时间保持一致(因为既然是有序的时间,就不需要设置延迟了,那么t就是0。所以watermark=maxtime-0 = maxtime),也就是理想状态下的水位线。当Watermark时间大于Windows结束时间就会触发对Windows的数据计算,以此类推,下一个Window也是一样。在这里插入图片描述

  1. 乱序事件中的Watermark

  现实情况下数据元素往往并不是按照其产生顺序接入到Flink系统中进行处理,而频繁出现乱序或迟到的情况,这种情况就需要使用Watermarks来应对。比如下图,设置延迟时间t为2在这里插入图片描述

  1. 并行数据流中的Watermark

  在多并行度的情况下,Watermark会有一个对齐机制,这个对齐机制会取所有Channel中最小的Watermark。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第73张图片
2) 引入 WatermarkEventTime

1. 有序数据流中引入Watermark和EventTime

  对于有序的数据,代码比较简洁,主要需要从源Event中抽取EventTime。
  注意:ds.assignAscendingTimestamps(…) 需要设置在keyBy和Window函数之前。

/**
 * Flink读取Socket数据,每隔5s计算最近10s内每个基站通话时间最长的一条数据信息,输出信息中输出窗口时间。时间选用EventTime
注意:为了方便演示,这里设置一个线程,因为有waterMark对齐问题,所以如果多个线程,需要输入的数据很多,不方便
 */
val env = StreamExecutionEnvironment.getExecutionEnvironment
import org.apache.flink.streaming.api.scala._
//设置一个线程
env.setParallelism(1)
//设置 waterMark 使用时间时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

val keyedStream: KeyedStream[StationLog, String] = env.socketTextStream("mynode5", 9999)
  .map(line => {
     
    val arr = line.split(",")
    StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
  }).assignAscendingTimestamps(sl=>{
     sl.callTime})//设置时间EventTime
  .keyBy(_.sid)

/**
  * ReduceFunction:窗口内的多条数据,出一条数据
  * WindowFunction :
  *     IN :输入数据的类型
  *     OUT:输出数据的类型
  *     KEY :key的类型
  *     W:窗口类型,这里是时间窗口
  */
val result: DataStream[String] = keyedStream.timeWindow(Time.seconds(10), Time.seconds(5))
  .reduce(new ReduceFunction[StationLog] {
     
    override def reduce(sl1: StationLog, sl2: StationLog): StationLog = {
     
      if (sl1.duration > sl2.duration) sl1 else sl2
    }
  }, new WindowFunction[StationLog, String, String, TimeWindow] {
     
    override def apply(key: String, window: TimeWindow, input: Iterable[StationLog], out: Collector[String]): Unit = {
     
      out.collect(s"窗口起始时间:${window.getStart}-${window.getEnd} ,基站ID:$key ,最大时长:${input.iterator.next().duration}")
    }
  })
result.print()
env.execute()

2. 乱序序数据流中引入Watermark和EventTime

  对于乱序数据流,有两种常见的引入方法:周期性和间断性。

  1. With Periodic(周期性的) Watermark

  周期性地生成Watermark的生成,默认是200ms。每隔N毫秒自动向流里注入一个Watermark,时间间隔由streamEnv.getConfig.setAutoWatermarkInterval()决定。
  最简单的写法如下:
  注意:ds.assignTimestampsAndWatermarks(…) 需要设置在keyBy和Window函数之前。

/**
 * Flink读取Socket数据,每隔5s计算最近10s内每个基站通话时间最长的一条数据信息,输出信息中输出窗口时间。时间选用EventTime
 */
val env = StreamExecutionEnvironment.getExecutionEnvironment
import org.apache.flink.streaming.api.scala._
//设置一个线程
env.setParallelism(1)
//设置 waterMark 使用事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

val ds1: DataStream[StationLog] = env.socketTextStream("mynode5", 9999)
  .map(line => {
     
    val arr = line.split(",")
    StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
  })

/**
  * 第一种设置方式:这里设置WaterMark ,从事件中获取事件时间,同时指定允许超时的时间 3s,也就是后期延迟3s触发窗口。
  */
val ds2: DataStream[StationLog] = ds1.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(3)) {
     
  override def extractTimestamp(element: StationLog): Long = element.callTime
})

ds2.keyBy(_.sid)
  .timeWindow(Time.seconds(10),Time.seconds(5))
  .reduce(
    (s1:StationLog,s2:StationLog)=>{
      if (s1.duration > s2.duration) s1 else s2 },
    //(K:Key的类型, W:窗口类型, Iterable[T]:T是输入的处理数据类型, Collector[R]:R输出数据类型) => Unit)
    (k:String,w:TimeWindow,iterable:Iterable[StationLog],out:Collector[String])=>{
     
      out.collect(s"窗口起始时间:${w.getStart}-${w.getEnd},基站ID:$k ,最大时长:${iterable.iterator.next().duration}")
    }
  ).print()

env.execute()

  另外还有一种复杂的写法:

/**
  * 第二种设置方式:
  */
val ds2: DataStream[StationLog] = ds1.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks[StationLog] {
     
  //定义最大的时间时间
  var maxEventTime: Long = 0
  //定义窗口延迟触发的时间,必须是毫秒
  val t: Long = 3000

  //获取WaterMark ,每隔100ms计算一次
  override def getCurrentWatermark: Watermark = {
     
    new Watermark(maxEventTime - t)
  }

  //从时间中获取事件时间,返回当前数据的事件时间,每条数据都会计算一次,注意:EventTime必须是毫秒时间戳
  override def extractTimestamp(element: StationLog, previousElementTimestamp: Long): Long = {
     
    maxEventTime = maxEventTime.max(element.callTime)
    element.callTime
  }
})
  1. With Punctuated(间断性的) Watermark

  间断性的生成Watermark一般是基于某些事件触发Watermark的生成和发送,比如:在我们的基站数据中,有一个基站的CallTime总是没有按照顺序传入,其他基站的时间都是正常的,那我们需要对这个基站来专门生成Watermark。
  注意:ds.assignTimestampsAndWatermarks(…) 需要设置在keyBy和Window函数之前。

/**
 *间断性的WaterMark 使用
 * 案例: Flink 读取Socket数据,当sid_1 基站数据被读取到时,根据sid_1基站当前日志事件时间设置WaterMark
 */
val env = StreamExecutionEnvironment.getExecutionEnvironment
import org.apache.flink.streaming.api.scala._
//设置一个并行度
env.setParallelism(1)
//设置 waterMark 使用事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

val ds1: DataStream[StationLog] = env.socketTextStream("mynode5", 9999)
  .map(line => {
     
    val arr = line.split(",")
    StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
  })

/**
  * 间断性的设置WaterMark
  */
val ds2: DataStream[StationLog] = ds1.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks[StationLog] {
     
  //获取WaterMark ,当有一条数据时就会执行。lastElement :最近一条数据 ,extractedTimestamp :抽取到的当前事件时间
  override def checkAndGetNextWatermark(lastElement: StationLog, extractedTimestamp: Long): Watermark = {
     
    println(s"extractedTimestamp = ${extractedTimestamp} --- lastElement = $lastElement")
    var maxTime: Long = 0L //设置当前最大的事件时间
    if ("sid_1".equals(lastElement.sid)) {
     
      maxTime = maxTime.max(extractedTimestamp)
      new Watermark(maxTime - 3000) //设置WaterMark,允许数据延迟3s,窗口延迟3s触发执行
    } else {
     
      null //非sid_1 基站id,不返回Watermark
    }
  }
  //获取事件时间,每条数据都会执行
  override def extractTimestamp(element: StationLog, previousElementTimestamp: Long): Long = {
     
    element.callTime
  }
})

ds2.keyBy(_.sid).timeWindow(Time.seconds(10),Time.seconds(5))
  .reduce(
    (s1:StationLog,s2:StationLog)=>{
     if (s1.duration > s2.duration) s1 else s2},
    //(K:Key的类型, W:窗口类型, Iterable[T]:T是输入的处理数据类型, Collector[R]:R输出数据类型) => Unit)
    (k:String,window:TimeWindow,iterable:Iterable[StationLog],out:Collector[String])=>{
     
      out.collect(s"窗口起始时间:${window.getStart} - ${window.getEnd},当前基站ID:$k ,最长时长:${iterable.iterator.next().duration}")
    }
  ).print()

env.execute()

3) Watermark案例

  需求:每隔5秒中统计一下最近10秒内每个基站中通话时间最长的一次通话发生的呼叫时间、主叫号码,被叫号码,通话时长。并且还得告诉我到底是哪个时间范围(10秒)内的。
  注意:基站日志数据传入的时候是无序的,通过观察发现时间最多延迟了3秒。

/**
 * 每隔5秒中统计一下最近10秒内每个基站中通话时间最长的一次通话发生的
 * 呼叫时间、主叫号码,被叫号码,通话时长。
 * 并且还得告诉我到底是哪个时间范围(10秒)内的。
 */
object MaxLongCallTime {
     

  def main(args: Array[String]): Unit = {
     
    //初始化Flink的Streaming(流计算)上下文执行环境
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment

    streamEnv.setParallelism(1)
    streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._

    //读取文件数据
    val data = streamEnv.socketTextStream("hadoop101",8888)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })
        .assignTimestampsAndWatermarks( //引入Watermark
          new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(3)){
     //延迟3秒
          override def extractTimestamp(element: StationLog) = {
     
            element.callTime
          }
        })
    //分组,开窗处理
    data.keyBy(_.sid)
      .timeWindow(Time.seconds(10),Time.seconds(5))
      //reduce 函数做增量聚合 ,MaxTimeAggregate能做到来一条数据处理一条,
      //ReturnMaxTime 在窗口触发的时候调用
      .reduce(new MaxTimeReduce,new ReturnMaxTime)
      .print()

    streamEnv.execute()
  }
  class  MaxTimeReduce extends ReduceFunction[StationLog]{
     
    override def reduce(t: StationLog, t1: StationLog): StationLog = {
     
      //通话时间比较
      if(t.duration > t1.duration) t else t1
    }
  }
  class ReturnMaxTime extends WindowFunction[StationLog,String,String,TimeWindow]{
     
    override def apply(key: String, window: TimeWindow, input: Iterable[StationLog], out: Collector[String]): Unit = {
     
      var sb =new StringBuilder
      sb.append("窗口范围是:").append(window.getStart).append("----").append(window.getEnd)
      sb.append("\n")
      sb.append("通话日志:").append(input.iterator.next())
      out.collect(sb.toString())
    }
  }
}

3. WindowallowedLateness

  基于Event-Time的窗口处理流式数据,虽然提供了Watermark机制,却只能在一定程度上解决了数据乱序的问题。但在某些情况下数据可能延时会非常严重,即使通过Watermark机制也无法等到数据全部进入窗口再进行处理。Flink中默认会将这些迟到的数据做丢弃处理,但是有些时候用户希望即使数据延迟到达的情况下,也能够正常按照流程处理并输出结果,此时就需要使用Allowed Lateness机制来对迟到的数据进行额外的处理。
  DataStream API中提供了allowedLateness方法来指定是否对迟到的数据进行处理,在该方法中传入Time类型的时间间隔大小(t),代表允许延时的最大时间,Flink窗口计算过程中会将Window的EndTime加上该时间,作为窗口最后被触发的结束时间P。当Watermark处于窗口EndTime与当前结束时间P时,只要进来一条属于当前窗口范围内的数据就会直接触发窗口计算。如果Watermark超过了最大延时时间P,再输入属于当前Window范围内的延迟数据则该数据只能被丢弃。
通常情况下用户虽然希望对迟到的数据进行窗口计算,但并不想将结果混入正常的计算流程中,例如用户大屏数据展示系统,即使正常的窗口中没有将迟到的数据进行统计,但为了保证页面数据显示的连续性,后来接入到系统中迟到数据所统计出来的结果不希望显示在屏幕上,而是将延时数据和结果存储到数据库中,便于后期对延时数据进行分析。对于这种情况需要借助Side Output来处理,通过使用sideOutputLateData(OutputTag)来标记迟到数据计算的结果,然后使用getSideOutput(lateOutputTag)从窗口结果中获取lateOutputTag标签对应的数据,之后转成独立的DataStream数据集进行处理,创建late-data的OutputTag,再通过该标签从窗口结果中将迟到数据筛选出来。
  注意:如果有Watermark同时也有Allowed Lateness。那么窗口函数再次触发的条件是:end-of-window <= watermark <= end-of-window + allowedLateness,以上watermark位于窗口结束至窗口结束加上延时时间之内,每当有一条符合条件的迟到数据,窗口都会被触发一次。
  案例:Flink读取Sokcet基站数据,每隔5秒统计最近10秒,每个基站的呼叫数量。要求:
  1、每个基站的数据会存在乱序
  2、大多数数据延迟2秒到,但是有些数据迟到时间比较长,在处理乱序的2s基础上,再允许数据迟到3s。
  3、迟到时间很长(5s = 2s +3s)的数据不能丢弃,放入侧流

/**
  *  使用 Window的allowedLateness 实现对迟到数据的处理
  *   案例:Flink读取Sokcet基站数据,每隔5秒统计最近10秒每个基站的呼叫数量。
  *     1、每个基站的数据会存在乱序
  *     2、大多数数据延迟2秒到,但是有些数据迟到时间比较长,在处理乱序的2s基础上,再允许数据迟到3s。
  *     3、迟到时间(5s = 2s +3s)很长的的数据不能丢弃,放入侧流
  */
val env = StreamExecutionEnvironment.getExecutionEnvironment
import org.apache.flink.streaming.api.scala._
//设置一个线程
env.setParallelism(1)
//设置 waterMark 使用时间时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

val ds1: DataStream[StationLog] = env.socketTextStream("mynode5", 9999)
  .map(line => {
     
    val arr = line.split(",")
    StationLog(arr(0), arr(1), arr(2), arr(3), arr(4).toLong, arr(5).toLong)
  }).assignAscendingTimestamps(sl=>{
     sl.callTime})//设置时间EventTime

//设置Watermark
val ds2: DataStream[StationLog] = ds1.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(2)) {
     
  override def extractTimestamp(element: StationLog): Long = {
     
    element.callTime
  }
})

//定义侧流标签
val tag: OutputTag[StationLog] = new OutputTag[StationLog]("late")

//处理数据
val mainStream = ds2.keyBy(_.sid)
  .timeWindow(Time.seconds(10), Time.seconds(5))
  /**
    * 注意:只要符合watermark < end-of-window + allowedLateness之内到达的数据,都会被再次触发窗口的计算
    *       超过之外的迟到数据会被放入侧输出流
    */
  .allowedLateness(Time.seconds(3)) //允许数据延迟3秒
  .sideOutputLateData(tag)
  .aggregate(
  new AggregateFunction[StationLog, Long, Long] {
     
    override def createAccumulator(): Long = 0L

    override def add(value: StationLog, accumulator: Long): Long = accumulator + 1L

    override def getResult(accumulator: Long): Long = accumulator

    override def merge(a: Long, b: Long): Long = a + b
  },
  new WindowFunction[Long, String, String, TimeWindow] {
     
    override def apply(key: String, window: TimeWindow, input: Iterable[Long], out: Collector[String]): Unit = {
     
      out.collect(s"窗口起始时间:${window.getStart} - ${window.getEnd},基站ID:${key},通话个数:${input.last}")
    }
  }
)
//打印侧流
mainStream.getSideOutput(tag).print("迟到的数据")
mainStream.print("结果")

env.execute()

第八章 TableAPI和Flink SQL

  在Spark中有DataFrame这样的关系型编程接口,因其强大且灵活的表达能力,能够让用户通过非常丰富的接口对数据进行处理,有效降低了用户的使用成本。Flink也提供了关系型编程接口Table API以及基于Table API的SQL API,让用户能够通过使用结构化编程接口高效地构建Flink应用。同时Table API以及SQL能够统一处理批量和实时计算业务,无须切换修改任何应用代码就能够基于同一套API编写流式应用和批量应用,从而达到真正意义的批流统一。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第74张图片
  在 Flink 1.8 架构里,如果用户需要同时流计算、批处理的场景下,用户需要维护两套业务代码,开发人员也要维护两套技术栈,非常不方便。 Flink 社区很早就设想过将批数据看作一个有界流数据,将批处理看作流计算的一个特例,从而实现流批统一,阿里巴巴的 Blink 团队在这方面做了大量的工作,已经实现了 Table API & SQL 层的流批统一。阿里巴巴已经将 Blink 开源回馈给 Flink 社区。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第75张图片

1. 开发环境构建

  在 Flink 1.9 中,Table 模块迎来了核心架构的升级,引入了阿里巴巴Blink团队贡献的诸多功能,取名叫: Blink Planner。在使用Table API和SQL开发Flink应用之前,通过添加Maven的依赖配置到项目中,在本地工程中引入相应的依赖库,库中包含了Table API和SQL接口。

>
    >org.apache.flink>
    >flink-table-planner_2.11>
    >1.9.1>
>
>
    >org.apache.flink>
    >flink-table-api-scala-bridge_2.11>
    >1.9.1>
>

2. TableEnvironment

  和DataStream API一样,Table API和SQL中具有相同的基本编程模型。首先需要构建对应的TableEnviroment创建关系型编程环境,才能够在程序中使用Table API和SQL来编写应用程序,另外Table API和SQL接口可以在应用中同时使用,Flink SQL基于Apache Calcite框架实现了SQL标准协议,是构建在Table API之上的更高级接口。
  首先需要在环境中创建TableEnvironment对象,TableEnvironment中提供了注册内部表、执行Flink SQL语句、注册自定义函数等功能。根据应用类型的不同,TableEnvironment创建方式也有所不同,但是都是通过调用create()方法创建。
  流计算环境下创建TableEnviroment:

        //初始化Flink的Streaming(流计算)上下文执行环境
val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
        //初始化Table API的上下文环境
 val tableEvn =StreamTableEnvironment.create(streamEnv)

  在Flink1.9之后由于引入了 Blink Planner,还可以为:

  val bsSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
  val bsTableEnv = StreamTableEnvironment.create(streamEnv, bsSettings)

  注意:Flink社区完整保留原有 Flink Planner (Old Planner),同时又引入了新的 Blink Planner,用户可以自行选择使用 Old Planner 还是 Blink Planner。官方推荐暂时使用Old Planner。

3. Table API

  在Flink中创建一张表可以由Dataset转换成一张表(静态),也可以由DataStream转换成一张表(动态)。DataSet转换成Flink中的Table可以参照官网,这里不再详述,下面以DataStream 转换成Flink中的Table为例来解释。

1) 创建 Table

  Table API中已经提供了TableSource从外部系统获取数据,例如常见的数据库、文件系统和Kafka消息队列等外部系统。

1. 从文件中创建Table-FlieSource

  Flink允许用户从本地或者分布式文件系统中读取和写入数据,在Table API中可以通过CsvTableSource类来创建,只需指定相应的参数即可。但是文件格式必须是CSV格式的。其他文件格式也支持(在Flink还有Connector的来支持其他格式或者自定义TableSource)。

//创建CSV格式的TableSource 
val fileSource = new CsvTableSource("/station.log",
Array[String]("f1","f2","f3","f4","f5","f6"),
Array(Types.STRING,Types.STRING,Types.STRING,Types.STRING,Types.LONG,Types.LONG))
//注册Table,表名为t_log    
tableEvn.registerTableSource("t_log",fileSource)
//转换成Table对象,并打印表结构
tableEvn.scan("t_log").printSchema()

  注意:最后面不要streamEnv.execute(),否则报错。因为没有其他流计算逻辑2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第76张图片
2. 从DataStream中创建Table

  前面已经知道Table API是构建在DataStream API和DataSet API之上的一层更高级的抽象,因此用户可以灵活地使用Table API将Table转换成DataStream或DataSet数据集,也可以将DataStream或DataSet数据集转换成Table,这和Spark中的DataFrame和RDD的关系类似。

 //读取数据
    val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)
//    val data = streamEnv.socketTextStream("hadoop101",8888)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })
    //把DataStream对象变成一个Table
    tableEvn.registerDataStream("t_station_log",data) //注册表
    val table: Table = tableEvn.scan("t_station_log") 
    table.printSchema() //打印表结构
    streamEnv.execute()

  还可以用第二种,如果纯粹使用Table API推荐第二种,如果使用SQL推荐第一种:

 //读取数据
    val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)
//    val data = streamEnv.socketTextStream("hadoop101",8888)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })
    //吧DataStream对象变成一个Table
    val table: Table = tableEvn.fromDataStream(data) //直接变成table对象
    table.printSchema() //打印表结构
    streamEnv.execute()

3. 以上完整代码:

val env = StreamExecutionEnvironment.getExecutionEnvironment
//第一种方式创建TableEnv
val settings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env,settings)

//第二种方式创建TableEnv,这种方式采用Blink ,目前不完善,建议使用OldPlanner
//    val settiongs = EnvironmentSettings.newInstance().useBlinkPlanner().inStreamingMode().build()
//    val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env,settiongs)

//读取CSV文件创建Table Source ,sid_6,18112340006,19156780004,success,1598447919835,18
val tableSource: CsvTableSource = new CsvTableSource("./data/stationlog.txt",
  Array[String]("sid", "call_out", "call_in", "call_type", "call_time", "duration"),
  Array(Types.STRING, Types.STRING, Types.STRING, Types.STRING, Types.LONG, Types.LONG)
)

//注册一张 station 表
tableEnv.registerTableSource("station",tableSource)

//表 <=> Table 互转 : 将以上 station表转换成 Table对象 ,后期可以使用Table Api进行操作, 同时Table 对象也可以注册成表
//    val table1: Table = tableEnv.fromTableSource(tableSource)
val table: Table = tableEnv.scan("station")
tableEnv.registerTable("sss",table)

//打印表的schema信息
table.printSchema()

// Table <=> DataStream 互转:想要查看数据,需要将Table 对象转换成DataStream对象,然后再print ,这里需要导入隐式转换
import org.apache.flink.streaming.api.scala._
val ds: DataStream[Row] = tableEnv.toAppendStream[Row](table)
ds.print()
//dataStream 也可以转换成Table 对象 ,可以将DataStream转换成自定义对象类型的dataStream,自动转换Table时,默认列名就是属性名
val stationLogDS: DataStream[StationLog] = ds.map(row => {
     
  val sid = row.getField(0).asInstanceOf[String]
  val cout = row.getField(1).asInstanceOf[String]
  val cin = row.getField(2).asInstanceOf[String]
  val ctype = row.getField(3).asInstanceOf[String]
  val ctime = row.getField(4).asInstanceOf[Long]
  val duration = row.getField(5).asInstanceOf[Long]
  StationLog(sid, cout, cin, ctype, ctime, duration)
})
//    val table2: Table = tableEnv.fromDataStream(stationLogDS)
//也可以自定义列名 ,单引号开头,但是需要导入 隐式转换 import org.apache.flink.table.api.scala._
import org.apache.flink.table.api.scala._
//    tableEnv.registerDataStream("xxx",stationLogDS)
//    val table: Table = tableEnv.scan("xxx")
val table2: Table = tableEnv.fromDataStream(stationLogDS,'a,'b,'c,'d,'e,'f)
table2.printSchema()

//转换成DataStream后需要使用env.execute() 或者调用  tableEnv.execute() 触发执行
tableEnv.execute("FlinkTableTest")

2) 修改 Table 中字段名

  Flink支持把自定义POJOs类的所有case类的属性名字变成字段名,也可以通过基于字段偏移位置和字段名称两种方式重新修改:

//导入table库中的隐式转换
    import org.apache.flink.table.api.scala._ 
// 基于位置重新指定字段名称为"field1", "field2", "field3"
val table = tStreamEnv.fromDataStream(stream, 'field1, 'field2, 'field3

  注意:要导入隐式转换。

3) 查询和过滤

  在Table对象上使用select操作符查询需要获取的指定字段,也可以使用filter或where方法过滤字段和检索条件,将需要的数据检索出来。

object TableAPITest {
     

  def main(args: Array[String]): Unit = {
     
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    streamEnv.setParallelism(1)
    //初始化Table API的上下文环境
    val tableEvn =StreamTableEnvironment.create(streamEnv)
    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._
    import org.apache.flink.table.api.scala._
    val data = streamEnv.socketTextStream("hadoop101",8888)
          .map(line=>{
     
            var arr =line.split(",")
            new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
          })

    val table: Table = tableEvn.fromDataStream(data)
    //查询
    tableEvn.toAppendStream[Row](
      table.select('sid,'callType as 'type,'callTime,'callOut))
      .print()
    //过滤查询
    tableEvn.toAppendStream[Row](
      table.filter('callType==="success") //filter
        .where('callType==="success"))    //where
      .print()
    tableEvn.execute("sql")
  }

  其中toAppendStream函数是吧Table对象转换成DataStream对象。

4) 分组聚合

  举例:我们统计每个基站的日志数量。

 val table: Table = tableEvn.fromDataStream(data)
    tableEvn.toRetractStream[Row](
      table.groupBy('sid).select('sid, 'sid.count as 'logCount))
      .filter(_._1==true) //返回的如果是true才是Insert的数据
      .print()

  在流式计算中Table的数据是不断动态更新的,在代码中可以看出,使用toAppendStream和toRetractStream方法将Table转换为DataStream[T]数据集,T可以是Flink自定义的数据格式类型Row,也可以是用户指定的数据格式类型。Append Model采用追加方式仅将insert更新变化的数据写入到DataStream中。Retract model 是一种更高级模式,在使用toRetractStream方法时,返回的数据类型结果为DataStream[(Boolean,T)],Boolean类型代表数据更新类型,True对应INSERT操作更新的数据,False对应DELETE操作更新的数据。Retract model 在实际应用情况下更常用。

5) UDF自定义的函数

  用户可以在Table API中自定义函数类,常见的抽象类和接口是:

  • ScalarFunction
  • TableFunction
  • AggregateFunction
  • TableAggregateFunction

  案例:使用Table完成基于流的WordCount

/**
  * Flink 读取Socket数据,统计workcount
  */
object TableAPIWordCountTest {
     
  def main(args: Array[String]): Unit = {
     
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    val settings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
    val tableEnv = StreamTableEnvironment.create(env,settings)

    import org.apache.flink.streaming.api.scala._
    import org.apache.flink.table.api.scala._
    //读取Socket数据加载Table
    val table: Table = tableEnv.fromDataStream(env.socketTextStream("mynode5",9999),'line)

    //创建自定义函数
    val myfun = new MyFun()

    //使用自定义函数处理数据
    val t1: Table = table.flatMap(myfun('line)).as('word,'cnt)
    t1.printSchema() //打印Schema

    //分组,聚合统计单词个数
    val t2: Table = t1.groupBy('word).select('word,'cnt.sum as 'totalcnt)

    //转换DataStream 打印结果
    val ds: DataStream[(Boolean, Row)] = tableEnv.toRetractStream[Row](t2)
    ds.filter(_._1.equals(true)).print()

    tableEnv.execute("tableapi")

  }
}

//注意返回的Row类型也可以是一个对象,自动会把对象中的属性当做列处理,如果返回对象可以不指定对应的getResultType类型
class MyFun extends TableFunction[Row]{
     
  //函数处理主体,名称为eval
  def eval(line:String) :Unit = {
     
    line.split(" ").foreach(word=>{
     
      val row = new Row(2)
      row.setField(0,word)
      row.setField(1,1)
      collect(row)
    })
  }
  //定义返回数据类型
  override def getResultType: TypeInformation[Row] = Types.ROW(Types.STRING,Types.INT)
}

6) Window

  Flink支持ProcessTime、EventTime和IngestionTime三种时间概念,针对每种时间概念,Flink Table API中使用Schema中单独的字段来表示时间属性,当时间字段被指定后,就可以在基于时间的操作算子中使用相应的时间属性。
  在Table API中通过使用.rowtime来定义EventTime字段,在ProcessTime时间字段名后使用.proctime后缀来指定ProcessTime时间属性
  案例:统计最近5秒钟,每个基站的呼叫数量

 object TableAPITest {
     

  def main(args: Array[String]): Unit = {
     
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //指定EventTime为时间语义
    streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    streamEnv.setParallelism(1)
    //初始化Table API的上下文环境
    val tableEvn =StreamTableEnvironment.create(streamEnv)
    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._
    import org.apache.flink.table.api.scala._

    val data = streamEnv.socketTextStream("hadoop101",8888)
          .map(line=>{
     
            var arr =line.split(",")
            new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
          })
      .assignTimestampsAndWatermarks( //引入Watermark
        new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(2)){
     //延迟2秒
          override def extractTimestamp(element: StationLog) = {
     
            element.callTime
          }
        })

    //设置时间属性
    val table: Table = tableEvn.fromDataStream(data,'sid,'callOut,'callIn,'callType,'callTime.rowtime)
    //滚动Window ,第一种写法
    val result: Table = table.window(Tumble over 5.second on 'callTime as 'window)
    //第二种写法
    val result: Table = table.window(Tumble.over("5.second").on("callTime").as("window"))
      .groupBy('window, 'sid)
      .select('sid, 'window.start, 'window.end, 'window.rowtime, 'sid.count)
    //打印结果
    tableEvn.toRetractStream[Row](result)
      .filter(_._1==true)
      .print()
  

    tableEvn.execute("sql")
  }
}

  上面的案例是滚动窗口,如果是滑动窗口也是一样,代码如下

//滑动窗口,窗口大小为:10秒,滑动步长为5秒 :第一种写法
table.window(Slide over 10.second every 5.second on 'callTime as 'window)
//滑动窗口第二种写法 
table.window(Slide.over("10.second").every("5.second").on("callTime").as("window"))

4. Flink 的 SQL 使用

  SQL作为Flink中提供的接口之一,占据着非常重要的地位,主要是因为SQL具有灵活和丰富的语法,能够应用于大部分的计算场景。Flink SQL底层使用Apache Calcite框架,将标准的Flink SQL语句解析并转换成底层的算子处理逻辑,并在转换过程中基于语法规则层面进行性能优化,比如谓词下推等。另外用户在使用SQL编写Flink应用时,能够屏蔽底层技术细节,能够更加方便且高效地通过SQL语句来构建Flink应用。Flink SQL构建在Table API之上,并含盖了大部分的Table API功能特性。同时Flink SQL可以和Table API混用,Flink最终会在整体上将代码合并在同一套代码逻辑中

1) 执行 SQL

  以下通过实例来了解 Flink SQL 整体的使用方式。
  案例:统计每个基站通话成功的通话时长总和。

//读取数据
val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })
    val table: Table = tableEvn.fromDataStream(data)
    //执行sql
    val result: Table = tableEvn.sqlQuery(s"select sid,sum(duration) as sd from $table where callType='success' group by sid")
    //打印结果
    tableEvn.toRetractStream[Row](result)
      .filter(_._1==true)
      .print()

    tableEvn.execute("sql_api")

另外可以有第二种写法:

 //第二种sql调用方式
    tableEvn.registerDataStream("t_station_log",data)
    val result: Table = tableEvn.sqlQuery("select sid ,sum(duration) as sd from t_station_log where callType='success' group by sid")
    tableEvn.toRetractStream[Row](result)
      .filter(_._1==true)
      .print()

2) SQL 中的 Window

  Flink SQL也支持三种窗口类型,分别为Tumble Windows、HOP Windows和Session Windows,其中HOP Windows对应Table API中的Sliding Window,同时每种窗口分别有相应的使用场景和方法。
  案例:统计最近每5秒中内,每个基站的通话成功时间总和:

 object TestSQL {
     

  def main(args: Array[String]): Unit = {
     
    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    //指定EventTime为时间语义
    streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    streamEnv.setParallelism(1)
    //初始化Table API的上下文环境
    val tableEvn =StreamTableEnvironment.create(streamEnv)
    //导入隐式转换,建议写在这里,可以防止IDEA代码提示出错的问题
    import org.apache.flink.streaming.api.scala._
    import org.apache.flink.table.api.scala._

//    val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)
    val data = streamEnv.socketTextStream("hadoop101",8888)
      .map(line=>{
     
        var arr =line.split(",")
        new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.toLong)
      })
      .assignTimestampsAndWatermarks( //引入Watermark
        new BoundedOutOfOrdernessTimestampExtractor[StationLog](Time.seconds(2)){
     //延迟2秒
          override def extractTimestamp(element: StationLog) = {
     
            element.callTime
          }
        })

    //滚动窗口,窗口大小为5秒,需求:统计每5秒内,每个基站的成功通话时长总和 tableEvn.registerDataStream("t_station_log",data,'sid,'callOut,'callIn,'callType,'callTime.rowtime,'duration)
var result =tableEvn.sqlQuery(
"select sid ,sum(duration) from t_station_log where callType='success' group by tumble(callTime,interval '5' second),sid"
)
    tableEvn.toRetractStream[Row](result)
      .filter(_._1==true)
      .print()
    tableEvn.execute("sql_api")
  }
}

  如果是滑动窗口的话:需求:每隔5秒钟,统计最近10秒内每个基站的通话成功时间总和。

 //滑动窗口,窗口大小10秒,步长5秒,需求:每隔5秒,统计最近10秒内,每个基站通话成功时长总和
    tableEvn.registerDataStream("t_station_log",data,'sid,'callType,'callTime.rowtime,'duration)
var result =tableEvn.sqlQuery(
      "select sid ,sum(duration) , hop_start(callTime,interval '5' second,interval '10' second) as winStart," +
        "hop_end(callTime,interval '5' second,interval '10' second) as winEnd " +
        "from t_station_log where callType='success' " +
        "group by hop(callTime,interval '5' second,interval '10' second),sid")
    tableEvn.toRetractStream[Row](result) //打印每个窗口的起始时间
      .filter(_._1==true)
      .print()
    tableEvn.execute("sql_api")

第九章 Flink 的复杂事件处理 CEP

  复杂事件处理(CEP)是一种基于流处理的技术,将系统数据看作不同类型的事件,通过分析事件之间的关系,建立不同的事件关系序列库,并利用过滤、关联、聚合等技术,最终由简单事件产生高级事件,并通过模式规则的方式对重要信息进行跟踪和分析,从实时数据中发掘有价值的信息。复杂事件处理主要应用于防范网络欺诈、设备故障检测、风险规避和智能营销等领域。Flink基于DataStrem API提供了FlinkCEP组件栈,专门用于对复杂事件的处理,帮助用户从流式数据中发掘有价值的信息。

1. CEP 相关概念

1) 配置依赖

  在使用FlinkCEP组件之前,需要将FlinkCEP的依赖库引入项目工程中。

 >
  >org.apache.flink>
  >flink-cep-scala_2.11>
  >1.9.1>
>

2) 事件定义

  • 简单事件:简单事件存在于现实场景中,主要的特点为处理单一事件,事件的定义可以直接观察出来,处理过程中无须关注多个事件之间的关系,能够通过简单的数据处理手段将结果计算出来。
  • 复杂事件:相对于简单事件,复杂事件处理的不仅是单一的事件,也处理由多个事件组成的复合事件。复杂事件处理监测分析事件流(Event Streaming),当特定事件发生时来触发某些动作。
    复杂事件中事件与事件之间包含多种类型关系,常见的有时序关系、聚合关系、层次关系、依赖关系及因果关系等。

2. Pattern API

  FlinkCEP中提供了Pattern API用于对输入流数据的复杂事件规则定义,并从事件流中抽取事件结果。包含四个步骤:

  • 输入事件流的创建
  • Pattern的定义
  • Pattern应用在事件流上检测
  • 选取结果

1) 模式定义

  定义Pattern可以是单次执行模式,也可以是循环执行模式。单次执行模式一次只接受一个事件,循环执行模式可以接收一个或者多个事件。通常情况下,可以通过指定循环次数将单次执行模式变为循环执行模式。每种模式能够将多个条件组合应用到同一事件之上,条件组合可以通过where方法进行叠加。
  每个Pattern都是通过begin方法定义的:

 val start = Pattern.begin[Event]("start_pattern")

  下一步通过Pattern.where()方法在Pattern上指定Condition(条件),只有当条件满足之后,当前的Pattern才会接受事件。

start.where(_.getCallType.equles("success"))

  1.设置循环次数

  对于已经创建好的Pattern,可以指定循环次数,形成循环执行的Pattern.。

  • times:可以通过times指定固定的循环执行次数。
/**
* 举例:事件流:a1,b1,a2,b2,a3,a4,a5,a6,a7... ....(a1代表第一次出现a,其他以此类
*       推,a5第5次出现事件a)
*  假设start.where(匹配a事件).times(4) ,匹配结果如下:
*   {a1,a2,a3,a4},{a2,a3,a4,a5},{a3,a4,a5,a6}
*  假设start.where(匹配a事件).times(2,4) ,匹配结果如下:
*   {a1,a2},{a1,a2,a3},{a2,a3},{a1,a2,a3,a4},{a2,a3,a4},{a3,a4}... ...
*/
//当where条件满足指定次数后,循环触发,连续出现4次匹配条件就触发,中间可以有其他事件
start.times(4);
//可以执行触发次数范围,让循环执行次数在该范围之内,中间可以有其他事件
start.times(2, 4);
  • optional:也可以通过optional关键字指定要么不触发要么触发指定的次数。需要与多个模式组合时,才有意义。
start.times(4).optional;//需要多个模式组合时,才有意义
start.times(2, 4).optional; //需要多个模式组合时,才有意义
  • greedy:可以通过greedy将Pattern标记为贪婪模式,在Pattern匹配成功的前提下,会尽可能多地触发。需要与多个模式组合时才有意义。
//触发2、3、4次,尽可能重复执行
start.times(2, 4).greedy; //需要与 oneOrMore 一起使用,才有效果。单独使用没意义
//触发0、2、3、4次,尽可能重复执行
start.times(2, 4).optional.greedy;//需要多个条件组合并且与 oneOrMore 一起使用,才有效果。单独使用没意义
  • oneOrMore:可以通过oneOrMore方法指定触发一次或多次。
// 触发一次或者多次
start.oneOrMore();
//触发一次或者多次,尽可能重复执行
start.oneOrMore().greedy();
// 触发0次或者多次
start.oneOrMore().optional();
// 触发0次或者多次,尽可能重复执行
start.oneOrMore().optional().greedy();
  • timesOrMore:通过timesOrMore方法可以指定触发固定次数以上,例如执行两次以上。
// 触发两次或者多次
start.timesOrMore(2);
// 触发两次或者多次,尽可能重复执行
start.timesOrMore(2).greedy();
// 不触发或者触发两次以上,尽可能重复执行
start.timesOrMore(2).optional().greedy();

  2.定义条件

  每个模式都需要指定触发条件,作为事件进入到该模式是否接受的判断依据,当事件中的数值满足了条件时,便进行下一步操作。在FlinkCFP中通过pattern.where()、pattern.or()及pattern.until()方法来为Pattern指定条件,且Pattern条件有Simple Conditions及Combining Conditions等类型。

  • 简单条件:Simple Condition继承于Iterative Condition类,其主要根据事件中的字段信息进行判断,决定是否接受该事件。
// 把通话成功的事件挑选出来
start.where(_.getCallType == "success")
  • 组合条件:组合条件是将简单条件进行合并,通常情况下也可以使用where方法进行条件的组合,默认每个条件通过AND逻辑相连。如果需要使用OR逻辑,直接使用or方法连接条件即可。
// 把通话成功,或者通话时长大于10秒的事件挑选出来
val start = Pattern.begin[StationLog]("start_pattern")
.where(_.callType=="success")
.or(_.duration>10)
  • 终止条件:如果程序中使用了oneOrMore或者oneOrMore().optional()方法,则必须指定终止条件,否则模式中的规则会一直循环下去,如下终止条件通过until()方法指定。
pattern.oneOrMore.until(_.callOut.startsWith("186"))

  3.模式序列

  将相互独立的模式进行组合然后形成模式序列。模式序列基本的编写方式和独立模式一致,各个模式之间通过邻近条件进行连接即可,其中有严格邻近、宽松邻近、非确定宽松邻近三种邻近连接条件。

  假设有数据流F:a,c,b1,b2… b1代表第一次出现b,b2代表第二次出现b

  • 严格邻近:严格邻近条件中,需要所有的事件都按照顺序满足模式条件,不允许忽略任意不满足的模式。

举例:从事件流F中匹配ab事件组,则匹配结果为空。

val strict: Pattern[Event] = start.next("middle").where(...)
  • 宽松邻近:在宽松邻近条件下,会忽略没有成功匹配模式条件,并不会像严格邻近要求得那么高,可以简单理解为OR的逻辑关系,忽略不匹配的事件直到下一个匹配出现为止。

  举例:从事件流F中匹配ab事件组,则匹配结果为:{a,b1}

val relaxed: Pattern[Event, _] = start.followedBy("middle").where(...)
  • 非确定宽松邻近:和宽松邻近条件相比,非确定宽松邻近条件指在模式匹配过程中可以忽略已经匹配的条件。

  举例:从事件流F中匹配ab事件组,则匹配结果为:{a,b1},{a,b2}

val nonDetermin: Pattern[Event, _] = start.followedByAny("middle").where(...)
  • 除以上模式序列外,还可以定义“不希望出现某种近邻关系”:
    .notNext() —— 不想让某个事件严格紧邻前一个事件发生
    .notFollowedBy() —— 不想让某个事件在两个事件之间发生,后面还需要有模式才可使用,即:一个模式不能以notFollowedBy()模式结束。

 注意:

 1、所有模式序列必须以 .begin() 开始
 2、模式序列不能以 .notFollowedBy() 结束
 3、“not” 类型的模式不能被 optional 所修饰
 4、此外,还可以为模式指定时间约束,用来要求在多长时间内匹配有效

//指定模式在10秒内有效
pattern.within(Time.seconds(10));

2) 模式检测

  调用 CEP.pattern(),给定输入流和模式,就能得到一个 PatternStream

//cep 做模式检测
val patternStream = CEP.pattern[EventLog](dataStream.keyBy(_.id),pattern)

3) 选择结果

  得到PatternStream类型的数据集后,接下来数据获取都基于PatternStream进行。该数据集中包含了所有的匹配事件。目前在FlinkCEP中提供select和flatSelect两种方法从PatternStream提取事件结果事件。

  1.通过Select Funciton抽取正常事件

  可以通过在PatternStream的Select方法中传入自定义Select Funciton完成对匹配事件的转换与输出。其中Select Funciton的输入参数为Map[String, Iterable[IN]],Map中的key为模式序列中的Pattern名称,Value为对应Pattern所接受的事件集合,格式为输入事件的数据类型。

def selectFunction(pattern : Map[String, Iterable[IN]]): T= {
     
  //获取pattern中的startEvent
  val startEvent = pattern.get("start_pattern").get.next
    //获取Pattern中middleEvent
    val middleEvent = pattern.get("middle").get.next
//返回结果
T(startEvent, middleEvent)
}

  2.通过Flat Select Funciton抽取正常事件

  Flat Select Funciton和Select Function相似,不过Flat Select Funciton在每次调用可以返回任意数量的结果。因为Flat Select Funciton使用Collector作为返回结果的容器,可以将需要输出的事件都放置在Collector中返回。

def flatSelectFn(pattern : Map[String, Iterable[IN]], collector : 
Collector[T]) = {
     
    //获取pattern中startEvent
  val startEvent = pattern.get("start_pattern").get.next
    //获取Pattern中middleEvent
  val middleEvent = pattern.get("middle").get.next
    //并根据startEvent的Value数量进行返回
  for (i <- 0 to startEvent.getValue) {
     
    collector.collect(T(startEvent, middleEvent))
  }}

  3.通过Select Funciton抽取超时事件

如果模式中有within(time)【注意:这里不是指的是watermark迟到的数据超时,因为这里没有窗口】,那么就很有可能有超时的数据存在,通过PatternStream. Select方法分别获取超时事件和正常事件。首先需要创建OutputTag来标记超时事件,然后在PatternStream.select方法中使用OutputTag,就可以将超时事件从PatternStream中抽取出来。

// 通过CEP.pattern方法创建PatternStream
  val patternStream: PatternStream[Event] = CEP.pattern(input, pattern)
  //创建OutputTag,并命名为timeout-output
  val timeoutTag = OutputTag[String]("timeout-output")
  //调用PatternStream select()并指定timeoutTag
  val result: SingleOutputStreamOperator[NormalEvent] = 
  patternStream.select(timeoutTag){
     
  //超时事件获取
    (pattern: Map[String, Iterable[Event]], timestamp: Long) => 
      TimeoutEvent()//返回异常事件
  } {
     
  //正常事件获取
pattern: Map[String, Iterable[Event]] => 
      NormalEvent()//返回正常事件
  }
//调用getSideOutput方法,并指定timeoutTag将超时事件输出
val timeoutResult: DataStream[TimeoutEvent] = result.getSideOutput(timeoutTag)

4) 案例

  1. 测试greedy:

/**
  * Greedy : Flink CEP贪婪模式,需要在多个事件中使用
  * 如果start 模式中不加上greedy ,结果如下:
  *     start : aa1,aa2,	,middle : aa3,
  *     start : aa2,aa3,	,middle : aa4,
  *     start : aa1,aa2,aa3,	,middle : aa4,
  *     start : aa2,aa3,aa4,	,middle : bbb,
  *     start : aa3,aa4,	,middle : bbb,
  * start 模式中加上greedy,结果如下:
  *     start : aa2,aa3,aa4,	,middle : bbb,
  *     start : aa3,aa4,	,middle : bbb,
  */
object GreedyTest {
     
  def main(args: Array[String]): Unit = {
     
    val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    import org.apache.flink.streaming.api.scala._
    val ds1: DataStream[String] = env.fromCollection(Array[String](
      "aa1",
      "aa2",
      "aa3",
      "aa4",
      "aa5",
      "bbb"
    ))

    //设置模式,start:判断以“a”开头,出现2-3次,middle : 判断长度是否为3,出现2-3次
    val pattern: Pattern[String, String] = Pattern.begin[String]("start").where(s => {
     
      s.startsWith("a")
    }).times(2, 3)
     /**
      *  注意:加上greedy:就是尽可能多的往后匹配start事件【这里尽可能多不是说组合多】,与middle满足 pattern
       *
      */
      .greedy
      .next("middle").where(s => {
     
      s.length == 3
    })

    //模式匹配流
    val ps: PatternStream[String] = CEP.pattern(ds1,pattern)

    //选择结果
    val result: DataStream[String] = ps.select(new PatternSelectFunction[String, String] {
     
      override def select(map: util.Map[String, util.List[String]]): String = {
     
        import scala.collection.JavaConverters._
        var returnStr = ""
        val startList: util.List[String] = map.get("start")
        if (startList != null) {
     
          returnStr += "start : "
          startList.asScala.toList.foreach(s => {
     
            returnStr += s + ","
          })
        }

        val middleList: util.List[String] = map.get("middle")
        if (middleList != null) {
     
          returnStr += "\t,middle : "
          middleList.asScala.toList.foreach(s => {
     
            returnStr += s + ","
          })
        }
        returnStr
      }
    })
    result.print()

    env.execute()
  }

}

  2. 需求:从一堆的登录日志中,匹配一个恶意登录的模式(如果一个用户连续失败三次,则是恶意登录),从而找到哪些用户名是恶意登录。

  /**
 * 登录告警系统
 * 从一堆的登录日志中,匹配一个恶意登录的模式(如果一个用户连续失败三次,则是恶意登录),从而找到哪些用户名是恶意 登录
 */

case class EventLog(id:Long,userName:String,eventType:String,eventTime:Long)

object TestCepDemo {
     

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

    val streamEnv: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    streamEnv.setParallelism(1)

    import org.apache.flink.streaming.api.scala._
    streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
    val stream: DataStream[EventLog] = streamEnv.fromCollection(List(
      new EventLog(1, "张三", "fail", 1574840003),
      new EventLog(1, "张三", "fail", 1574840004),
      new EventLog(1, "张三", "fail", 1574840005),
      new EventLog(2, "李四", "fail", 1574840006),
      new EventLog(2, "李四", "sucess", 1574840007),
      new EventLog(1, "张三", "fail", 1574840008)
    )).assignAscendingTimestamps(_.eventTime * 1000)
    stream.print("input data")
    //定义模式
    val pattern: Pattern[EventLog, EventLog] = Pattern.begin[EventLog]("begin").where(_.eventType.equals("fail"))
      .next("next1").where(_.eventType.equals("fail"))
      .next("next2").where(_.eventType.equals("fail"))
      .within(Time.seconds(10))

    //cep 做模式检测
    val patternStream: PatternStream[EventLog] = CEP.pattern[EventLog](stream.keyBy(_.id),pattern)

    //第三步: 输出alert

    val result: DataStream[String] = patternStream.select(new PatternSelectFunction[EventLog, String] {
     
      override def select(map: util.Map[String, util.List[EventLog]]) = {
     
        val iter: util.Iterator[String] = map.keySet().iterator()
        val e1: EventLog = map.get(iter.next()).iterator().next()
        val e2: EventLog = map.get(iter.next()).iterator().next()
        val e3: EventLog = map.get(iter.next()).iterator().next()

        "id:" + e1.id + " 用户名:" + e1.userName + "登录的时间:" + e1.eventTime + ":" + e2.eventTime + ":" + e3.eventTime
      }
    })

    result.print(" main ")

    streamEnv.execute()
  }
}

  3. 案例分析:读取订单数据,如果用户下单后,15分钟内付款完成,则返回付款成功代发货信息,如果用户15分钟后再付款,则返回订单超时信息。

/**
  * 读取order.log 订单数据,
  *   需求:如果用户自从下订单到付款如果在15分钟内则返回待发货信息,如果在15分钟后支付订单则返回订单超时
  */
case class OrderInfo(uid:String,payType:String,orderid:String,time:Long)

object CepExample2 {
     
  def main(args: Array[String]): Unit = {
     
    val env = StreamExecutionEnvironment.getExecutionEnvironment

    //导入隐式转换
    import org.apache.flink.streaming.api.scala._

    //设置时间语义
    env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

    //1.读取文件 ,创建事件流
    val ds: KeyedStream[OrderInfo, String] = env.readTextFile("./data/order.log")
      .map(line => {
     
        val arr = line.split(",")
        OrderInfo(arr(0), arr(1), arr(2), arr(3).toLong)
      }).assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[OrderInfo](Time.seconds(2)) {
     
      override def extractTimestamp(element: OrderInfo): Long = element.time * 1000L
    }).keyBy(_.uid)

    //2.设置模式匹配
    val pattern: Pattern[OrderInfo, OrderInfo] = Pattern.begin[OrderInfo]("create").where(_.payType.equals("create"))
      .followedBy("pay").where(_.payType.equals("pay"))
      .within(Time.minutes(15))


    //3.事件流检测
    val patternStream: PatternStream[OrderInfo] = CEP.pattern(ds,pattern)

    val outPutTag = new OutputTag[String]("timeout")

    //4.获取结果
    val result: DataStream[String] = patternStream.select(outPutTag)(
      //超时事件,map[模式名称,匹配内容] ,time:超时的时间截止点
      (map: Map[String, Iterable[OrderInfo]], time: Long) => {
     
        val pay = map.keys.last
        val orderInfo = map.get(pay).get.last
        s"【支付超时】 - 支付类型:${pay},信息:${orderInfo},超时时间点:${time}"
      }
    )(
      (map: Map[String, Iterable[OrderInfo]]) => {
     
        val createInfo = map.get("create").get.last
        val payInfo = map.get("pay").get.last
        s"【支付成功,待发货】 - 创建订单 : ${createInfo} ,支付:${payInfo}"
      }
    )
    //获取侧流 - 超时时间
    result.getSideOutput(outPutTag).print("超时订单")

    result.print("正常订单")

    //触发执行
    env.execute()

  }

}

第十章 Flink 性能优化

  对于构建好的Flink集群,如何能够有效地进行集群以及任务方面的监控与优化是非常重要的,尤其对于7*24小时运行的生产环境。重点介绍Checkpointing的监控。然后通过分析各种监控指标帮助用户更好地对Flink应用进行性能优化,以提高Flink任务执行的数据处理性能和效率。

1. Checkpoint 页面监控与优化

  Flink Web页面中也提供了针对Job Checkpointing相关的监控信息,Checkpointing监控页面中共有Overview、History、Summary和Configuration四个页签,分别对Checkpointing从不同的角度进行了监控,每个页面中都包含了与Checkpointing相关的指标。

1) Overview页签

  Overview页签中宏观地记录了Flink应用中Checkpoints的数量以及Checkpoint的最新记录,包括失败和完成的Checkpoints记录。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第77张图片

  • Checkpoint Counts:包含了触发、进行中、完成、失败、重置等Checkpoint状态数量统计。
  • Latest Completed Checkpoint:记录了最近一次完成的Checkpoint信息,包括结束时间,端到端时长,状态大小等。
  • Latest Failed Checkpoint:记录了最近一次失败的Checkpoint信息。
  • Latest Savepoint:记录了最近一次Savepoint触发的信息。
  • Latest Restore:记录了最近一次重置操作的信息,包括从Checkpoint和Savepoint两种数据中重置恢复任务。

2) Configuration页签

  Configuration页签中包含Checkpoints中所有的基本配置,具体的配置解释如下:

  • Checkpointing Mode:标记Checkpointing是Exactly Once还是At Least Once的模式。
  • Interval: Checkpointing触发的时间间隔,时间间隔越小意味着越频繁的Checkpointing。
  • Timeout: Checkpointing触发超时时间,超过指定时间JobManager会取消当次Checkpointing,并重新启动新的Checkpointing。
  • Minimum Pause Between Checkpoints:配置两个Checkpoints之间最短时间间隔,当上一次Checkpointing结束后,需要等待该时间间隔才能触发下一次Checkpoints,避免触发过多的Checkpoints导致系统资源被消耗。
  • Persist Checkpoints Externally:如果开启Checkpoints,数据将同时写到外部持久化存储中。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第78张图片

2. Flink 内存优化

  在大数据领域,大多数开源框架(Hadoop、Spark、Storm)都是基于JVM运行,但是JVM的内存管理机制往往存在着诸多类似OutOfMemoryError的问题,主要是因为创建过多的对象实例而超过JVM的最大堆内存限制,却没有被有效回收掉,这在很大程度上影响了系统的稳定性,尤其对于大数据应用,面对大量的数据对象产生,仅仅靠JVM所提供的各种垃圾回收机制很难解决内存溢出的问题。在开源框架中有很多框架都实现了自己的内存管理,例如Apache Spark的Tungsten项目,在一定程度上减轻了框架对JVM垃圾回收机制的依赖,从而更好地使用JVM来处理大规模数据集。
  Flink也基于JVM实现了自己的内存管理,将JVM根据内存区分为Unmanned Heap、Flink Managed Heap、Network Buffers三个区域。在Flink内部对Flink Managed Heap进行管理,在启动集群的过程中直接将堆内存初始化成Memory Pages Pool,也就是将内存全部以二进制数组的方式占用,形成虚拟内存使用空间。新创建的对象都是以序列化成二进制数据的方式存储在内存页面池中,当完成计算后数据对象Flink就会将Page置空,而不是通过JVM进行垃圾回收,保证数据对象的创建永远不会超过JVM堆内存大小,也有效地避免了因为频繁GC导致的系统稳定性问题。2021-03-08~09~10~11~12 大数据课程笔记 day47day48day49day50day51_第79张图片
1) JobManager配置

  JobManager在Flink系统中主要承担管理集群资源、接收任务、调度Task、收集任务状态以及管理TaskManager的功能,JobManager本身并不直接参与数据的计算过程中,因此JobManager的内存配置项不是特别多,只要指定JobManager堆内存大小即可。

jobmanager.heap.size:设定JobManager堆内存大小,默认为1024MB。

2) TaskManager配置

  TaskManager作为Flink集群中的工作节点,所有任务的计算逻辑均执行在TaskManager之上,因此对TaskManager内存配置显得尤为重要,可以通过以下参数配置对TaskManager进行优化和调整。对应的官方文档url是:
  https://ci.apache.org/projects/flink/flink-docs-release-1.9/zh/ops/config.html#configuring-the-network-buffers

  • taskmanager.heap.size:设定TaskManager堆内存大小,默认值为1024M,如果在Yarn的集群中,TaskManager取决于Yarn分配给TaskManager Container的内存大小,且Yarn环境下一般会减掉一部分内存用于Container的容错。
  • taskmanager.jvm-exit-on-oom:设定TaskManager是否会因为JVM发生内存溢出而停止,默认为false,当TaskManager发生内存溢出时,也不会导致TaskManager停止。
  • taskmanager.memory.size:设定TaskManager管理内存大小,默认为0,如果不设定该值将会使用taskmanager.memory.fraction作为内存分配依据。
  • taskmanager.memory.fraction:设定TaskManager堆中去除Network Buffers内存后的内存分配比例,默认0.7。该内存主要用于TaskManager任务排序、缓存中间结果等操作。例如,如果设定为0.8,则代表TaskManager保留80%内存用于中间结果数据的缓存,剩下20%的内存用于创建用户定义函数中的数据对象存储。注意,该参数只有在taskmanager.memory.size不设定的情况下才生效。
  • taskmanager.memory.off-heap:设置是否开启堆外内存供Managed Memory或者Network Buffers使用,默认false。
  • taskmanager.numberOfTaskSlots:每个TaskManager分配的slot数量。

3. Flink 的网络缓存优化

  Flink将JVM堆内存切分为三个部分,其中一部分为Network Buffers内存。Network Buffers内存是Flink数据交互层的关键内存资源,主要目的是缓存分布式数据处理过程中的输入数据。通常情况下,比较大的Network Buffers意味着更高的吞吐量。如果系统出现“Insufficient number of network buffers”的错误,一般是因为Network Buffers配置过低导致,因此,在这种情况下需要适当调整TaskManager上Network Buffers的内存大小,以使得系统能够达到相对较高的吞吐量。
  目前Flink能够调整Network Buffer内存大小的方式有两种:一种是通过直接指定Network Buffers内存数量的方式,另外一种是通过配置内存比例的方式。

1) 设定 Network Buffer 内存数量(过时了)

  直接设定Nework Buffer数量需要通过如下公式计算得出:
  NetworkBuffersNum = total-degree-of-parallelism * intra-node-parallelism * n
  其中total-degree-of-parallelism表示每个TaskManager的总并发数量,intra-node-parallelism表示每个TaskManager输入数据源的并发数量,n表示在预估计算过程中Repar-titioning或Broadcasting操作并行的数量。intra-node-parallelism通常情况下与Task-Manager的所占有的CPU数一致,且Repartitioning和Broadcating一般下不会超过4个并发。可以将计算公式转化如下:
  NetworkBuffersNum = ^2 * < TMs>* 4
  其中slots-per-TM是每个TaskManager上分配的slots数量,TMs是TaskManager的总数量。对于一个含有20个TaskManager,每个TaskManager含有8个Slot的集群来说,总共需要的Network Buffer数量为8^2*20*4=5120个,因此集群中配置Network Buffer内存的大小约为160M较为合适。
  计算完Network Buffer数量后,可以通过添加如下两个参数对Network Buffer内存进行配置。其中segment-size为每个Network Buffer的内存大小,默认为32KB,一般不需要修改,通过设定numberOfBuffers参数以达到计算出的内存大小要求。

  • taskmanager.network.numberOfBuffers:指定Network堆栈Buffer内存块的数量。
  • taskmanager.memory.segment-size.:内存管理器和Network栈使用的内存Buffer大小,默认为32KB。

2) 设定 Network 内存比例(推荐)

  从1.3版本开始,Flink就提供了通过指定内存比例的方式设置Network Buffer内存大小。

  • taskmanager.network.memory.fraction: JVM中用于Network Buffers的内存比例。默认0.1。
  • taskmanager.network.memory.min: 最小的Network Buffers内存大小,默认为64MB。
  • taskmanager.network.memory.max: 最大的Network Buffers内存大小,默认1GB。
  • taskmanager.memory.segment-size: 内存管理器和Network栈使用的Buffer大小,默认为32KB。

你可能感兴趣的:(西行日记,lamp,scipy,zk,makefile,crm)