【Flink】概念、入门、部署(yarn和standalone模式)、架构(组件和运行流程)、批处理、流处理API、window、时间语义、Wartermark、ProcessFunction、状态编程、Table API和SQL、CEP、面试题

一、Flink简介

1、概述

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

对无界和有界数据流进行有状态计算

2、重要特点

(1)事件驱动型:从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作(对比SparkStreaming微批次)

(2)流处理(无界、实时)与批处理(spark)

flink数据流分为无界数据流(按事件发生顺序获取)和有界数据流(对数据排序,也被称为批处理)

(3)分层API

有状态流通过过程函数(ProcessFunction)被嵌入到DataStreamAPI中

DataStreamAPI(有界或无界流数据)为数据处理提供了通用的构建模块,比如由用户定义的多种形式的转换(transformations),连接(joins),聚合(aggregations),窗口操作(windows)等等。

TableAPI是以表为中心的声明式编程,提供可比较的操作,执行之前会经过内置优化器进行优化

主要学习DataStreamAPI和Flink CEP(复杂事件处理),其他模块还不完善

二、快速上手

1、搭建maven工程FlinkTutorial

pom导包:flink-scala_2.11、flink-streaming-scala_2.11

添加Scala文件夹

2、批处理wordcount--DataSet

   val env = ExecutionEnvironment.getExecutionEnvironment
    // 从文件中读取数据
    val inputPath = "D:\\Projects\\BigData\\TestWC1\\src\\main\\resources\\hello.txt"
    val inputDS: DataSet[String] = env.readTextFile(inputPath)
    // 分词之后,对单词进行groupby分组,然后用sum进行聚合
    val wordCountDS: AggregateDataSet[(String, Int)] = inputDS.flatMap(_.split(" ")).map((_, 1)).groupBy(0).sum(1)
    // 打印输出
    wordCountDS.print()

3、流处理wordcount--DataStream

测试 nc -lk  7777

object StreamWordCount {
  def main(args: Array[String]): Unit = {
    // 从外部命令中获取参数
    val params: ParameterTool =  ParameterTool.fromArgs(args)
    val host: String = params.get("host")
    val port: Int = params.getInt("port")
    // 创建流处理环境
    val env = StreamExecutionEnvironment.getExecutionEnvironment
    // 接收socket文本流
    val textDstream: DataStream[String] = env.socketTextStream(host, port)
    // flatMap和Map需要引用的隐式转换
    import org.apache.flink.api.scala._
    val dataStream: DataStream[(String, Int)] = textDstream.flatMap(_.split("\\s")).filter(_.nonEmpty).map((_, 1)).keyBy(0).sum(1)
    dataStream.print().setParallelism(1)
    // 启动executor,执行任务
    env.execute("Socket stream word count")
  }
}

三、Flink部署

1、Standalone模式

分发配置到集群、8081端口监控管理

数据分发到taskmanager机器

执行程序:./flink run -c com.atguigu.wc.StreamWordCount –p 2 FlinkTutorial-1.0-SNAPSHOT-jar-with-dependencies.jar --host lcoalhost –port 7777

查看结果和计算过程

2、Yarn模式---Hadoop版本>2.2且安装hdfs

两种模式:Session-Cluster和Per-Job-Cluster模式

(1)Session-Cluster:初始化flink集群并常驻与yarn集群中

  • 启动:./yarn-session.sh-n2-s2-jm1024-tm1024-nmtest-d
  • 执行任务:./flinkrun-ccom.atguigu.wc.StreamWordCountFlinkTutorial-1.0-SNAPSHOT-jar-with-dependencies.jar--hostlcoalhost–port7777

(2)Per-Job-Cluster模式:每提交一个作业都会向yarn申请资源,创建一个新的flink集群

  • 直接执行任务:./flinkrun–myarn-cluster-ccom.atguigu.wc.StreamWordCountFlinkTutorial-1.0-SNAPSHOT-jar-with-dependencies.jar--hostlcoalhost–port7777

3、Kubernetes部署

分别启动Flink的docker组件:JobManager、TaskManager、JobManagerService

// 启动jobmanager-service 服务
kubectl create -f jobmanager-service.yaml

通过jobmanager配置访问Flink UI界面

四、Flink运行架构

1、运行时的组件

作业管理器(JobManager)、资源管理器(ResourceManager)、任务管理器(TaskManager),以及分发器(Dispatcher)

作业管理器JobManager:(请求资源)主进程、将作业图(JobGraph)转化为数据流图/执行图;请求资源、分发、协调

