——wirte by 橙心橙意橙续缘,
前言
白话系列
————————————————————————————
也就是我在写作时完全不考虑写作方面的约束,完全把自己学到的东西、以及理由和所思考的东西等等都用大白话诉说出来,这样能够让信息最大化的从自己脑子里输出并且输入到有需要的同学的脑中。PS:较为专业的地方还是会用专业口语诉说,大家放心!
白话Flink系列
————————————————————————————
主要是记录本人(国内某985研究生)在Flink基础理论阶段学习的一些所学,更重要的是一些所思所想,所参考的视频资料或者博客以及文献资料均在文末放出.由于研究生期间的课题组和研究方向与Flink接轨较多,而且Flink的学习对于想进入大厂的同学们来说也是非常的赞,所以该系列文章会随着本人学习的深入来不断修改和完善,希望大家也可以多批评指正或者提出宝贵建议。
目录
复杂时间处理CEP
CEP概念
- Flink中的复杂事件处理(Complex Event Processing,CEP)是在 Flink 中实现的复杂事件处理(CEP)
库
。 - CEP 允许在无休止的
事件流
中检测事件模式,让我们有机会掌握数据中重要的部分
重要的部分也即数据与数据之间的关系等高阶的特征,比如顺序关系等
- 一个或多个由简单事件构成的事件流通过一定的规则匹配,然后输出用户想得到的数据——满足规则的复杂事件
有点儿类似
过滤
的操作,将简单事件流中符合复杂关系的事件过滤出来
CEP特点
- 目标:从有序的简单事件流中发现一些高阶特征
- 输入:一个或多个由简单事件构成的事件流
- 处理:识别简单事件之间的内在联系,多个符合一定规则的简单事件构成复杂事件
- 输出:满足规则的复杂事件(一组或者多组)
pattern API
简介
模式
处理事件的规则,被叫做“模式”(Pattern)。
pattern API
Flink CEP 提供了 Pattern API,用于对输入流数据进行复杂事件规则定义, 用来提取符合规则的事件序列。
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep_2.11</artifactId>
<version>1.12.0</version>
</dependency>
需要插入以上CEP依赖才可以在CEP程序里使用pattern API
简单程序结构
DataStream<Event> input = ... //输入的简单事件流
//定义一个Pattern
Pattern<Event,Event> = Pattern.<Event>begin("start").where(...)
.next("middle").subtype(SubEvent.class).where(...)
.followedBy("end").where(...);
//将创建好的Pattern应用到事件流上进行匹配
PatternStream<Event> patternStream = CEP.pattern(input,pattern);
//检出匹配时间序列,处理得到结果
DataStream<Alert> result = patternStream.select(...);
模式类别
-
个体模式(Individual Patterns)
- 组成复杂规则的每一个单独的模式定义,就是“个体模式”
start.times(3).where(new SimpleCondition<Event>() {...})
-
组合模式(Combining Patterns,也叫模式序列)
- 很多个体模式组合起来,就形成了整个的模式序列
- 模式序列必须以一个“初始模式”开始:
Pattern<Event,Event> start = Pattern.<Event>begin("start")
-
模式组(Groups of patterns)
- 将一个模式序列作为条件嵌套在个体模式里,成为一组模式
可以发现,最重要的就是个体模式。其他2种归根到底也是个体模式的一种变形,所以接下来重点看一下个体模式。
个体模式
- 个体模式可以包括“
单例(singleton)模式
”和“循环(looping)模式
” - 单例模式只接收
一个
事件,而循环模式可以接收多个
接下来我们介绍一下构成个体模式的一些附加规则,看看这些规则是怎么组合形成可用于筛选复杂事件的模式的。
量词->循环模式
可以在一个个体模式后追加量词,也就是指定循环次数,也就是循环模式(多次匹配得符合pattern多个结果)。
//匹配出现4次,start是初始模式
start.times(4)
//匹配出现0次或4次
start.times(4).optional()
/匹配出现2,3,或者4次
start.times(2,4)
//匹配出现2,3,或者4次,并且尽可能多地重复匹配
start.times(2,4).greedy()
//匹配出现1次或多次
start.oneOrMore()
//匹配出现0次、2次或多次,并且尽可能多地重复匹配
start.timesOrMore(2).optional().greedy()
.optional()
代表可有可无的意思;.greedy()
代表在可选匹配次数里尽可能多的匹配;.oneOrMore()
代表匹配一次或多次;.timesOrMore(N)代表匹配N次或多次。
条件
- 每个模式都需要指定触发条件,作为模式是否接受事件进入的判断依据
- CEP 中的个体模式主要通过调用
.where()
和or()
和.until()
来指定条件 - 按不同的调用方式,可以分成以下几类:
-
简单条件
- 通过
.where()
方法对事件中的字段进行判断筛选,决定是否接受该事件
start.where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) { return value.getName().startsWith("foo"); } });
- 还可以通过
pattern.subtype(subClass)
方法将接受的事件类型限制为初始事件类型的一个子类型(这里是Event)
start.subtype(SubEvent.class).where(new SimpleCondition<SubEvent>() { @Override public boolean filter(SubEvent value) { return ... // some condition } });
- 通过
-
组合条件
- 将简单条件进行合并;
.or()
方法表示或逻辑
相连,.where()
的直接组合就是与逻辑
相连。
pattern.where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) { return ... // some condition } }).or(new SimpleCondition<Event>() { @Override public boolean filter(Event value) { return ... // or condition } });
- 将简单条件进行合并;
-
终止条件
- 如果使用了
oneOrMore
或者oneOrMore.optional
,建议使用.until()
作为终止 条件,以便清理状态。
例如,接受值大于5的事件,直到值的总和小于50这样的。
pattern:one or more "a" until "b" 输入事件流为:"a1" "c" "a2" "b" "a3" 输出:{a1 a2} {a1} {a2} {a3} PS:{a1 a2 a3}和{a2 a3}由于停止条件,没有返回
- 如果使用了
-
迭代条件
- 能够对模式之前所有接收的事件进行处理,根据之前接受的事件的属性或它们的一个子集的统计数据来接受后续事件。
- 这是
最通用
的条件类型。 - 可以调用
ctx.getEventsForPattern(“name”)
为给定的潜在匹配找到所有之前接受的事件。这个操作的成本可能会有所不同,所以在实现你的条件时,尽量减少它的使用。
下面是一个迭代条件的代码,如果一个名为 "middle "的模式的名称以 "foo "开头,并且如果该模式之前接受的事件的价格加上当前事件的价格之和不超过5.0的值,则接受该模式的下一个事件。
迭代条件可以发挥强大的作用,尤其是与循环模式相结合
,例如oneOrMore()。middle.oneOrMore() .subtype(SubEvent.class) .where(new IterativeCondition<SubEvent>() { @Override public boolean filter(SubEvent value, Context<SubEvent> ctx) throws Exception { if (!value.getName().startsWith("foo")) { return false; } double sum = value.getPrice(); for (Event event : ctx.getEventsForPattern("middle")) { sum += event.getPrice(); } return Double.compare(sum, 5.0) < 0; } });
-
模式序列
-
下面主要是
个体模式
中的不同的“近邻”模式 -
严格近邻(Strict Contiguity)
- 所有事件按照严格的顺序出现,中间没有任何不匹配的事件,由
.next()
指定 - 例如对于模式”a next b”,事件序列 [a, c, b1, b2] 没有匹配
- 所有事件按照严格的顺序出现,中间没有任何不匹配的事件,由
-
宽松近邻( Relaxed Contiguity )
- 允许中间出现不匹配的事件,由
.followedBy()
指定 - 例如对于模式”a followedBy b”,事件序列 [a, c, b1, b2] 匹配为 {a, b1}
- 允许中间出现不匹配的事件,由
-
非确定性宽松近邻( Non-Deterministic Relaxed Contiguity )
- 进一步放宽条件,之前已经匹配过的事件也可以再次使用,由
.followedByAny()
指定 - 例如对于模式”a followedByAny b”,事件序列 [a, c, b1, b2] 匹配为 {a, b1},{a,b2}
- 进一步放宽条件,之前已经匹配过的事件也可以再次使用,由
-
不希望出现某种近邻关系
.notNext()
——不想让某个事件严格紧邻前一个事件发生.notFollowedBy()
——不想让某个事件在两个事件之间发生
可以看到
notFollowedBy()
限定的是范围,而不是字面意思上的不让某事件出现在前面的事件后面;很明显,对于流数据来说是没有边界的,那么系统怎么能知道后面会不会来呢?也就是说直接使用notFollowedBy()
它永远匹配不出合适的事件。那又为什么说它能限定范围呢,是因为它可以和
.fllowedBy()
配合使用,比如start.notFollowedBy(a).fllowedBy(c)
就相当于手动给.fllowedBy()
设置了一个边界,这个模式表达的是,start后面没有a但是有c,也即a不在start和c之间。
注意事项
————————
-
所有模式序列必须以 .begin() 开始
-
模式序列不能以
.notFollowedBy()
结束 //很明显,这个条件放在最后没有边界 -
“not” 类型的模式不能被
optional
所修饰 //这样会造成歧义 -
此外,还可以为模式指定
时间
约束,用来要求在多长时间内匹配有效next.within(Time.second(10))
处理时间还是事件时间具体由时间语义来确定,一个模式序列只能有一个时间约束。如果在不同的单个模式上定义了多个这样的约束,那么就采用
最小
的约束。
循环模式及其模式序列
我们介绍过的3种序列关系同样适用于循环模式。接下来我们举个例子
对于模式 “a” followedByAny “b” oneOrMore followedBy “c” ,假设输入为"a", “b1”, “d1”, “b2”, “d2”, “b3” “c”。
- 严格近邻: {a b3 c}
- 宽松近邻:{a b1 c}, {a b1 b2 c}, {a b1 b2 b3 c}, {a b2 c}, {a b2 b3 c}, {a b3 c}
- 非确定性近邻: {a b1 c}, {a b1 b2 c},
{a b1 b3 c}
, {a b1 b2 b3 c}, {a b2 c}, {a b2 b3 c}, {a b3 c}
解答:这个模式为a随便用,但是后面要有一个或者多个b,然后后面有1个c;我们的重点就在于研究b是怎么匹配的,因为b是循环模式,个体模式是很直观的。循环模式b的严格近邻就是b后面紧挨着c;宽松近邻就是b和c不用紧挨着;进一步放宽近邻就是用过的b还可以用
对于循环模式(oneOrMore()
或 times()
),默认是(1)宽松近邻
。
-
(2)严格近邻 ——通过调用
continuous()
Pattern.<Event>begin("start").where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) throws Exception { return value.getName().equals("c"); } }) .followedBy("middle").where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) throws Exception { return value.getName().equals("a"); } }).oneOrMore().consecutive() .followedBy("end1").where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) throws Exception { return value.getName().equals("b"); } });
-
(3)非确定性宽松相邻性——通过调用
allowCombinations()
Pattern.<Event>begin("start").where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) throws Exception { return value.getName().equals("c"); } }) .followedBy("middle").where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) throws Exception { return value.getName().equals("a"); } }).oneOrMore().allowCombinations() .followedBy("end1").where(new SimpleCondition<Event>() { @Override public boolean filter(Event value) throws Exception { return value.getName().equals("b"); } });
模式组 Groups of patterns 及其模式序列
- 模式组(Groups of patterns)也可以定义一个模式序列作为
begin、followBy、followByAny 和 next
的条件,该模式序列将被视为逻辑上的匹配条件,并将返回一个GroupPattern
。 - 并且可以对
GroupPattern
应用oneOrMore()、times(#ofTimes)、times(#fromTimes、#toTimes)、optional()、continuous()、allowCombinations()
。
详细操作见Flink官方文档。
// 定义1个start模式组(Pattern内部有1个SubPattern)
Pattern<Event, ?> start = Pattern.begin(
Pattern.<Event>begin("start").where(...).followedBy("start_middle").where(...)
);
// 严格近邻 - next
Pattern<Event, ?> strict = start.next(
Pattern.<Event>begin("next_start").where(...).followedBy("next_middle").where(...)
).times(3);
// 宽松近邻 - followedBy
Pattern<Event, ?> relaxed = start.followedBy(
Pattern.<Event>begin("followedby_start").where(...).followedBy("followedby_middle").where(...)
).oneOrMore();
// 不确定性宽松近邻 - followedByAny
Pattern<Event, ?> nonDetermin = start.followedByAny(
Pattern.<Event>begin("followedbyany_start").where(...).followedBy("followedbyany_middle").where(...)
).optional();
对于一个
个体模式
来说,Pattern API的参数一般是#name
,而对于一个模式组
来说,Pattern API的参数是#pattern_sequence
。
模式的检测
- 指定要查找的模式序列后,就可以将其应用于输入流以检测潜在匹配
- 调用
CEP.pattern()
,给定输入流和模式,就能得到一个PatternStream
- 第3个参数为一个可选的比较器,用于在
EventTime
的情况下对具有相同时间戳的事件或在同一时刻到达的事件进行排序
DataStream<Event> input = ...
Pattern<Event, ?> pattern = ...
EventComparator<Event> comparator = ... // 一个可选的比较器
PatternStream<Event> patternStream = CEP.pattern(input, pattern, comparator);
如果事件流
没有经过keyBy()
,那么模式监测的并行度将会是1
匹配事件的提取
- 创建 PatternStream 之后,就可以应用
select
或者flatselect
方法,从检测到的事件序列中提取事件了 - select() 方法需要输入一个
select function
作为参数,每个成功匹配的事件序列都会调用它 - select() 以一个 Map<String,List > 来接收匹配到的事件序列,其中 key就是每个模式的名称,而 value 就是所有接收到的事件的 List 类型
public Out select(Map<String,List<IN>> pattern) throws Exception{
IN startEvent = pattern.get("start").get(0);
IN endEvent = pattern.get("end").get(0);
return new OUT(startEvent,endEvent);
}
超时事件的处理(侧输出流)
- 当一个模式通过 within 关键字定义了检测窗口时间时,部分事件序列可能因为超过窗口长度而被丢弃;为了能够处理这些超时的部分匹配,select 和 flatSelect API 调用允许指定超时处理程序
- 超时处理程序会接收到目前为止由模式匹配到的所有事件,由一个 OutputTag 定义接收到的超时事件序列
也就是说如果pattern中包含了
within
,而且使用的是EventTime的话,就需要在我们进行提取的时候进行超时处理,可以看到在.flatSelect()和.select()
中除了基本的事件提取方法以外,还实现了一个超时事件处理方法和一个测输出流(用于接收超时处理后的数据)标志OutputTag
。
总结
可以看到Flink中的CEP就像是正则表达式一样,从一堆复杂的事件流中按照我们定义的模式匹配出我们想要的事件组,所以我们整篇都在讲如何可以设计出我们想要的模式,比如不同的模式(个体模式、循环模式、模式组)以及他们中的模式序列(严格近邻、宽松近邻、不确定性宽松近邻),然后就是根据设计的模式从PatternStream中提取想要的事件。
几个比较难以理解的地方:
- 循环模式中的模式序列
- 模式组的定义
- 包含时间语义时的超时事件处理
我建议这部分还是只有自己多练才可以熟悉其中的一些容易造成人混乱的地方,尤其是循环模式中的3种模式序列比起个体模式就让人很晕,实在太晕的建议去Flink官方文档去看一下example。这块不建议死记硬背,还是多用,用得少的话就是用到的时候再去看example理解,马上就能弄明白。