Flink CEP
概念
Flink CEP是其实一个Flink库,跟机器学习库是一样的。它是为了更快,更及时的发现一些我们所关心的事情,而不是需要等待几天或则几个月相当长的时间,才发现问题。比如我们的银行卡被人盗刷,如果没有CEP,那么我们即使丢了银行卡,可能也不知道,等我们发现银行卡丢失后,再去挂失等,发现银行卡里已经没钱了。有了CEP,则可以及时提醒,存在银行卡被盗刷的可能性。
在应用系统中,总会发生这样或那样的事件,有些事件是用户触发的,有些事件是系统触发的,有些可能是第三方触发的,但它们都可以被看做系统中可观察的状态改变,例如用户登陆应用失败、用户下了一笔订单或传感器的消息。应对状态改变的策略可以分为两类,一类是简单事件处理(Simple event processing),一般简单事件处理会有两个步骤,过滤和路由,决定是否要处理,由谁处理,另一类是复杂事件处理(Complex event processing),复杂事件处理本身也会处理单一的事件,但其典型特质是需要对多个事件组成的事件流进行检测分析并响应。
在维基百科中也对CEP做了定义,“CEP是一种事件处理模式,它从若干源中获取事件,并侦测复杂环境的事件或模式,CEP的目的是确认一些有意义的事件(比如某种威胁或某种机会),并尽快对其作出响应”,可见CEP的主要特点包括:复杂性,需要在多源的事件流中进行检测;低延迟,秒级或毫秒级的响应,比如应对威胁;高吞吐,需要迅速对大量或者超大量事件流作出响应。以往的CEP框架往往处理大量收集到的事件,不能处理正在收集的事件。
CEP在生活中的各行各业可以有很多应用,比如金融行业的风险控制、欺诈识别、行情策略等等,比如安全领域的攻击告警、危险建模、漏洞发现等等,再比如智能交通、用户漏斗等等,再关联目前的IOT,其可应用的场景不胜枚举。需要进一步挖掘和改进Flink CEP的能力,为各种业务场景赋能和输出价值。
Flink作为目前大数据领域实时计算的主流计算框架,天然支持低延迟、高吞吐等特性,再加上Flink中的窗口模型和状态模型,更是对CEP提供了非常强大的支撑。Flink中专门实现了复杂事件处理的库——Flink CEP,用来方便的进行在事件流中检测事件模式。
快速入门
检测出a+ b
模型的数据,检查出用户的登陆风险指数。
var env=StreamExecutionEnvironment.getExecutionEnvironment
val stream = env.socketTextStream("localhost", 9999)
//1.定义一个Pattern,用于匹配复杂事件,例如实现 a+ b 匹配模式
val pattern: Pattern[String, String] = Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.timesOrMore(1) //匹配 a+
.consecutive()//表示a不可以被间断
.next("end") //连续模式 严格
.where(_.contains("b"))
//2.应用模式匹配
val patternStream:PatternStream[String] = CEP.pattern(stream, pattern)
patternStream.select(new PatternSelectFunction[String,String] {
override def select(pattern: util.Map[String, util.List[String]]): String = {
val startEvents: util.List[String] = pattern.get("begin")
val endEvent = pattern.get("end")
startEvents.asScala.mkString(" - ")+"\t"+endEvent.asScala.mkString(" | ")
}
})
.print()
env.execute("FlinkQuickStartapplication")
匹配模式
Flink CEP的核心在于模式匹配,对于不同模式匹配特性的支持,往往决定相应的CEP框架是否能够得到广泛应用。Flink CEP对模式提供了如下的一些支持:
Individual Patterns
Quantifiers
案例1
val pattern: Pattern[String, String] = Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.followedBy("middle")
.where(_.startsWith("b"))
.times(2)
.followedBy("end")
.where(_.contains("c"))
案例2
val pattern: Pattern[String, String] = Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.followedBy("middle")
.where(_.startsWith("b"))
.times(2)
.optional
.followedBy("end")
.where(_.contains("c"))
案例3
val pattern: Pattern[String, String] = Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.followedBy("middle")
.where(_.startsWith("b"))
.timesOrMore(1) //等价oneOrMore
.followedBy("end")
.where(_.contains("c"))
案例4
val pattern: Pattern[String, String] = Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.followedBy("middle")
.where(_.startsWith("b"))
.timesOrMore(1)
.greedy
.followedBy("end")
.where(_.contains("c"))
表示输入的条件同时满足begin和end的时候,是有限将匹配作为begin还是end,如果不设置greedy的话会将临界值作为end条件,例如以上a1 b1 bc
如果不加greedy则结果输出a1 b1 bc
如果加上greedy不会有结果输出,因为程序优先将匹配规则给start,所以不会输出.
Conditions
每一个pattern都可以指定被匹配的的条件,例如,判断当前值大于5 ,或者 值大于以前的平局值等。用户可以通过where、or、until等方法指定条件。这些方法的实现可以使 IterativeCondition
或者 SimpleCondition
.
IterativeCondition: 最普遍的情况。用户可以根据先前接受的事件的属性或部分事件的统计信息来指定接受后续事件的条件,一般用在oneOrMore场景中比较多,用户可以拿到历史信息然后计算是否添加该事件。
Simple Conditions:基于事件本身的属性来决定是否接受事件。
案例分析:例如以下场景收集用户订单信息 ,如果存在则不再收集,直到接收到pay信号之后,提取出用户的完整订单!
//001 apple 2 4.5 order
val stream = env.socketTextStream("localhost", 9999)
.keyBy(t=>t.split("\\s+")(0))
var pattern=Pattern.begin[String]("begin")
.where((v,ctx)=>{//Iterative Conditions
val list = ctx.getEventsForPattern("begin").toList
v.contains("order") && !list.contains(v)
})
.oneOrMore
.followedBy("next")
.where(log => log.contains("pay")) //Simple Conditions
Combining Conditions: 用户可以将子类型条件与其他条件组合。这适用于所有条件。您可以通过顺序调用where()任意组合条件。最终结果将是各个条件结果的逻辑与。要使用OR合并条件,可以使用or()方法
var pattern=Pattern.begin[String]("begin")
.where(_.equals("start")).or(_.startsWith("start"))
.followedBy("next")
.where(log => log.startsWith("stop"))
Stop condition:如果是循环模式,例如:oneOrMore()或者oneOrMore().optional您还可以指定停止条件。
var pattern=Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.oneOrMore
.until(_.equals("b"))
例如输入:a1 c a2 b a3
输出结果 a1
a1 a2
、a2
、a3
Combining Patterns
连续性(非循环)
除了Individual Patterns支持量化和条件,Flink CEP还支持Pattern间的组合模式,目前支持的有三种类型:
- Strict Contiguity: 期望所有匹配事件严格地一个接一个地出现,而在它们之间没有任何不匹配事件。
- Relaxed Contiguity: 忽略匹配事件之间出现的不匹配事件。
- Non-Deterministic Relaxed Contiguity: 进一步放松了连续性,允许其他匹配忽略某些匹配事件。
为了实现以上Pattern间的连续性,用户可以指定:
next()
, 严格地一个一个出现followedBy()
, 忽略匹配事件之间出现的不匹配事件。followedByAny()
, 进一步放松了连续性,允许其他匹配忽略某些匹配事件。
或者
notNext()
,不希望你的事件后面出现内容notFollowedBy()
, 不希望出现任何一个
现在就以a b
这种模式进行对比以上几种组合模式区别,同时对应a c b1 b2
输入
-
Strict Contiguity-严格连续
由于a个b之间被c阻断,因此这种情况没有任何输出
var pattern=Pattern.begin[String]("begin")
.where(_.contains("a"))
.next("next")
.where(_.contains("b"))
- Relaxed Contiguity-宽松连续
由于使用followedBy,因此允许a和b之间被阻断,因此输出a b1
var pattern=Pattern.begin[String]("begin")
.where(_.contains("a"))
.followedBy("next")
.where(_.contains("b"))
- Non-Deterministic Relaxed Contiguity-非确定性宽松连续
由于使用followedByAny,因此允许a和b之间被阻断,同时允许a匹配可以重复使用,因此输出a b1
和 a b2
var pattern=Pattern.begin[String]("begin")
.where(_.contains("a"))
.followedByAny("next")
.where(_.contains("b"))
连续性(循环)
用户可以在循环中使用匹配的连续性,默认循环中使用的是宽松连续性。用户可以使用以下的方法指定连续性:allowCombinations和consecutive用于指定循环元素间的连续性。为了描述这种关系,现在我们输入a b1 d1 b2 d2 b3 c
以下序列研究a b+ c
这种模式下的:严格连续、宽松连续以及非确定性连续。
- Strict Contiguity-严格连续
var pattern=Pattern.begin[String]("begin")
.where(_.contains("a"))
.followedByAny("middle")
.where(_.contains("b"))
.oneOrMore.consecutive().next("end").where(_.contains("c"))
要求b之间没有间隔不允许被阻断因此输出结果为 a b3 c
- Relaxed Contiguity-宽松连续
var pattern=Pattern.begin[String]("begin")
.where(_.contains("a"))
.followedByAny("middle")
.where(_.contains("b"))
.oneOrMore.followedBy("end").where(_.contains("c"))
因为b+允许被阻断,因此输出a b1 c
、a b1 b2 c
、a b1 b2 b3 c
、a b2 c
、a b2 b3 c
、a b3 c
- Non-Deterministic Relaxed Contiguity-非确定性宽松连续
var pattern=Pattern.begin[String]("begin")
.where(_.contains("a"))
.followedByAny("middle")
.where(_.contains("b"))
.oneOrMore.allowCombinations().followedBy("end").where(_.contains("c"))
不仅仅允许b被阻断,还允许b间的各种组合输出,因此输出a b1 c
、a b1 b2 c
、a b1 b2 b3 c
、a b2 c
、a b2 b3 c
、a b3 c
、a b1 b3 c
Groups of patterns
除了可以定义一个单一Pattern,也可以在begin、FollowedBy、FollowedByAny或者next后面定义一个Pattern的序列作为匹配的条件。
//1.定义一个Pattern,用于匹配复杂事件,例如实现 a+ b 匹配模式
val pattern: Pattern[String, String] = Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.followedBy(
Pattern.begin[String]("middle_begin").where(_.contains("b"))
.next("middle_next").where(_.contains("c"))
).oneOrMore
.followedBy("end").where(_.contains("d"))
.within(Time.seconds(5))
After Match Skip Strategy
After Match Skip Strategy
指的是当某一组事件成功匹配了某个模式之后,这组事件以何种方式参与后续的模式匹配。不同的匹配后策略会导致大相径庭的匹配结果,所以在实际开发中,需要小心的选择合适的匹配后策略。Flink CEP支持如下五种匹配后策略:
- NO_SKIP: 在一个事件完成匹配之后,保留所有匹配项,用于下次匹配
val pattern: Pattern[String, String] = Pattern.begin[String]("begin",AfterMatchSkipStrategy.noSkip())
.where(_.startsWith("b"))
.oneOrMore
.next("end")
.where(_.contains("c"))
- SKIP_TO_NEXT: 在一个事件完成匹配之后,丢弃掉所有匹配项,不可用于下次匹配
val pattern: Pattern[String, String] = Pattern.begin[String]("begin",AfterMatchSkipStrategy.skipToNext())
.where(_.startsWith("b"))
.oneOrMore
.next("end")
.where(_.contains("c"))
- SKIP_PAST_LAST_EVENT: 在匹配开始和匹配结束之前丢弃所有的局部匹配,仅仅输出一条
val pattern: Pattern[String, String] = Pattern.begin[String]("begin",AfterMatchSkipStrategy.skipPastLastEvent())
.where(_.startsWith("b"))
.oneOrMore
.next("end")
.where(_.contains("c"))
- SKIP_TO_FIRST: 仅仅输出第一次匹配成功的结果,然后跳跃到第一个匹配项,继续匹配后续结果!
val pattern: Pattern[String, String] = Pattern.begin[String]("begin",AfterMatchSkipStrategy.skipToFirst("begin"))
.where(_.contains("b"))
.oneOrMore
.next("end")
.where(_.contains("c"))
- SKIP_TO_LAST: 仅仅输出第一次结果,然后跳跃到最后一次匹配项输出最后结果!
val pattern: Pattern[String, String] = Pattern.begin[String]("begin",AfterMatchSkipStrategy.skipToLast("begin"))
.where(_.startsWith("b"))
.timesOrMore(1)
.next("end")
.where(_.contains("c"))
Detecting Patterns
在用户制定完匹配的序列之后,需要我们使用这个定义的模式去检测潜在的匹配。因此为了将流数据和检测模式运行在一起,我们需要创建一个PatternStrem.使用一个Stream作为input以及一个Pattern作为可选的比较器用于排列统一时间(EventTime)抵达的事件源。
val input : DataStream[Event] = ...
val pattern : Pattern[Event, _] = ...
var comparator : EventComparator[Event] = ... // optional
val patternStream: PatternStream[Event] = CEP.pattern(input, pattern, comparator)
输入的数据源可以是keyed 或者 non-keyed取决于用户的使用场景,将Pattern应用于non-keyed控制流将导致运行的job的并行度等于1。
Selecting from Patterns
一旦获得了PatternStream,就可以将转换应用于检测到的事件序列。建议的实现方法是通过PatternProcessFunction。
PatternProcessFunction具有processMatch方法,每个匹配事件序列都会调用该方法。它以Map <String,List <IN >>
的形式接收匹配,其中键是模式序列中每个模式的名称,值是该模式所有可接受事件的列表(IN是您的类型输入元素)。给定模式的事件按时间戳排序。返回每个模式的接受事件List<IN>
的原因是,当使用循环模式(例如oneToMany()和times())时,给定模式可能会接受多个事件。
class MyPatternProcessFunction<IN, OUT> extends PatternProcessFunction<IN, OUT> {
@Override
public void processMatch(Map<String, List<IN>> match, Context ctx, Collector<OUT> out) throws Exception;
IN startEvent = match.get("start").get(0);
IN endEvent = match.get("end").get(0);
out.collect(OUT(startEvent, endEvent));
}
}
PatternProcessFunction可以访问Context对象。有了它,就可以访问与时间相关的特征,例如currentProcessingTime或当前Match的timestamp(这是分配给Match的最后一个元素的时间戳)
class UserDefinePatternSelectFunction extends PatternProcessFunction[String,String]{
override def processMatch(matchInputs: util.Map[String, util.List[String]],
ctx: PatternProcessFunction.Context,
out: Collector[String]): Unit = {
val beginEvent = matchInputs.getOrDefault("begin",List[String]().asJava)
val endEvent = matchInputs.getOrDefault("end",List[String]().asJava)
out.collect(beginEvent.asScala.mkString(" , ")+"\t"+endEvent.asScala.mkString(" , "))
}
}
object FlinkCEPSelectPattern {
def main(args: Array[String]): Unit = {
var env=StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val stream = env.socketTextStream("localhost", 9999)
//1.定义一个Pattern,用于匹配复杂事件,例如实现 a+ b 匹配模式
val pattern: Pattern[String, String] = Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.timesOrMore(1) //匹配 a+
.consecutive()//表示a不可以被间断
.next("end") //连续模式 严格
.where(_.contains("b"))
//2.应用模式匹配
val patternStream:PatternStream[String] = CEP.pattern(stream, pattern)
patternStream.process(new UserDefinePatternSelectFunction)
.print()
env.execute("FlinkQuickStartapplication")
}
}
Handling Timed Out Partial Patterns
只要某个Pattern具有通过within
关键字指定了匹配时效长度,就有可能会丢弃部分事件序列,因为它们超过了窗口长度。要对超时的部分匹配采取行动,可以使TimedOutPartialMatchHandler接口。该接口应该以混合样式使用。这意味着您还可以使用PatternProcessFunction实现此接口。 TimedOutPartialMatchHandler提供了附加的processTimedOutMatch方法,将为每个超时的部分匹配调用该方法。
class UserDefinePatternSelectFunction(lateTag:OutputTag[String]) extends PatternProcessFunction[String,String] with TimedOutPartialMatchHandler[String]{
override def processMatch(matchInputs: util.Map[String, util.List[String]],
ctx: PatternProcessFunction.Context,
out: Collector[String]): Unit = {
val beginEvent = matchInputs.getOrDefault("begin",List[String]().asJava)
val endEvent = matchInputs.getOrDefault("end",List[String]().asJava)
out.collect(beginEvent.asScala.mkString(" , ")+"\t"+endEvent.asScala.mkString(" , "))
}
override def processTimedOutMatch(matchInputs: util.Map[String, util.List[String]], ctx: PatternProcessFunction.Context): Unit = {
val beginEvent = matchInputs.getOrDefault("begin",List[String]().asJava)
ctx.output(lateTag,beginEvent.asScala.mkString(" , "))
}
}
var env=StreamExecutionEnvironment.getExecutionEnvironment
env.setParallelism(1)
val stream = env.socketTextStream("localhost", 9999)
//1.定义一个Pattern,用于匹配复杂事件,例如实现 a+ b 匹配模式
val pattern: Pattern[String, String] = Pattern.begin[String]("begin")
.where(_.startsWith("a"))
.timesOrMore(1)
.consecutive()
.next("end")
.where(_.contains("b"))
.within(Time.seconds(5))
val late = new OutputTag[String]("late-data")
val patternStream:PatternStream[String] = CEP.pattern(stream, pattern)
var normalStream= patternStream.process(new UserDefinePatternSelectFunction(late))
normalStream.print()
normalStream.getSideOutput(late).printToErr("迟到")
env.execute("FlinkCEPSelectPatternLateData")