资源管理器ResourceManager:(分配slot插槽)将有空闲插槽的TaskManager分配给JobManager,发起会话、中止释放资源

任务管理器TaskManager:(包含一定并发量)注册插槽、与同一程序的task M交换数据

分发器:跨作业运行、为应用提交提供了REST接口

2、任务提交流程(都有ResourceManager)

Yarn模式任务提交流程

使用yarn的resourcemanager而非自身的

3、任务调度原理

(1)TaskManger与Slots:JVM进程、Task Slot是静态的概念,是指TaskManager具有的并发执行能力

(2)程序与数据流(DataFlow):Flink程序-Source、Transformation和Sink,转换运算(transformations)跟dataflow中的算子(operator)是一一对应的关系

(3)执行图(ExecutionGraph):直接映射成的数据流图是StreamGraph,也被称为逻辑流图,需要转换为物理视图

执行图包括4层:StreamGraph->JobGraph->ExecutionGraph->物理执行图

(4)并行度(Parallelism):特定算子的子任务(subtask)的个数

算子之间传输数据的形式:One-to-one类似于窄依赖,Redistributing类似于宽依赖

(5)任务链(OperatorChains):相同并行度的One-to-one操作算子,形成一个task,减少线程之间的切换和基于缓存区的数据交换

五、Flink流处理API

1、Environment

getExecutionEnvironment,客户端,当前执行程序的上下文

createLocalEnvironment:返回本地执行环境

createRemoteEnvironment:集群执行环境,需要指定ip、端口及jar包

2、Source

从集合读取数据:env.fromCollection(List(SensorReading("sensor_1",1547718199,35.8),SensorReading("sensor_6",1547718201,15.4))

从文件读取数据:valstream2=env.readTextFile("YOUR_FILE_PATH")

以kafka消息队列的数据作为来源:valstream3=env.addSource(newFlinkKafkaConsumer011[String]("sensor",newSimpleStringSchema(),properties))

自定义Source:valstream4=env.addSource(newMySensorSource()

3、Transform转换算子

map

flatMap:flatMap(List(1,2,3))(i⇒List(i,i))变成112233自动加逗号

Filter

KeyBy:流拆分成不相交的分区,每个分区内的key相同

滚动聚合算子(RollingAggregation):sum()、min()、max()、minBy()、maxBy()针对每个支流聚合

Reduce:分组数据流的聚合操作,合并当前的元素和上次聚合的结果,产生一个新的值

Split和Select:拆分和获取指定的流

Connect(放在同一个流中)和CoMap(组合成一个流)

Union:产生一个包含所有DataStream元素的新DataStream

Connect只能操作两个流,Union可以操作多个。

4、支持的数据类型env.fromElements(XXXX)

(1)基础数据类型

(2)Java和Scala元组(Tuples)

(3)Scala样例类(caseclasses)

(4)Java简单对象(POJOs)

(5)其它(Arrays,Lists,Maps,Enums,等等)

5、实现UDF-更细粒度的控制流

函数类(FunctionClasses):实现MapFunction,FilterFunction,ProcessFunction接口

匿名函数(LambdaFunctions)

富函数(RichFunctions):函数类的接口,所有Flink函数类都有其Rich版本,自带一系列生命周期方法(开关、得到上下文),可以实现复杂功能

6、Sink

没有spark中的forEach方法

需要通过stream.addSink(newMySink(xxxx))完成任务最终输出

kafka:union.addSink(new FlinkKafkaProducer011[String]("localhost:9092", "test", new SimpleStringSchema()))

redis:dataStream.addSink(newRedisSink[SensorReading](conf,newMyRedisMapper))

Elasticsearch:dataStream.addSink( esSinkBuilder.build() )

自定义sink:dataStream.addSink(newMyJdbcSink())

六、Flink中的Window

1、Window

(1)概念:切割无限数据为有限块进行处理

(2)两种类型:CountWindow(按条数生成)和TimeWindow(按时间生成)

time window时间窗口又可以分为滚动窗口(Tumbling Window)、滑动窗口(Sliding Window)和会话窗口(Session Window)。

滚动窗口:按固定的窗口长度对数据进行切片,没有重叠,适用于每个时间段的聚合计算

滑动窗口:将元素分配到固定长度的窗口中,窗口可以重叠,如10分钟的窗口和5分钟的滑动【适用于对最近一个时间段内的统计,如5分钟内是否需要报警】

会话窗口(Session Windows):session间隔定义了非活跃周期的长度,一段时间没有接收到新数据就会生成新的窗口

2、WindowAPI

(1)TimeWindow:一次对一个window里面的所有数据进行计算。

.timeWindow参数个数不同包括

滚动窗口:将Flink获取到的数据根据进入Flink的时间划分到不同的窗口中

滑动窗口(SlidingEventTimeWindows):函数名相同,传两个参数,

(2)CountWindow-根据key的数量计算

countWindow根据参数不同

滚动窗口:元素数量达到窗口大小时,就会触发窗口的执行

滑动窗口:sliding_size设置为了2,也就是说,每收到两个相同key的数据就计算一次,每一次计算的window范围是10个元素

(3)windowfunction:增量聚合(每次到来都计算)、全窗口函数(全部到来再遍历)

(4)其它可选API:.trigger()——触发器、.evitor()——移除器、.allowedLateness()——允许处理迟到的数据、.getSideOutput() —— 获取侧输出流

七、时间语义与Wartermark

1、Flink中的时间语义

Event Time:事件的创建时间

Ingestion Time:事件进入事件

Processing Time:本地系统事件(默认的时间属性)

要根据日志的生成时间(Event Time)进行统计

2、Event Time的引入

绝大部分使用event time,需要引入EventTime的时间属性

val env = StreamExecutionEnvironment.getExecutionEnvironment
// 从调用时刻开始给env创建的每一个stream追加时间特征
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)

3、WaterMark

(1)概念:由于分布式等原因,用于处理乱序事件

不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了,这个特别的机制,就是Watermark。

每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于maxEventTime-t的所有数据都已经到达

触发前一窗口的“关窗时间”,一旦触发关门那么以当前时刻为准在窗口范围内的所有所有数据都会收入窗中

一旦Watermark比当前未触发的窗口的停止时间要晚,那么就会触发相应窗口的执行

(2)WaterMark的引入

dataStream.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[SensorReading](Time.milliseconds(1000)) {
  override def extractTimestamp(element: SensorReading): Long = {
    element.timestamp * 1000
  }
} )

