Flink CEP复杂事件处理
FLINK复杂事件处理CEP ,基于流处理技术将系统数据看做不同事件,寻找不同事件的相互关系,来列出关系序列库,并利用过滤和关联和聚合等技术,最终由简单的事件产生复杂事件,使用模式规则来对重要数据进行追踪和分析, 从实时数据中挖掘隐藏的数据信息。
复杂事件的用途:
- 反欺诈,
- 网络欺诈,
- 设备故障检测
Flink基于DataStream Api 来做 FlinkCEP组件栈
在idea项目中导入maven配置
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep-scala_2.11</artifactId>
<version>1.9.1</version>
</dependency>
简单事件
主要特点是:处理的事件是单一的事件,不用考虑事件之间的相互关系,能通过简单的处理可以得到结果。
复杂事件
不仅要处理单一事件,还要处理由多个事件组合成的复杂事件,监控分析事件流,对特定事件的发生,来触发一定的(动作)处理事件。
复杂事件之间存在的几种关系:
- 时序,
- 层次,
- 聚合,
- 依赖,
- 因果
FlinkCEP提供了pattern api 用于对输入流数据的复杂事件规则定义,并从复杂事件中抽取数据结果。
步骤:
1:输入流事件的创建
2:pattern规则定义
3:pattern应用在流数据上的检测
4:选取相应的数据
pattern规则定义(模式定义)
pattern有两种定义模式:
1:单次:只接受一个事件
2:循环:接收一个或多个事件
可以指定单次的循环次数,把单次执行模式转换为循环执行模式
每种模式可以将多个条件组合到同一个事件中。条件的组合通过where进行组合
pattern是通过begin方法来定义的
var pattern=Pattern.beginevent
要满足where里的条件,当前的patten才会接受事件
patten.where(.getCallType=“success”)
设置循环次数
触发4次
patten.times(4)
触发次数的范围2,3,4
patten.times(2,4)
通过适用optional关键词指定要么不触发要么触发指定的次数
pattern.times(4).optional()
pattern.times(2,4).optional()
通过使用关键词greedy来开启pattern的贪婪模式,尽可能的触发
pattern.times(2,4).greedy()
pattern.times(2,4).optional().greedy()
oneOrMore 一次或多次
pattern.oneOrMore();
greedy尽可能的进行多次触发
pattern.oneOrMore().greedy();
optional要么不触发要么触发多次
pattern.oneOrMore().optional();
pattern.oneOrMore().optional().greedy();
timeOrMore指定触发的次数
pattern.timeOrMore(2)
pattern.timeOrMore(2).greedy()
pattern.timeOrMore(2).optional().greedy()
定义条件:每个模式都要一个指定触发的条件,用来判断事件流在模式中是否被接受,如果满足了条件的话,继续往下执行。
FlinkCEP使用pattern.where() pattern.or() pattern.until()来为pattern来指定条件
pattern条件有两种
Simple Conditions
Combing Conditions
简单条件:Simple Conditions继承 Iterative Conditions根据事件的相关的字段信息进行判断,决定是否接受该事件
把通话成功的信息筛选出来
pattern.where(.callType==“sucess”)
组合条件:把简单的条件进行组合,使用where方法进行条件的组合。
默认每个条件是使用and进行连接的,也可以使用or方法连接条件
var start=Pattern.beginstationlog
.where(.callType==“sucess”)
.or(.duration>10)
终止条件:如果程序中使用了oneOrMore,oneOrMore.optional方法的话程序可能会一直循环下去,我们要使用until()来终止它
pattern.oneOrMore().until(_.callOut.startsWith(“186”))
模式序列:将相互独立的模式进行组合形成模式序列,根据各个模式通过的邻近条件进行连接
有严格邻近、宽松邻近、非确定宽松邻 近三种邻近连接条件
严格邻近:需要所有的事件按顺序都满足模式条件,不会忽略不满足的模式条件
val strict=pattern.next(“middle”).where(…)
宽松邻近:会忽略不满足(没有成功匹配)的模式条件
val relaxed=pattern.followedBy(“middle”).where(…)
非确定宽松邻近:可以忽略已经匹配的模式条件
val nonDetermin=pattern.followByAny(“middle”).where(…)
除了上述的模式序列 不希望出现某种邻近关系
notNext()不希望一个事件后面出现某种事件
notFollowedBy()不希望某个事件在两个事件之间发生
注意: 1、所有模式序列必须以 .begin() 开始
2、模式序列不能以 .notFollowedBy() 结束
3、“not” 类型的模式不能被 optional 所修饰
4、此外,还可以为模式指定时间约束,用来要求在多长时间内匹配有效
pattern.within(Time.seconds(10))
模式检测:CEP.pattern,给定输入流和模式,可以得到一个PatternStream
val patternStream = CEP.patterneventlog
选择结果:
FilnkCEP提供了两种方式从patternStream中提取事件的结果事件,select 和flatselect
选择结果
得到 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]]): OUT = {
//获取pattern中的startEvent
val startEvent = pattern.get(“start_pattern”).get.next
//获取Pattern中middleEvent
val middleEvent = pattern.get(“middle”).get.next
//返回结果
OUT(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[OUT]) = {
//获取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(OUT(startEvent, middleEvent))
}}
3.通过 Select Funciton 抽取超时事件
如果模式中有 within(time),那么就很有可能有超时的数据存在,
通过 PatternStream.Select 方法分别获取超时事件和正常事件。
首先需要创建 OutputTag 来标记超时事件,
然 后在 PatternStream.select 方法中使用 OutputTag
,就可以将超时事件从 PatternStream 中抽取出来。
// 通过CEP.pattern方法创建PatternStream
val patternStream: PatternStream[Event] = CEP.pattern(input, pattern)
//创建OutputTag,并命名为timeout-output
val timeoutTag = OutputTagString
//调用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)
- 案例需求:从一堆的登录日志中,匹配一个恶意登录的模式(如果一个用户连续失败三次, 则是恶意登录),从而找到哪些用户名是恶意登录。 /** * 登录告警系统 * 从一堆的登录日志中,匹配一个恶意登录的模式(如果一个用户连续失败三次,则是 恶意登录),从而找到哪些用户名是恶意 登录 */
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.beginEventLog
.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.patternEventLog
//第三步: 输出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()
}
}
初识 Flink
Flink 快速入门
Flink 常用 API 详解
Flink State 管理与恢复
Flink Window(窗口)详解
通过固定的时间或长度将数据流切分成不同的窗口,对窗口中的数据集进行计算
window分类
根据上游的数据集分成不同的类型
KeyStream window(keyedStream 类型)调用DataStream的window()方法,在不同的task实例中分别计算结果,最后得出key的结果。
global window(non-key 类型)调用DataStream的windowall()方法,在同一个task实例中计算,并统计出全局的结果。
val datas = 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.to Long)
})
//global
datas.window(自定义的WindowAssigner)
//keyStream
datas.keyBy(.sid).window(自定义的WindowAssigner)
根据业务数据分类
Time Window(时间窗口)
根据不同的业务场景可以分
滑动(Tumbling):按照固定的时间进行划分,窗口与窗口之间没有相互重叠的。我们只需要指定一个窗口的长度即可。
data.map(stationLog=>((stationLog.sid,1)))
.keyBy(.1)
.timeWindow(Time.seconds(5))
//.window(TumblingEventTimeWindow.of(Time.seconds(5))
.sum(1)
滚动(Sliding): 窗口之间是有重叠的,我们需要指定窗口的长度和滑动的长度两个
data.map(stationLog=>((stationLog.sid,1)))
.keyBy(.1)
.sildWindow(Time.seconds(10),Time.seconds(5)) //.window(SlidingEventSildWindow.of(Time.seconds(10),Time.seconds(5)))
.sum(1)
会话(Session):是指在规定的时间(session gap)之内没有数据活跃接入的话,就会结束这个窗口
主要是把某段时间内活跃度高的数据放在同一个窗口中,进行计算。
data.map(stationLog=>((stationLog.sid,1)))
.keyBy(.1)
.window(SessionEventSessionWindow.withGap(Time.seconds(3)))
.sum(1)
Count Window(数量窗口)
也有SlidingWindow,TumblingWindow
window的API
掌握 Keyed Window 的算子,其中 Windows Assigner 和 Windows Funciton 是所有窗口算子必须指定的属性, 其余的属性都是根据实际情况选择指定。
stream.keyBy(…) // 是Keyed类型数据集
.window(…) //指定窗口分配器类型
[.trigger(…)] //指定触发器类型(可选)
[.evictor(…)] //指定evictor或者不指定(可选)
[.allowedLateness(…)] //指定是否延迟处理数据(可选)
[.sideOutputLateData(…)] //指定Output Lag(可选)
.reduce/aggregate/fold/apply() //指定窗口计算函数
[.getSideOutput(…)] //根据Tag输出数据(可选)
Windows Assigner:指定窗口的类型,定义如何将数据流分配到一个或多个窗口;
Windows Trigger:指定窗口触发的时机,定义窗口满足什么样的条件触发计算;
Evictor:用于数据剔除;
allowedLateness:标记是否处理迟到数据,当迟到数据到达窗口中是否触发计算
Output Tag:标记输出标签,然后在通过 getSideOutput 将窗口中的数据根据标签输出;
Windows Funciton:定义窗口上数据处理的逻辑,例如对数据进行 sum 操作。
window聚合函数
如果定义了 Window Assigner 之后,下一步就可以定义窗口内数据的计算逻辑,这也就 是 Window Function 的定义。
Flink 中提供了四种类型的 Window Function,分别为 ReduceFunction、AggregateFunction 以及 ProcessWindowFunction,(sum 和 max)等。
前三种类型的 Window Fucntion 按照计算原理的不同可以分为两大类:
一类是增量聚合函数:对应有 ReduceFunction、AggregateFunction;
另一类是全量窗口函数,对应有 ProcessWindowFunction(还有 WindowFunction)。
增量聚合函数计算性能较高,占用存储空间少,主要因为基于中间状态的计算结果,窗 口中只维护中间结果状态值,不需要缓存原始数据。而全量窗口函数使用的代价相对较高,
性能比较弱,主要因为此时算子需要对所有属于该窗口的接入数据进行缓存,然后等到窗口 触发的时候,对所有的原始数据进行汇总计算。
-
ReduceFunction ReduceFunction 定义了对输入的两个相同类型的数据元素按照指定的计算方法进行聚 合的逻辑,然后输出类型相同的一个结果元素。
//每隔5秒统计每个基站的日志数量
data.map(stationLog=>((stationLog.sid,1)))
.keyBy(_._1)
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.reduce((v1,v2)=>(v1._1,v1._2+v2._2)) -
AggregateFunction 和 ReduceFunction 相似,AggregateFunction 也是基于中间状态计算结果的增量计算 函数,但 AggregateFunction 在窗口计算上更加通用。AggregateFunction 接口相对 ReduceFunction 更加灵活,实现复杂度也相对较高。AggregateFunction 接口中定义了三个 需要复写的方法,其中
add()定义数据的添加逻辑
getResult 定义了根据 accumulator 计算结果的逻辑
merge 方法定义合并 accumulator 的逻辑
//每隔3秒计算最近5秒内,每个基站的日志数量
data.map(stationLog=>((stationLog.sid,1)))
.keyBy(_._1)
.timeWindow(Time.seconds(5),Time.seconds(3))
.aggregate(new AggregateFunction[(String,Int),(String,Long),(String,Long)] {
override def createAccumulator() = ("",0)
override def add(in: (String, Int), acc: (String, Long)) = { (in._1,acc._2+in._2) }
override def getResult(acc: (String, Long)) = acc
override def merge(acc: (String, Long), acc1: (String, Long)) = {
(acc._1,acc1._2+acc._2) } })
- ProcessWindowFunction 前面提到的 ReduceFunction 和 AggregateFunction 都是基于中间状态实现增量计算的 窗口函数,虽然已经满足绝大多数场景,但在某些情况下,统计更复杂的指标可能需要依赖 于窗口中所有的数据元素,或需要操作窗口中的状态数据和窗口元数据,这时就需要使用到 ProcessWindowsFunction,ProcessWindowsFunction 能够更加灵活地支持基于窗口全部数 据 元 素 的 结 果 计 算 , 例 如 对 整 个 窗 口 数 据 排 序 取 TopN, 这 样 的 需 要 就 必 须 使 用 ProcessWindowFunction。
//每隔5秒统计每个基站的日志数量
data.map(stationLog=>((stationLog.sid,1)))
.keyBy(_._1) .timeWindow(Time.seconds(5))
.process(new ProcessWindowFunction[(String,Int),(String,Int),String,TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[(String, Int)],out:Collector[(String, Int)]): Unit = { println("-------") out.collect((key,elements.size)) } }).print()
Flink Time 详解
flink按照时间的产生的位置不同,分为三种时间语义
事件的产生事件(Event time)
事件进入Flink缓存区的时间(Ingestion Time)
事件被flink处理的时间(Process Time)
时间语义 Time
flink支持上述的三种语义,用户需要选择时间语义作为流数据处理的依据。
flink增强了对流数据处理的灵活性和准确性
设置时间语义,默认情况下使用Process Time的,如果需要设置事件的时间语义的话可以通过如下代码所示
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
streamEnv.setStreamTimeCharacteristic(TimeCharacteristic.IngestionTime)
WaterMark 水位线
WaterMark用于处理乱序数据问题
EventTime处理Stream数据时的乱序问题
WaterMark原理
EventTime在window计算中,我们的window不能一直处于等待延迟数据的状态,在一定的情况下,面对延迟时间比较久的事件。我们的window没必要一直等待它,我们可以使用WaterMark来实现对处理延迟事件的处理,WaterMark可以保证延迟事件可以按预期一样计算出正确而且连续的结果。
WaterMark的计算
Watermark = 进入 Flink 的最大的事件时间(maxtEventTime)— 指定的延迟时间(t)
WaterMark如何触发窗口函数的计算
Watermark >= WindowEndTime
WaterMark机制的本质是延迟触发机制
WaterMark使用的三种情况
1)有序的Stream
WaterMark = maxEventTime - 0
2)无序的Stream
WaterMark = maxEventTime - t
3)并行无序的Stream
并行的对齐机制,取各个WaterMark中的最小值为MaxEventTim
Window 的 allowedLateness
是对与WaterMark机制都处理不了的延迟数据来说的。比如延迟的数据是比较长的时间的话,WaterMark机制就无法对他进行处理。
所以我们使用allowerLateness来处理延迟时间比较长的事件。
通常情况下用户虽然希望对迟到的数据进行窗口计算,但并不想将结果混入正常的计算 流程中,例如用户大屏数据展示系统,即使正常的窗口中没有将迟到的数据进行统计,但为 了保证页面数据显示的连续性,后来接入到系统中迟到数据所统计出来的结果不希望显示在屏幕上,
而是将延时数据和结果存储到数据库中,便于后期对延时数据进行分析。
对于这种情况需要借助 Side Output 来处理,通过使用 sideOutputLateData(OutputTag)来标记迟到数据计算的结果,然后使用 getSideOutput(lateOutputTag)从窗口结果中获取 lateOutputTag 标签对应的数据,之后转成独立的 DataStream 数据集进行处理,创建 late-data 的 OutputTag,再通过该标签从窗口结果中将迟到数据筛选出来。
注意:如果有 Watermark 同时也有 Allowed Lateness。那么窗口函数再次触发的条件 是:watermark < end-of-window + allowedLateness
案例:每隔 5 秒统计最近 10 秒,每个基站的呼叫数量。要求: 1、每个基站的数据会存在乱序 2、大多数数据延迟 2 秒到,但是有些数据迟到时间比较长 3、迟到时间超过两秒的数据不能丢弃,放入侧流
object LateDataOnWindow {
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 =lin.esplit(",") new StationLog(arr(0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.to Long) }).assignTimestampsAndWatermarks(
//引入Watermark
new BoundedOutOfOrdernessTimestampExtractorStationLog{
//延迟2 秒
override def extractTimestamp(element: StationLog) = { element.callTime } })
//分组,开窗处理 //定义一个侧输出流 的标签
var lateTag =new OutputTagStationLog
val mainStream: DataStream[String] = data.keyBy(_.sid)
.timeWindow(Time.seconds(10), Time.seconds(5))
//注意:只要符合watermark < end-of-window + allowedLateness之内到达的数 据都会被再次触发窗口的计算 //超过之外的迟到数据会被放入侧输出流
.allowedLateness(Time.seconds(5))
//允许数据迟到5秒
.sideOutputLateData(lateTag)
.aggregate(new AggregateCount,new OutputResult)
mainStream.getSideOutput(lateTag).print(“late”)
//迟到很久的数据可以另外再处理
mainStream.print(“main”)
streamEnv.execute()
}
class AggregateCount extends AggregateFunction[StationLog,Long,Long]{
override def createAccumulator(): Long = 0
override def add(in: StationLog, acc: Long): Long = acc+1
override def getResult(acc: Long): Long = acc
override def merge(acc: Long, acc1: Long): Long = acc+acc1
}
class OutputResult extends WindowFunction[Long,String,String,TimeWindow]{
override def apply(key: String, window: TimeWindow, input: Iterable[Long],
out: Collector[String]): Unit = {
var sb =new StringBuilder
sb.append(“窗口范围是: “)
.append(window.getStart)
.append(”----”)
.append(window.getEnd)
sb.append("\n")
sb.append(“当前基站是:”).append(key) .append(" 呼叫数量是: ").append(input.iterator.next()) out.collect(sb.toString())
}
} }
TableAPI 和 Flink SQL
Flink 也提供了关系型编程接口 Table API 以及基于 Table API 的 SQL API,让用户能够通过使用结构化编程 接口高效地构建 Flink 应用。同时 Table API 以及 SQL 能够统一处理批量和实时计算业务, 无须切换修改任何应用代码就能够基于同一套 API 编写流式应用和批量应用,从而达到真正意义的批流统一
开发环境构建
使用 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
TableEnvironment
Flink SQL基于Apache Calcite 框架实现了 SQL 标准协议,是构建在Table API之上的更高级接口。
流计算环境下创建 TableEnviroment:
val streamEnv=StreamExecutionEnvironment.getExecutionment
val tableEnv=StreanTableEnviroment.create(streamEnv)
val bsSettings = EnvironmentSettings.newInstance().useOldPlanner().inStreamingMode().build()
val bsTableEnv = StreamTableEnvironment.create(streamEnv, bsSettings)
Table API
在 Flink 中创建一张表有两种方法:
1. 从一个文件中导入表结构(Structure)(常用于批计算)(静态)
2. 从 DataStream 或者 DataSet 转换成 Table (动态)
- 创建 Table Table API 中已经提供了 TableSource 从外部系统获取数据,例如常见的数据库、文件 系统和 Kafka 消息队列等外部系统。
-
从文件中创建 Table(静态表) Flink 允许用户从本地或者分布式文件系统中读取和写入数据,在 Table API 中可以通 过 CsvTableSource 类来创建,只需指定相应的参数即可。但是文件格式必须是 CSV 格式的。
其 他 文 件 格 式 也 支 持 ( 在 Flink 还 有 Connector 的 来 支 持 其 他 格 式 或 者 自 定 义 TableSource)。 //创建CSV格式的 TableSource
val fileSource = new CsvTableSource("/station.log",
ArrayString,
Array(Types.STRING,Types.STRING,Types.STRING,Types.STRING,Types.LONG,T ypes.LONG)
)
//注册Table,表名为t_log
tableEvn.registerTableSource(“t_log”,fileSource)
//转换成Table对象,并打印表结构
tableEvn.scan(“t_log”).printSchema()注意:本案例的最后面不要 streamEnv.execute(),否则报错。因为没有其他流计算逻辑
- 从 DataStream 中创建 Table(动态表) 前面已经知道 Table API 是构建在 DataStream API 和 DataSet API 之上的一层更高级 的抽象,
因此用户可以灵活地使用 Table API 将 Table 转换成 DataStream 或 DataSet 数据集,
也可以将 DataSteam 或 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.to Long)
})
//吧DataStream对象变成一个Table
tableEvn.registerDataStream(“t_station_log”,data)
//注册表
val table: Table = tableEvn.scan(“t_station_log”)
table.printSchema()
//打印表结构
streamEnv.execute()
- 从 DataStream 中创建 Table(动态表) 前面已经知道 Table API 是构建在 DataStream API 和 DataSet API 之上的一层更高级 的抽象,
//读取数据
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.to Long)
})
//吧DataStream对象变成一个Table
val table: Table = tableEvn.fromDataStream(data)
//直接变成table对象
table.printSchema()
//打印表结构
streamEnv.execute()
-
修改 Table 中字段名 Flink 支持把自定义 POJOs 类的所有 case 类的属性名字变成字段名,也可以通过基于 字段偏移位置和字段名称两种方式重新修改:
//导入table库中的隐式转换
import org.apache.flink.table.api.scala._
// 基于位置重新指定字段名称为"field1", “field2”, “field3”
val table = tStreamEnv.fromDataStream(stream, 'field1, 'field2, 'field3)
// 将DataStream转换成Table,并且将字段名称重新成别名
val table: Table = tStreamEnv.fromDataStream(stream, 'rowtime as 'newTime, 'id as 'newId,'variable as 'newVariable) -
查询和过滤 在 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._ i
mport 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.to Long) })
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 对象。
-
分组聚合
举例:我们统计每个基站的日志数量。
val table: Table = tableEvn.fromDataStream(data)
tableEvn.toRetractStream[Row]( table.groupBy('sid).select('sid, 'sid.count as 'logCount))
.filter(_._1==true)
//返回的如果是true才是Insert的数据
.print()
在代码中可以看出,使用 toAppendStream 和 toRetractStream 方法将 Table 转换为 DataStream[T]数据集,
T 可以是 Flink 自定义的数据格式类型 Row,
也可以是用户指定的数 据 格 式 类 型
在 使 用 toRetractStream 方 法 时
返 回 的 数 据 类 型 结 果 为 DataStream[(Boolean,T)]
Boolean 类型代表数据更新类型
True 对应 INSERT 操作更新的数据
False 对应 DELETE 操作更新的数据 -
UDF 自定义的函数 用户可以在 Table API 中自定义函数类,常见的抽象类和接口是:
ScalarFunction
TableFunction
AggregateFunction
TableAggregateFunction案例:使用 Table 完成基于流的 WordCount
object TableAPITest2 {
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 stream: DataStream[String] = streamEnv.socketTextStream(“hadoop101”,8888)
val table: Table = tableEvn.fromDataStream(stream,'words)
var my_func =new MyFlatMapFunction()
//自定义UDF
val result: Table = table.flatMap(my_func('words)).as('word, 'count).groupBy('word)
//分组
.select('word, 'count.sum as 'c)
//聚合
tableEvn.toRetractStreamRow
.filter(_._1==true)
.print()
tableEvn.execute(“table_api”) }
//自定义UDF
class MyFlatMapFunction extends TableFunction[Row]{
//定义类型
override def getResultType: TypeInformation[Row] = { Types.ROW(Types.STRING, Types.INT) }
//函数主体
def eval(str:String):Unit ={ str.trim.split(" ") .foreach({word=>{ var row =new Row(2) row.setField(0,word) row.setField(1,1) collect(row) }})
} } }
- 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.to Long) }) .assignTimestampsAndWatermarks(
//引入Watermark
new BoundedOutOfOrdernessTimestampExtractorStationLog
{//延迟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.toRetractStreamRow
.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 最终会在整体上将代码合并在同一套代码逻辑中
- 执行 SQL 以下通过实例来了解 Flink SQL 整体的使用方式,
案例:统计每个基站通话成功的通话 时长总和。 //读取数据
val data = streamEnv.readTextFile(getClass.getResource("/station.log").getPath)
.map(line=>{ var arr =line.split(",") new StationLog(arr0).trim,arr(1).trim,arr(2).trim,arr(3).trim,arr(4).trim.toLong,arr(5).trim.to Long) })
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.toRetractStreamRow
.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.toRetractStreamRow
.filter(_._1==true)
.print()
- 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.to Long) }).assignTimestampsAndWatermarks(
//引入Watermark
new BoundedOutOfOrdernessTimestampExtractorStationLog
{
//延迟2 秒
override def extractTimestamp(element: StationLog) = { element.callTime } })
//滚动窗口,窗口大小为5秒,
需求:统计每5秒内,每个基站的成功通话时长总和 tableEvn.registerDataStream(“t_station_log”,data,'sid,'callOut,'callIn,'callType,'callTim e.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.toRetractStreamRow
.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.toRetractStreamRow
//打印每个窗口的起始时间 .filter(._1==true)
.print() tableEvn.execute(“sql_api”)
Flink 性能优化
对于构建好的 Flink 集群,如何能够有效地进行集群以及任务方面的监控与优化是非常 重要的,尤其对于 7*24 小时运行的生产环境。
重点介绍 Checkpointing 的监控。
然后通过分析各种监控指标帮助用户更好地对 Flink 应用进行性能优化,以提高 Flink 任务执行的数据处理性能和效率。
- Checkpoint 页面监控与优化 Flink Web 页面中也提供了针对 Job Checkpointing 相关的监控信息,Checkpointing 监控页面中共有 Overview、History、Summary 和 Configuration 四个页签,
分别对 Checkpointing 从不同的角度进行了监控,每个页面中都包含了与 Checkpointing 相关的指 标。
- Overview 页签 Overview 页签中宏观地记录了 Flink 应用中 Checkpoints 的数量以及 Checkpoint 的最 新记录,包括失败和完成的 Checkpoints 记录。
Checkpoint Counts:包含了触发、进行中、完成、失败、重置等 Checkpoint 状态数量 统计。
Latest Completed Checkpoint:记录了最近一次完成的 Checkpoint 信息,包括结束时 间,端到端时长,状态大小等。
Latest Failed Checkpoint:记录了最近一次失败的 Checkpoint 信息。
Latest Savepoint:记录了最近一次 Savepoint 触发的信息。
Latest Restore:记录了最近一次重置操作的信息,包括从 Checkpoint 和 Savepoint 两种数据中重置恢复任务。 - 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,数据将同时写到外部持久化存储中。
- 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导致的系统稳定性问题。
- JobManager 配置 JobManager 在 Flink 系统中主要承担管理集群资源、接收任务、调度 Task、收集任务 状态以及管理 TaskManager 的功能,JobManager 本身并不直接参与数据的计算过程中,因 此 JobManager 的内存配置项不是特别多,只要指定 JobManager 堆内存大小即可。
jobmanager.heap.size:设定JobManager堆内存大小,默认为1024MB。 - TaskManager 配置 TaskManager作为Flink集群中的工作节点,所有任务的计算逻辑均执行在TaskManager 之上,因此对 TaskManager内存配置显得尤为重要,可以通过以下参数配置对 TaskManager 进行优化和调整。对应的官方文档是:
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 内存后的内存分配比例。该内存主要用于 TaskManager 任务排序、缓存中间结果等操作。例如, 如果设定为 0.8,则代表 TaskManager 保留 80%内存用于中间结果数据的缓存,剩下 20% 的 内 存 用 于 创 建 用 户 定 义 函 数 中 的 数 据 对 象 存 储 。 注 意 , 该 参 数 只 有 在 taskmanager.memory.size 不设定的情况下才生效。
taskmanager.memory.off-heap:设 置是 否开 启堆 外内 存供 Managed Memory 或 者 Network Buffers 使用。
taskmanager.memory.preallocate:设置是否在启动 TaskManager 过程中直接分配 TaskManager 管理内存。
taskmanager.numberOfTaskSlots:每个 TaskManager 分配的 slot 数量。
- 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^2204=5120 个,因此集群中配置 Network Buffer 内存的大小约为 300M 较为合适。 计算完 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 的内存比例。
taskmanager.network.memory.min: 最小的 Network Buffers 内存大小,默认为 64MB。
taskmanager.network.memory.max: 最大的 Network Buffers 内存大小,默认 1GB。
taskmanager.memory.segment-size: 内存管理器和 Network 栈使用的 Buffer 大小,默 认为 32KB。