在实际应用中,有一类需求是要检测以特定顺序先后发生的一组事件,进行统计或作报警提示。
多个事件的组合我们将其称为复杂事件。对于多个复杂事件的处理,由于涉及到事件的严格顺序,有时还有时间约束,很难直接用SQL或者DataStream API来完成。可以用最强悍的工具——底层的处理函数,对于非常复杂的组合事件,可能需要设置很多状态、定时器,并在代码中定义各种条件分支逻辑来处理,复杂度很高,可能会使代码失去可读性。Flink提供了专门用于处理复杂事件的库——CEP,可以让我们更加轻松地解决这类棘手的问题
文章目录
基本概念
CEP介绍
CEP是复杂事件处理(Complex Event Processing)的缩写。Flink CEP是Flink实现的一个用于复杂事件处理的库
CEP是针对流处理而言的,分析的是低延迟、频繁产生的事件流。主要目的,就是在无界流中检测出特定的数据组合,方便我们掌握数据中重要的高阶特征
复杂事件处理:在事件流里,检测到特定的事件组合并进行处理。具体的过程是,把事件流中的一个个简单事件,通过一定的规则匹配组合起来,然后基于这些满足规则的一组组复杂事件进行转换处理,得到想要的结果进行输出
复杂事件处理的流程可分为三个步骤:
- 1、定义一个匹配规则
- 2、将匹配规则应用到事件流上,检测满足规则的复杂事件
- 3、对检测到的复杂事件进行处理,得到结果进行输出
输入:不同形状的事件流
匹配规则:在圆形后面紧跟着三角形
输出:针对检测到的复杂事件,处理之后输出一个提示或报警信息
模式
模式:CEP第一步所定义的匹配规则
模式的定义主要是两部分内容:
- 每个简单事件的特征
- 简单事件之间的组合关系
进一步扩展模式的功能,比如:匹配检测的时间限制;每个简单事件是否可以重复出现;对于事件可重复出现的模式,遇到一个匹配后是否可以跳过后面的匹配等
事件之间的组合关系,也就是事件发生的顺序,我们称之为近邻关系。
严格的近邻关系:两个事件之间不能有任何其他事件
宽松的近邻关系:相对顺序正确,中间可以由其他事件
另外,也可以进行反向定义,例如,谁后面不能跟谁
CEP在流上进行模式匹配,根据模式的近邻关系条件不同,可以检测连续的事件或不连续但先后发生的事件;模式还可以设定在时间范围内没有满足匹配条件,就会导致模式匹配超时(timeout)
应用场景
CEP主要用于实时流数据的分析处理,CEP可以帮助在复杂的、看似不相关的事件流中找出哪些有意义的事件组合,进而可以接近实时地进行分析判断、输出通知信息或报警,这在企业项目的风控管理、用户画像、运维监控中都有重要应用
- 风险控制:设定一些行为模式,针对用户的异常行为进行实时检测。当一个用户行为符合了异常行为模式,比如短时间内频繁登录并失败、大量下单却不支付(刷单),就可以向用户发送通知信息,或是进行报警提示、由人工进一步判定用户是否有违规操作的嫌疑。这样就可以有效地控制用户个人和平台的风险
- 用户画像:利用 CEP 可以用预先定义好的规则,对用户的行为轨迹进行实时跟踪,从而检测出具有特定行为习惯的一些用户,做出相应的用户画像。基于用户画像可以进行精准营销,即对行为匹配预定义规则的用户实时发送相应的营销推广
- 运维监控:对于企业服务的运维管理,利用CEP灵活配置多指标、多依赖来实现更复杂的监控模式
CEP 的应用场景非常丰富。很多大数据框架,如 Spark、Samza、Beam 等都提供了不同的CEP 解决方案,但没有专门的库(library)。而 Flink 提供了专门的 CEP 库用于复杂事件处理,可以说是目前 CEP 的最佳解决方案
快速上手
引入依赖
在代码中使用Flink CEP,需要在项目的pom文件中添加相关依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
为了精简和避免依赖冲突,Flink会保持尽量少的核心依赖,核心依赖中不包括任何的连接器和库,库中友SQL、CEP、ML等等。若想要在Flink集群中提交运行CEP作业,将依赖的jar包放在/lib目录下
简单实例
例子:检测用户行为,如果连续三次登录失败,就输出报警信息
定义一个登录事件POJO类
public class LoginEvent {
public String userId;
public String ipAddress;
public String eventType;
public Long timestamp;
public LoginEvent() {
}
public LoginEvent(String userId, String ipAddress, String eventType, Long timestamp) {
this.userId = userId;
this.ipAddress = ipAddress;
this.eventType = eventType;
this.timestamp = timestamp;
}
@Override
public String toString() {
return "LoginEvent{" +
"userId='" + userId + '\'' +
", ipAddress='" + ipAddress + '\'' +
", eventType='" + eventType + '\'' +
", timestamp=" + timestamp +
'}';
}
}
业务逻辑的编写。Flink CEP在代码中主要通过Pattern API来实现,分三步:
- 1、定义一个模式
- 2、将Pattern应用到DataStream上,检测满足规则的复杂事件,得到一个PatternStream
- 3、将PatternStream进行转换处理,将检测到的复杂事件提取出来,包装成报警信息输出
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternSelectFunction;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import java.util.List;
import java.util.Map;
public class LoginFailDetectExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//1、获取登录事件流,并提取时间戳、生成水位线
KeyedStream<LoginEvent, String> loginEventstream = env.fromElements(
new LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
new LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
new LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 7000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 8000L),
new LoginEvent("user_2", "192.168.1.29", "success", 6000L)
).assignTimestampsAndWatermarks(WatermarkStrategy.<LoginEvent>forMonotonousTimestamps()
.withTimestampAssigner(
new SerializableTimestampAssigner<LoginEvent>() {
@Override
public long extractTimestamp(LoginEvent loginEvent, long l) {
return loginEvent.timestamp;
}
}
)
).keyBy(r -> r.userId);
//2、定义模式,连续三次登录失败
Pattern<LoginEvent, LoginEvent> pattern = Pattern.<LoginEvent>begin("first") // 第一次登录失败事件
.where(new SimpleCondition<LoginEvent>() {
@Override
public boolean filter(LoginEvent value) throws Exception {
return value.eventType.equals("fail");
}
})
.next("second") //紧跟着第二次登录失败事件
.where(new SimpleCondition<LoginEvent>() {
@Override
public boolean filter(LoginEvent value) throws Exception {
return value.eventType.equals("fail");
}
})
.next("third") //紧跟着第三次登录失败事件
.where(new SimpleCondition<LoginEvent>() {
@Override
public boolean filter(LoginEvent value) throws Exception {
return value.eventType.equals("fail");
}
});
//3.将模式应用到数据流上,检测复杂事件
PatternStream<LoginEvent> patternStream = CEP.pattern(loginEventstream.keyBy(event -> event.userId), pattern);
//4.将检测到的复杂事件提取出来,进行处理得到报警信息输出
SingleOutputStreamOperator<String> warningStream = patternStream.select(new PatternSelectFunction<LoginEvent, String>() {
@Override
public String select(Map<String, List<LoginEvent>> map) throws Exception {
// 提取复杂事件中的三次登录失败事件
LoginEvent firstFailEvent = map.get("first").get(0);
LoginEvent secondFailEvent = map.get("second").get(0);
LoginEvent thirdFailEvent = map.get("third").get(0);
return firstFailEvent.userId + " 连续三次登录失败! 登录时间 " +
firstFailEvent.timestamp + ", " +
secondFailEvent.timestamp + ", " +
thirdFailEvent.timestamp;
}
});
//打印输出
warningStream.print();
env.execute();
}
}
模式API
Flink CEP的核心是复杂事件的模式匹配。Flink CEP库中提供了Pattern类,基于它可以调用一系列方法来定义匹配模式,这就是所谓模式API
个体模式
基于流中事件的匹配是有先后顺序的,一个匹配规则可以表达成先后发生的一个个简单事件,按顺序串联组合在一起。这里的每一个简单事件都有一定的条件规则,称为个体模式
每个个体模式都是以连接词开始定义的,比如begin、next等,这些是Pattern对象的一个方法,返回还是Pattern对象。连接词方法有一个String类型参数,这时当前个体模式唯一的名字,在之后检测到匹配事件时,以这个名字指代匹配事件
个体模式需要一个过滤条件,用来指定具体的匹配规则,这个条件一般是通过调用where()方法实现,具体的过滤逻辑则是通过传入SimpleCondition内的filter()方法定义
另外,个体模式可以匹配接收一个或多个事件。给个体模式增加一个量词就可以让其循环匹配,接收多个事件
量词
个体模式后面可以跟一个量词,用来指定循环的次数。在这个角度分类,个体模式可分为单例模式、循环模式
- 单例模式:匹配接收一个事件
- 循环模式:匹配接收多个事件
在Flink CEP中,可以使用不同的方法指定循环模式,主要有:
- oneOrMore():匹配事件出现一次或多次。假如a是一个个体模式,a.oneOrMore()表示可以匹配1个或多个a的事件组合
- times(times):匹配事件发生特定次数(times),例如a.times(3)表示aaa
- times(fromTimes,toTimes):指定匹配事件出现的次数范围,最小次数为fromTimes,最大次数为toTimes
- greedy():只能用在循环模式后,使当前循环模式变得贪心。尽可能多的去匹配
- optional():当前模式为可选,可以满足这个匹配条件,也可以不满足
对于一个个体模式pattern来说,后面所有可以添加的量词如下:
// 匹配事件出现 4 次
pattern.times(4);
// 匹配事件出现 4 次,或者不出现
pattern.times(4).optional();
// 匹配事件出现 2, 3 或者 4 次
pattern.times(2, 4);
// 匹配事件出现 2, 3 或者 4 次,并且尽可能多地匹配
pattern.times(2, 4).greedy();
// 匹配事件出现 2, 3, 4 次,或者不出现
pattern.times(2, 4).optional();
// 匹配事件出现 2, 3, 4 次,或者不出现;并且尽可能多地匹配
pattern.times(2, 4).optional().greedy();
// 匹配事件出现 1 次或多次
pattern.oneOrMore();
// 匹配事件出现 1 次或多次,并且尽可能多地匹配
pattern.oneOrMore().greedy();
// 匹配事件出现 1 次或多次,或者不出现
pattern.oneOrMore().optional();
// 匹配事件出现 1 次或多次,或者不出现;并且尽可能多地匹配
pattern.oneOrMore().optional().greedy();
// 匹配事件出现 2 次或多次
pattern.timesOrMore(2);
// 匹配事件出现 2 次或多次,并且尽可能多地匹配
pattern.timesOrMore(2).greedy();
// 匹配事件出现 2 次或多次,或者不出现
pattern.timesOrMore(2).optional()
// 匹配事件出现 2 次或多次,或者不出现;并且尽可能多地匹配
pattern.timesOrMore(2).optional().greedy();
条件
对于每个个体模式,匹配事件的核心在于定义匹配条件,选取事件的规则,Flink CEP会按照这个规则对流中的事件进行筛选,判断是否接受当前事件
主要通过调用Pattern调用的where()实现,主要可以分为简单条件、迭代条件、复合条件、终止条件几种类型。此外,也可以调用Pattern对象的subtype()方法来限定匹配事件的子类型
-
限定子类型:调用subtype()方法wield当前模式增加子类型限制条件
pattern.subtype(SubEvent.class);
-
简单条件:最简单的匹配规则,根据当前的事件特征来决定是否接受它,本质上是一个filter操作
pattern.where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) { return value.user.startsWith("A");//要求匹配事件的属性以“A”开头 } });
-
迭代条件:依靠之前事件作判断
middle.oneOrMore() .where(new IterativeCondition<Event>() { @Override public boolean filter(Event value, Context<Event> ctx) throws Exception { // 事件中的 user 必须以 A 开头 if (!value.user.startsWith("A")) { return false; } int sum = value.amount; // 获取当前模式之前已经匹配的事件,求所有事件 amount 之和 for (Event event : ctx.getEventsForPattern("middle")) { sum += event.amount; } // 在总数量小于 100 时,当前事件满足匹配规则,可以匹配成功 return sum < 100; } });
-
组合条件:独立定义多个条件,在外部把它们连接起来,构成一个组合条件。逻辑与where().where,逻辑或where().or()。子类型限定条件也可以和其他条件结合,成为组合条件
pattern.subtype(SubEvent.class) .where(new SimpleCondition<SubEvent>() { @Override public boolean filter(SubEvent value) { return ... // some condition } });
-
终止条件:遇到某个特定事件时当前模型不再继续循环匹配了,调用模式对象的until()。终止条件只与oneOrMore()或者oneOrMore().optional()结合使用
组合模式
多个个体模式组合起来的完整模式,叫做组合模式,也称序列模式
Pattern<Event, ?> pattern = Pattern
.<Event>begin("start").where(...)
.next("next").where(...)
.followedBy("follow").where(...)
...
1、初始模式(Initial Pattern)
组合模式都必须以初始模式开头,初始模式必须调用Pattern的静态方法begin()来创建
Pattern<Event, ?> start = Pattern.<Event>begin("start");
Pattern有两个泛型参数,第一个是检测事件的基本类型Event,第二个是当前模式例事件的子类,由于子类型限制条件指定,这里用类型通配符(?)替代
2、近邻条件(Contiguity Conditions)
指明先后事件之间的近邻关系,Flink CEP中提供了三种近邻关系:
- 严格近邻(Strict Contiguity):匹配的事件严格按顺序出现,中间不会有任何其他事件,next()
- 宽松近邻(Relaxed Contiguity):两个匹配事件的相对顺序,默认采用,followedBy()
- 非确定性宽松近邻(Non-Deterministic Relaxed Contiguity):可以重复使用之前已经匹配过的事件;以同一个事件作为开始匹配,followedByAny()
3、其他限制条件
我们还可以用否定的连接词来组合个体模式
- notNext():事件后面不能紧跟某事件
- notFollowedBy():两个事件中间不会出现某种事件
Flink CEP还可以为模式指定一个时间限制,调用within()方法传入时间参数,模式序列中第一个事件到最后一个事件之间的最大时间间隔。一个模式序列中只能有一个时间限制,调用within()位置不限,多次调用取最小时间间隔
// 严格近邻条件
Pattern<Event, ?> strict = start.next("middle").where(...);
// 宽松近邻条件
Pattern<Event, ?> relaxed = start.followedBy("middle").where(...);
// 非确定性宽松近邻条件
Pattern<Event, ?> nonDetermin = start.followedByAny("middle").where(...);
// 不能严格近邻条件
Pattern<Event, ?> strictNot = start.notNext("not").where(...);
// 不能宽松近邻条件
Pattern<Event, ?> relaxedNot = start.notFollowedBy("not").where(...);
// 时间限制条件
middle.within(Time.seconds(10));
4、循环模式中的近邻条件
-
consecutive():为循环模式中的匹配事件增加严格的近邻条件,保证所有匹配事件是严格连续的
// 1. 定义 Pattern,登录失败事件,循环检测 3 次 Pattern<LoginEvent, LoginEvent> pattern = Pattern .<LoginEvent>begin("fails") .where(new SimpleCondition<LoginEvent>() { @Override public boolean filter(LoginEvent loginEvent) throws Exception { return loginEvent.eventType.equals("fail"); } }).times(3).consecutive();
-
allowCombinations():非确定性宽松近邻条件,表示可以重复使用已经匹配的事件,调用allowCombinations(),效果与followedByAny()相同
模式组
在一些非常复杂的场景中,可能需要划分多个阶段,每个阶段又有一连串的匹配规则,为了应对这样的需求,Flink CEP 允许我们以嵌套的方式来定义模式
在模式组中,每一个模式序列被当做某一阶段的匹配条件,返回的类型是一个GroupPatter。GroupPattern是Pattern的子类
// 以模式序列作为初始模式
Pattern<Event, ?> start = Pattern.begin(
Pattern.<Event>begin("start_start").where(...)
.followedBy("start_middle").where(...)
);
// 在 start 后定义严格近邻的模式序列,并重复匹配两次
Pattern<Event, ?> strict = start.next(
Pattern.<Event>begin("next_start").where(...)
.followedBy("next_middle").where(...)
).times(2);
// 在 start 后定义宽松近邻的模式序列,并重复匹配一次或多次
Pattern<Event, ?> relaxed = start.followedBy(
Pattern.<Event>begin("followedby_start").where(...)
.followedBy("followedby_middle").where(...)
).oneOrMore();
//在 start 后定义非确定性宽松近邻的模式序列,可以匹配一次,也可以不匹配
Pattern<Event, ?> nonDeterminRelaxed = start.followedByAny(
Pattern.<Event>begin("followedbyany_start").where(...)
.followedBy("followedbyany_middle").where(...)
).optional();
匹配后跳过策略
匹配后跳过策略(After Match Skip Strategy),专门用来精准控制循环模式的匹配结果,该策略在Pattern的初始模式定义中,作为begin()的第二个参数传入
Pattern.begin("start", AfterMatchSkipStrategy.noSkip())
.where(...)
...
下面举例来说明不同的跳过策略,例如:开始是事件a,可以重复一次或多次,后面跟着一个事件b,a和b直接可以有其他事件,简写形式为a+followedBy b
Pattern.<Event>begin("a").where(new SimpleCondition<Event>() {
@Override
public boolean filter(Event value) throws Exception {
return value.user.equals("a");
}
}).oneOrMore()
.followedBy("b").where(new SimpleCondition<Event>() {
@Override
public boolean filter(Event value) throws Exception {
return value.user.equals("b");
}
});
输入事件序列:a a a b,区分不同a事件记为 a1 a2 a3 b,检测匹配6个结果:(a1 a2 a3 b),(a1 a2 b),(a1 b),(a2 a3 b),(a2 b),(a3 b)
初始模式的量词.oneOrMore()后加上.greedy()定义为贪心匹配,那么结果就是:(a1 a2 a3 b)、(a2 a3 b)、(a3 b)
讨论不同跳过策略对匹配结果的影响:
- 不跳过NO_SKIP:AfterMatchSkipStrategy.noSkip(),默认策略。所有可能得匹配都会输出,也就是完整的6个匹配
- 跳至下一个SKIP_TO_NEXT:AfterMatchSkipStrategy.skipToNext(),找到a1开始的最大匹配,直接开始a2的最大匹配,依次类推。结果为(a1 a2 a3 b)、(a2 a3 b)、(a3 b)
- 跳过所有子匹配SKIP_PAST_LAST_EVENT:找到a1开始的匹配(a1 a2 a3 b),直接跳过所有a1至a3开头的匹配,最终得到(a1 a2 a3 b),最为精简的跳过策略
- 跳至第一个SKIP_TO_FIRST[A]:AfterMatchSkipStrategy.skipToFirst(“a”),得到以a1为开始的匹配,最终得到(a1 a2 a3 b)、(a1 a2 b)、(a1 b)
- 跳至最后一个SKIP_TO_LAST[a]:AfterMatchSkipStrategy.skipToLast(“a”),得到以a3为结束的匹配,最终得到(a1 a2 a3 b)、(a2 a3 b)、(a3 b)
模式的检测处理
将模式应用到事件流上、检测提取匹配的复杂事件并定义处理转换的方法,最终得到想要的输出信息
将模式应用到流上
DataStream<Event> inputStream = ...
Pattern<Event, ?> pattern = ...
PatternStream<Event> patternStream = CEP.pattern(inputStream, pattern);d
DataStream也可以通过keyBy进行按键分区得到KeyedSream,接下来对于复杂事件的检测就会针对不同的key单独进行
模式中定义的复杂事件,发送有先后顺序。默认采用事件时间语义,按各自的时间戳;处理时间语义,数据到达顺序。若时间戳相同或同时到达的事件,可传入一个比较器作为第三个参数来排序
// 可选的事件比较器
EventComparator<Event> comparator = ...
PatternStream<Event> patternStream = CEP.pattern(input, pattern, comparator);
处理匹配事件
PatternStream的转换操作主要可以分成两种:简单便捷的选择提取(select)、更加通用强大的处理(process),具体实现是在调用API时传入一个函数类。
1、匹配事件的选择提取
基于PatternStream直接调用select()方法,传入一个PatternSelectFunction参数
PatternStream<Event> patternStream = CEP.pattern(inputStream, pattern);
DataStream<String> result = patternStream.select(new MyPatternSelectFunction());
这里的事件名称就对应着模式中定义的每个个体模式的名称,个体可以是循环模式,一个名称会对应多个事件,最终会保存在Map里的value就是一个事件的列表(List)
class MyPatternSelectFunction implements PatternSelectFunction<Event, String>{
@Override
public String select(Map<String, List<Event>> pattern) throws Exception {
Event startEvent = pattern.get("start").get(0);
Event middleEvent = pattern.get("middle").get(0);
return startEvent.toString() + " " + middleEvent.toString();
}
}
基于PatternFlatSelectFunction,扁平化版本,通过收集器Collertor,调用collect()方法就可以实现多次发送输出数据
patternStream.flatSelect(new PatternFlatSelectFunction<LoginEvent, String>() {
@Override
public void flatSelect(Map<String, List<LoginEvent>> map,
Collector<String> out) throws Exception {
LoginEvent first = map.get("fails").get(0);
LoginEvent second = map.get("fails").get(1);
LoginEvent third = map.get("fails").get(2);
out.collect(first.userId + " 连续三次登录失败!登录时间:" + first.timestamp +
", " + second.timestamp + ", " + third.timestamp);
}
}).print("warning");
2、匹配事件的通用处理
自1.8版本之后,引入了匹配事件的通用检测处理方式,直接调用PatternStream的process()方法,传入PatternProcessFunction,该功能丰富、调用灵活。PatternSelectFunction 和 PatternFlatSelectFunction在CEP内部执行都会被转为PatternProcessFunction
// 3. 将匹配到的复杂事件选择出来,然后包装成报警信息输出
patternStream.process(new PatternProcessFunction<LoginEvent, String>() {
@Override
public void processMatch(Map<String, List<LoginEvent>> map, Context ctx,
Collector<String> out) throws Exception {
LoginEvent first = map.get("fails").get(0);
LoginEvent second = map.get("fails").get(1);
LoginEvent third = map.get("fails").get(2);
out.collect(first.userId + " 连续三次登录失败!登录时间:" + first.timestamp +
", " + second.timestamp + ", " + third.timestamp);
}
}).print("warning");
处理超时事件
复杂事件的检测结果一般只有两种:要么匹配,要么不匹配。检测处理的过程具体如下:
- (1)当前事件复合模式匹配的条件,接受该事件,保存到对应的Map中
- (2)在模式序列定义中,当前事件后面还应该有其他事件,就继续读取事件流进行检测;如果模式序列的定义已经全部满足,那么就成功检测到了一组匹配的复杂事件,调用PatternProcessFunction 的 processMatch()方法进行处理
- (3)当前事件不符合模式匹配的条件,丢弃该事件
- (4)当前事件破坏模式序列中定义的限制条件,如不满足严格近邻要求,那么当前已检测的一组部分匹配事件丢弃,重新开始检查
在有时间限制的情况下,用within()指定了模式检测的时间间隔,超出这个时间当前这组检测就应该失败,然而这种超时失败跟真正的匹配失败不同,它是一种部分成功匹配。只有在开头能够正常匹配的前提下,没有等到后续的匹配事件才会超时,故往往不应该丢弃,而使需要输出一个提示或报警信息。这要求我们有能力捕获并处理超时事件
1、使用 PatternProcessFunction 的侧输出流
TimedOutPartialMatchHandler接口实现processTimedOutMatch()方法,将超时的、已检测到的部分匹配事件放在一个Map中,作为方法的第一个参数,方法的第二个参数为Context
class MyPatternProcessFunction extends PatternProcessFunction<Event, String>
implements TimedOutPartialMatchHandler<Event> {
// 正常匹配事件的处理
@Override
public void processMatch(Map<String, List<Event>> match, Context ctx,
Collector<String> out) throws Exception{
...
}
// 超时部分匹配事件的处理
@Override
public void processTimedOutMatch(Map<String, List<Event>> match, Context ctx) throws Exception{
Event startEvent = match.get("start").get(0);
OutputTag<Event> outputTag = new OutputTag<Event>("time-out"){};
ctx.output(outputTag, startEvent);
}
}
在 processTimedOutMatch()方法中定义了一个输出标签(OutputTag),调用 ctx.output() 方法,就可以将超时的部分匹配事件输出到标签所标识的侧输出流了
2、使用PatternTimeoutFunction
PatternTimeoutFunction是早期版本中用于捕获超时事件的接口。需要实现一个timeout()方法,会将部分匹配的事件放在一个Map中作为参数传入,此外还有一个参数是当前的时间戳。提取部分匹配事件进行处理转换后,可以将通知或报警信息输出
调用select()方法后会得到唯一的DataStream,正常匹配事件的处理结果会进入转换后得到的DataStream,超时事件的处理结果则会进入侧输出流,侧输出流需要另外传如一个侧输出标签来指定
PatternStream的select()方法需要传入三个参数:侧输出标签、超时事件处理函数、匹配事件提取函数
// 定义一个侧输出流标签,用于标识超时侧输出流
OutputTag<String> timeoutTag = new OutputTag<String>("timeout"){};
// 将匹配到的,和超时部分匹配的复杂事件提取出来,然后包装成提示信息输出
SingleOutputStreamOperator<String> resultStream = patternStream
.select(timeoutTag,
// 超时部分匹配事件的处理
new PatternTimeoutFunction<Event, String>() {
@Override
public String timeout(Map<String, List<Event>> pattern, long timeoutTimestamp) throws Exception {
Event event = pattern.get("start").get(0);
return "超时:" + event.toString();
}
},
// 正常匹配事件的处理
new PatternSelectFunction<Event, String>() {
@Override
public String select(Map<String, List<Event>> pattern) throws Exception
{
...
}
}
);
// 将正常匹配和超时部分匹配的处理结果流打印输出
resultStream.print("matched");
resultStream.getSideOutput(timeoutTag).print("timeout");
在超时事件处理的过程中,Map里只能取到已经检测到匹配的哪些事件,如果取可能未匹配的事件并调用它的对象方法,则可能会报空指针异常
3、应用实例
在电商平台中,最终创造收入和利润的是用户下单购买的环节。用户下单的行为可以表明用户对商品的需求,但在现实中,并不是每次下单都会被用户立刻支付。当拖延一段时间后,用户支付的意愿会降低。所以为了让用户更有紧迫感从而提高支付转化率,同时也为了防范订单支付环节的安全风险,电商网站往往会对订单状态进行监控,设置一个失效时间(比如 15 分钟),如果下单后一段时间仍未支付,订单就会被取消
定义POJO类OrderEvent
public class OrderEvent {
public String userId;
public String orderId;
public String eventType;
public Long timestamp;
public OrderEvent() {
}
public OrderEvent(String userId, String orderId, String eventType, Long timestamp) {
this.userId = userId;
this.orderId = orderId;
this.eventType = eventType;
this.timestamp = timestamp;
}
@Override
public String toString() {
return "OrderEvent{" +
"userId='" + userId + '\'' +
", orderId='" + orderId + '\'' +
", eventType='" + eventType + '\'' +
", timestamp=" + timestamp +
'}';
}
}
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.functions.PatternProcessFunction;
import org.apache.flink.cep.functions.TimedOutPartialMatchHandler;
import org.apache.flink.cep.pattern.Pattern;
import org.apache.flink.cep.pattern.conditions.SimpleCondition;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.Collector;
import org.apache.flink.util.OutputTag;
import java.time.Duration;
import java.util.List;
import java.util.Map;
public class OrderTimeoutDetectExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 1.获取数据流
SingleOutputStreamOperator<OrderEvent> orderEventStream = env.fromElements(
new OrderEvent("user_1", "order_1", "create", 1000L),
new OrderEvent("user_2", "order_2", "create", 2000L),
new OrderEvent("user_1", "order_1", "modify", 10 * 1000L),
new OrderEvent("user_1", "order_1", "pay", 60 * 1000L),
new OrderEvent("user_2", "order_3", "create", 10 * 60 * 1000L),
new OrderEvent("user_2", "order_3", "pay", 20 * 60 * 1000L)
).assignTimestampsAndWatermarks(WatermarkStrategy.<OrderEvent>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<OrderEvent>() {
@Override
public long extractTimestamp(OrderEvent element, long recordTimestamp) {
return element.timestamp;
}
}));
// 2.定义模式
Pattern<OrderEvent, OrderEvent> pattern = Pattern.<OrderEvent>begin("create")
.where(new SimpleCondition<OrderEvent>() {
@Override
public boolean filter(OrderEvent value) throws Exception {
return value.eventType.equals("create");
}
})
.followedBy("pay")
.where(new SimpleCondition<OrderEvent>() {
@Override
public boolean filter(OrderEvent value) throws Exception {
return value.eventType.equals("pay");
}
})
.within(Time.minutes(15));
// 3.将模式应用到订单数据流上
PatternStream<OrderEvent> patternStream = CEP.pattern(orderEventStream.keyBy(event -> event.orderId), pattern);
// 4.定义一个侧输出流标签
OutputTag<String> timeoutTag = new OutputTag<String>("timeout") {
};
// 5.将完全匹配和超时部分匹配的复杂事件提取出来,进行处理
SingleOutputStreamOperator<String> result = patternStream.process(new OrderPayMatch());
// 打印输出
result.print("payed:");
result.getSideOutput(timeoutTag).print("timeout:");
env.execute();
}
// 自定义PatternProcessFunction
public static class OrderPayMatch extends PatternProcessFunction<OrderEvent, String> implements TimedOutPartialMatchHandler<OrderEvent> {
@Override
public void processMatch(Map<String, List<OrderEvent>> map, Context context, Collector<String> collector) throws Exception {
//获取当前的支付事件
OrderEvent payEvent = map.get("pay").get(0);
collector.collect("用户" + payEvent.userId + " 订单 " + payEvent.orderId + " 已支付");
}
@Override
public void processTimedOutMatch(Map<String, List<OrderEvent>> map, Context context) throws Exception {
OrderEvent createEvent = map.get("create").get(0);
OutputTag<String> timeoutTag = new OutputTag<String>("timeout") {
};
context.output(timeoutTag, "用户" + createEvent.userId + " 订单 " + createEvent.orderId + " 超时未支付");
}
}
}
处理迟到的数据
Flink CEP沿用了通过设置水位线延迟来处理乱序数据的做法。当一个事件到来时,并不会立即做检测匹配处理,而是先放入一个缓冲区(buffer)。缓冲区内的数据会按照时间戳由小到大排序;当一个水位线到来时,就会将缓冲区中所有时间戳小于水位线的事件依次取出,进行检测匹配。这样就保证了匹配事件的顺序和事件时间的进展一致,处理的 顺序就一定是正确的。这里水位线的延迟时间,也就是事件在缓冲区等待的最大时间
水位线延迟时间不可能保证将所有乱序数据完美包括进来,总 会有一些事件延迟比较大,以至于等它到来的时候水位线早已超过了它的时间戳。借鉴窗口的做法,Flink CEP 同样提供了将迟到事件输出到侧输出流的方式: 我们可以基于 PatternStream 直接调 用.sideOutputLateData()方法,传入一个 OutputTag,将迟到数据放入侧输出流另行处理
PatternStream<Event> patternStream = CEP.pattern(input, pattern);
// 定义一个侧输出流的标签
OutputTag<String> lateDataOutputTag = new OutputTag<String>("late-data"){};
SingleOutputStreamOperator<ComplexEvent> result = patternStream
.sideOutputLateData(lateDataOutputTag) // 将迟到数据输出到侧输出流
.select(
// 处理正常匹配数据
new PatternSelectFunction<Event, ComplexEvent>() {...}
);
// 从结果中提取侧输出流
DataStream<String> lateData = result.getSideOutput(lateDataOutputTag);
CEP的状态机实现
通过分析CEP的检测处理流程,可以认为检测匹配事件的过程中会有初始(没有任何匹配)、检查中(部分匹配成功)、匹配成功、匹配失败等不同的状态。随着每个事件的到来,都会改变当前检测的状态;而这种改变跟当前事件的特性有关、也跟当前所处的状态有关。这样的系统其实就是一个状态机,这也是正则表达式底层引擎的实现原理
Flink CEP的底层工作原理与正则表达式是一致的,是一个非确定有限状态自动机(Nondeterministic Finite Automaton,NFA)
import org.apache.flink.api.common.functions.RichFlatMapFunction;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.util.Collector;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.KeyedStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import java.io.Serializable;
import static org.apache.flink.util.Preconditions.checkNotNull;
public class NFAExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 获取登录事件流,这里与时间无关,就不生成水位线了
KeyedStream<LoginEvent, String> stream = env.fromElements(
new LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
new LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
new LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
new LoginEvent("user_2", "192.168.1.29", "success", 6000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 7000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 8000L)
).keyBy(r -> r.userId);
// 将数据依次输入状态机进行处理
DataStream<String> alertStream = stream.flatMap(new StateMachineMapper());
alertStream.print("warning");
env.execute();
}
@SuppressWarnings("serial")
public static class StateMachineMapper extends RichFlatMapFunction<LoginEvent, String> {
// 声明当前用户对应的状态
private ValueState<State> currentState;
@Override
public void open(Configuration conf) {
// 获取状态对象
currentState = getRuntimeContext().getState(new ValueStateDescriptor<>("state", State.class));
}
@Override
public void flatMap(LoginEvent event, Collector<String> out) throws Exception {
// 获取状态,如果状态为空,置为初始状态
State state = currentState.value();
if (state == null) {
state = State.Initial;
}
// 基于当前状态,输入当前事件时跳转到下一状态
State nextState = state.transition(event.eventType);
if (nextState == State.Matched) {
// 如果检测到匹配的复杂事件,输出报警信息
out.collect(event.userId + " 连续三次登录失败");
// 需要跳转回 S2 状态,这里直接不更新状态就可以了
} else if (nextState == State.Terminal) {
// 如果到了终止状态,就重置状态,准备重新开始
currentState.update(State.Initial);
} else {
// 如果还没结束,更新状态(状态跳转),继续读取事件
currentState.update(nextState);
}
}
}
// 状态机实现
public enum State {
Terminal, // 匹配失败,当前匹配终止
Matched, // 匹配成功
// S2 状态
S2(new Transition("fail", Matched), new Transition("success", Terminal)),
// S1 状态
S1(new Transition("fail", S2), new Transition("success", Terminal)),
// 初始状态
Initial(new Transition("fail", S1), new Transition("success", Terminal));
private final Transition[] transitions; // 状态转移规则
// 状态的构造方法,可以传入一组状态转移规则来定义状态
State(Transition... transitions) {
this.transitions = transitions;
}
// 状态的转移方法,根据当前输入事件类型,从定义好的转移规则中找到下一个状态
public State transition(String eventType) {
for (Transition t : transitions) {
if (t.getEventType().equals(eventType)) {
return t.getTargetState();
}
}
// 如果没有找到转移规则,说明已经结束,回到初始状态
return Initial;
}
}
// 定义状态转移类,包括两个属性:当前事件类型和目标状态
public static class Transition implements Serializable {
private static final long serialVersionUID = 1L;
// 触发状态转移的当前事件类型
private final String eventType;
// 转移的目标状态
private final NFAExample.State targetState;
public Transition(String eventType, NFAExample.State targetState) {
this.eventType = checkNotNull(eventType);
this.targetState = checkNotNull(targetState);
}
public String getEventType() {
return eventType;
}
public NFAExample.State getTargetState() {
return targetState;
}
}
}
总结
Flink CEP 是 Flink 对复杂事件处理提供的强大而高效的应用库,在大数据分析方面,应用场景主要可以分为统计分析和逻辑分析。企业的报表统计、商业决策都离不开统计分析,这部分需求在目前企业的分析指标中占了很大的比重,实时的流数据统计可以通过 Flink SQL 方便地实现;而逻辑分析可以进一步细分为风险控制、数据挖掘、用户画像、精准推荐等各个应用场景,如今对实时性要求也越来越高,Flink CEP 就可以作为对流数据进行逻辑分析、进行实时风控和推荐的有力工具
DataStream API和处理函数是Flink应用的基石,而SQL和CEP就是Flink大厦顶层扩展的两大工具。Flink SQL也提供了与SQL相结合的模式识别语句——MATCH_RECOGNIZE,可以支持在SQL语句中进行复杂事件处理