需要指定数据源中的时间戳

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

4、EventTime在Window中的应用

滚动窗口(Tumbling Event Time Windows)

val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(TumblingEventTimeWindows.of(Time.seconds(2)))

滑动窗口(Sliding Event Time Windows)

val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(SlidingEventTimeWindows.of(Time.seconds(2),Time.milliseconds(500)))

会话窗口(Event Time Session Windows)

val windowStream: WindowedStream[(String, Long, Int), Tuple, TimeWindow] = textKeyStream.window(EventTimeSessionWindows.withGap(Time.milliseconds(500)) )

八、ProcessFunctionAPI(底层API)

访问时间戳、watermark以及注册定时事件。还可以输出特定的一些事件,例如超时事件等

共有8个:

ProcessFunction
KeyedProcessFunction
CoProcessFunction
ProcessJoinFunction
BroadcastProcessFunction
KeyedBroadcastProcessFunction
ProcessWindowFunction
ProcessAllWindowFunction

1、KeyedProcessFunction

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

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

此外,额外提供了下列两个方法

processElement(v: IN, ctx: Context, out: Collector[OUT]), 流中的每一个元素都会调用这个方法,调用结果将会放在Collector数据类型中输出

onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])是一个回调函数。当之前注册的定时器触发时调用

2、TimerService和定时器(Timers)

事件相关:返回时间、触发定时器、删除定时器

例子:温度在1秒内持续上升,则报警(类实现接口有对应的方法)

val warnings = readings
.keyBy(_.id)
.process(new TempIncreaseAlertFunction)

3、侧输出流(SideOutput)

大部分算子都是单一输出,process function的side outputs功能可以产生多条流,流的数据类型也可不同

一个sideoutput可以定义为OutputTag[X]对象,X是输出流的数据类型

通过Context对象发射一个事件到一个或者多个sideoutputs

示例

val monitoredReadings: DataStream[SensorReading] = readings
  .process(new FreezingMonitor)

monitoredReadings
  .getSideOutput(new OutputTag[String]("freezing-alarms"))
  .print()

readings.print()

FreezingMonitor函数的实现

class FreezingMonitor extends ProcessFunction[SensorReading, SensorReading] {
  // 定义一个侧输出标签
  lazy val freezingAlarmOutput: OutputTag[String] =
    new OutputTag[String]("freezing-alarms")

  override def processElement(r: SensorReading,
                              ctx: ProcessFunction[SensorReading, SensorReading]#Context,
                              out: Collector[SensorReading]): Unit = {
    // 温度在32F以下时,输出警告信息
    if (r.temperature < 32.0) {
      ctx.output(freezingAlarmOutput, s"Freezing Alarm for ${r.id}")
    }
    // 所有数据直接常规输出到主流
    out.collect(r)
  }
}

4、CoProcessFunction

两条输入流,DataStream API提供了CoProcessFunction这样的low-level操作

提供了操作每一个输入流的方法:processElement1()和processElement2()

两种方法都通过Context对象来调用

九、状态编程与容错机制

流式计算分为无状态和有状态(每一步操作输出一个状态/操作结果)两种情况

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

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

1、有状态的算子和应用程序

数据源source,数据存储sink都是有状态的

两种类型的状态:算子状态(operator state)、键控状态(keyed state)

(1)算子状态(operator state)

作用范围限定为算子任务

不能由相同或不同算子的另一个任务访问。

Flink为算子状态提供三种基本数据结构:列表状态(List state)、联合列表状态(Union list state)、广播状态(Broadcast state)

(2)键控状态(keyed state)

根据输入数据流中定义的键(key)来维护和访问,每个键值维护一个状态实例

相同键的所有数据,都分区到同一个算子任务中

Flink的Keyed State支持以下数据类型:单个值、列表值、map值、AggregatingState、ReducingState

2、状态一致性

含义:成功处理故障并恢复之后得到的结果,与没有发生任何故障时得到的结果相比,前者到底有多正确

(1)一致性级别:at-most-once(计数结果可能丢失)、at-least-once(计数结果可能大于但不会小于)、exactly-once(计数结果准确)

第一代流处理器(如Storm和Samza)只保证at-least-once

Flink的一个重大价值在于,它既保证了exactly-once,也具有低延迟和高吞吐的处理能力。

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

数据源和输出,结果的正确性贯穿了整个流处理应用的始终

整个端到端的一致性级别取决于所有组件中一致性最弱的组件。具体可以划分如下:内部保证(依赖于检查点)、source(外部数据源可以重设数据读取位置)、sink(保证数据源不会重复写入外部系统,又包含幂等写入、事务写入)

幂等写入:可以执行多次,再重复执行就不起作用了(例如转账500元)

事务写入:构建事务,checkpoint真正完成,才会将结果写入

3、检查点(checkpoint)

flink使用检查点保证exactly-once(计数结果准确),在出现故障时将系统重置回正确状态

flink的检查点即为重新计数的参考点

(1)检查点算法

按照输入记录的第一个字段(一个字符串)进行分组并维护第二个字段的计数状态

val stream: DataStream[(String, Int)] = ... 
val counts: DataStream[(String, Int)] = stream
.keyBy(record => record._1)
.mapWithState(  (in: (String, Int), state: Option[Int])  => 
state match { 
case Some(c) => ( (in._1, c + in._2), Some(c + in._2) ) 
case None => ( (in._1, in._2), Some(in._2) )
})

当Flink数据源(在本例中与keyBy算子内联)遇到检查点分界线(barrier)时,它会将其在输入流中的位置保存到持久化存储中。这让 Flink可以根据该位置重启。  

检查点操作完成,状态和位置均已备份到稳定存储中

如果检查点操作失败,Flink可以丢弃该检查点并继续正常执行

(2)Flink+Kafka如何实现端到端的exactly-once语义

Flink + Kafka的数据管道系统(Kafka进、Kafka出)

内部:利用checkpoint机制,把状态存盘

source:kafka consumer作为source,可以将偏移量保存下来,故障恢复可以重置偏移量

sink:采用两阶段提交 sink,需要实现一个 TwoPhaseCommitSinkFunction

4、选择一个状态后端(statebackend)

MemoryStateBackend:将键控状态作为内存中的对象进行管理,状态存在JVM堆上,检查点存在jobmanager内存中

FsStateBackend:检查点存在存到远程的持久化文件系统(FileSystem)上

RocksDBStateBackend:所有状态序列化后,存入本地的RocksDB中存储

RocksDB需要引入对应的依赖

设置状态后端为FsStateBackend

val env = StreamExecutionEnvironment.getExecutionEnvironment
val checkpointPath: String = ???
val backend = new RocksDBStateBackend(checkpointPath)

env.setStateBackend(backend)
env.setStateBackend(new FsStateBackend("file:///tmp/checkpoints"))
env.enableCheckpointing(1000)
// 配置重启策略
env.setRestartStrategy(RestartStrategies.fixedDelayRestart(60, Time.of(10, TimeUnit.SECONDS)))

十、TableAPI与SQL

Table API是流处理和批处理通用的关系型API

1、需要引入的pom依赖

flink-table-XXXX

2、了解TableAPI

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

  val inputStream = env.readTextFile("..\\sensor.txt")
  val dataStream = inputStream
    .map( data => {
      val dataArray = data.split(",")
      SensorReading(dataArray(0).trim, dataArray(1).trim.toLong, dataArray(2).trim.toDouble)
    }
    )
  // 基于env创建 tableEnv
val settings: EnvironmentSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)

  // 从一条流创建一张表
  val dataTable: Table = tableEnv.fromDataStream(dataStream)

  // 从表里选取特定的数据
  val selectedTable: Table = dataTable.select('id, 'temperature)
    .filter("id = 'sensor_1'")

  val selectedStream: DataStream[(String, Double)] = selectedTable
    .toAppendStream[(String, Double)]

  selectedStream.print()

  env.execute("table test")

}

(1)动态生成表

tableEnv.fromDataStream(dataStream,’id,’timestamp  .......)   
//转化成流
table.toAppendStream[(String,String)]

(2)字段

单引放到字段前面来标识字段名, 如 ‘name , ‘id ,’amount   

3、TableAPI的窗口聚合操作

(1)通过一个例子了解TableAPI 

 // 基于env创建 tableEnv
val settings: EnvironmentSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val tableEnv: StreamTableEnvironment = StreamTableEnvironment.create(env, settings)

  // 从一条流创建一张表,按照字段去定义,并指定事件时间的时间字段
  val dataTable: Table = tableEnv.fromDataStream(dataStream, 'id, 'temperature, 'ts.rowtime)

  // 按照时间开窗聚合统计
  val resultTable: Table = dataTable
    .window( Tumble over 10.seconds on 'ts as 'tw )
    .groupBy('id, 'tw)
    .select('id, 'id.count)

  val selectedStream: DataStream[(Boolean, (String, Long))] = resultTable
    .toRetractStream[(String, Long)]

(2)关于group by

table转换为流的时候只能用toRetractDstream

val dataStream: DataStream[(Boolean, (String, Long))] = table
.toRetractStream[(String,Long)]

如果使用的api包括时间窗口,那么窗口的字段必须出现在groupBy中。  

val resultTable: Table = dataTable
    .window( Tumble over 10.seconds on 'ts as 'tw )
    .groupBy('id, 'tw)
    .select('id, 'id.count)

(3)关于时间窗口  

提前声明时间字段,如果是processTime直接在创建动态表时进行追加就可以。

val dataTable: Table = tableEnv.fromDataStream(dataStream, 'id, 'temperature, 'ps.proctime)

使用Tumble over 10000.millis on 来表示滑动窗口  

4、SQL如何编写

// 从一条流创建一张表,按照字段去定义,并指定事件时间的时间字段
  val dataTable: Table = tableEnv.fromDataStream(dataStream, 'id, 'temperature, 'ts.rowtime)

  // 直接写sql完成开窗统计 
  val resultSqlTable: Table = tableEnv.sqlQuery("select id, count(id) from "
  + dataTable + " group by id, tumble(ts, interval '15' second)")

  val selectedStream: DataStream[(Boolean, (String, Long))] = resultSqlTable.toRetractStream[(String, Long)]

  selectedStream.print()

  env.execute("table window test")

十一、FlinkCEP简介

1、什么是复杂事件处理CEP

每个Pattern都应该包含几个步骤,或者叫做state

识别简单事件之间的内在联系,多个符合一定规则的简单事件构成复杂事件

2、Flink CEP(有专门的library支持)

Flink CEP library的组件:

 

 开发人员要在DataStream流上定义出模式条件,之后Flink CEP引擎进行模式检测,必要时生成告警。

val loginFailDataStream = patternStream
  .select((pattern: Map[String, Iterable[LoginEvent]]) => {
    val first = pattern.getOrElse("begin", null).iterator.next()
    val second = pattern.getOrElse("next", null).iterator.next()

    Warning(first.userId, first.eventTime, second.eventTime, "warning")
  })

十二、常见面试题汇总

架构、监控、数据高峰处理、特性解决、CEP

select方法需要实现一个PatternSelectFunction,通过select方法来输出需要的警告

posted @ 2021-11-26 22:35  哥们要飞  阅读(242)  评论(0编辑  收藏  举报