Flink之复杂事物处理(CEP)
CEP指的是复杂事物处理,FlinkCEP是复杂事物处理库在Flink上的实现。它使你可以检测无穷无尽的事件流中的事件模式,从而有机会掌握数据中的重要信息。
入门
首先要导入FlinkCEP的依赖。
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep-scala_2.12</artifactId>
<version>1.11.2</version>
</dependency>
导入依赖后才能使用FlinkCEP中提供的API。以下是FlinkCEP程序的一般模版。
val input: DataStream[Event] = ...
val pattern = Pattern.begin[Event]("start").where(_.getId == 42)
.next("middle").subtype(classOf[SubEvent]).where(_.getVolume >= 10.0)
.followedBy("end").where(_.getName == "end")
val patternStream = CEP.pattern(input, pattern)
val result: DataStream[Alert] = patternStream.process(
new PatternProcessFunction[Event, Alert]() {
override def processMatch(
`match`: util.Map[String, util.List[Event]],
ctx: PatternProcessFunction.Context,
out: Collector[Alert]): Unit = {
out.collect(createAlertFrom(pattern))
}
})
Pattern API
Pattern API可以定义要从输入流中提取的复杂模式序列。
每个复杂模式序列都包含多个简单模式,即寻找具有相同属性的单个事件的模式。我们将称这些简单模式为模式,以及称我们在流中搜索的最终复杂模式为模式序列。
个体模式
一个模式(即前面说的简单模式)可以是单例模式,也可以是循环模式。单例模式接收一个事件,而循环模式可以接收多个模式。
例如在模式匹配的符号中,a b+ c? d
代表a
后面跟了一个或多个的b
,可能有个c
,然后有个d
。其中a,c?,d
就是单例模式,而b+
是循环模式。通过添加量词,可以将单例模式转换为循环模式。每个模式都可以基于若干个条件来接收事件。
量词
利用量词,可以指定循环的次数。还有greedy()
方法让模式匹配尽可能多次,optional()
方法让模式要么匹配若干次,要么不匹配,以模式start
为例。
// 匹配4次
start.times(4)
// 要么匹配0次,要么匹配4次
start.times(4).optional()
// 匹配2到4次
start.times(2, 4)
// 匹配2到4次且希望尽可能多地匹配
// expecting 2, 3 or 4 occurrences and repeating as many as possible
start.times(2, 4).greedy()
// 匹配要么0次要么2到4次
start.times(2, 4).optional()
// 匹配0次、2到4次且尽可能地多
start.times(2, 4).optional().greedy()
// 匹配1次或多次
start.oneOrMore()
// 匹配1次或者尽可能多次
start.oneOrMore().greedy()
// expecting 0 or more occurrences
start.oneOrMore().optional()
// expecting 0 or more occurrences and repeating as many as possible
start.oneOrMore().optional().greedy()
// expecting 2 or more occurrences
start.timesOrMore(2)
// expecting 2 or more occurrences and repeating as many as possible
start.timesOrMore(2).greedy()
// expecting 0, 2 or more occurrences
start.timesOrMore(2).optional()
// expecting 0, 2 or more occurrences and repeating as many as possible
start.timesOrMore(2).optional().greedy()
条件
对于每一个模式,都可以设置一些条件来控制该模式是否要开始接收事件。
可以通过pattern.where()
、pattern.or()
或pattern.until()
来给事件的属性指定条件。
迭代条件
迭代条件是最普遍的条件类型。可以根据先前接受的事件的属性或这些事件的子集的统计信息来指定接受后续事件的条件。
以下是一段迭代条件的示例代码。如果当前事件的名称以“ foo”开头,并且该模式先前接受的事件的价格加上当前事件的价格的值不超过5.0
,则该迭代条件接受名为模式middle
的下一个事件。
middle.oneOrMore()
.subtype(classOf[SubEvent])
.where(
(value, ctx) => {
lazy val sum = ctx.getEventsForPattern("middle").map(_.getPrice).sum
value.getName.startsWith("foo") && sum + value.getPrice < 5.0
}
)
简单条件
这种类型的条件扩展了IterativeCondition
类,并仅基于事件本身的属性来决定是否接受事件。例如:
start.where(event => event.getName.startsWith("foo"))
最后,您还可以通过pattern.subtype(subClass)
方法将接受事件的类型限制为初始事件类型的子类型(此处为Event)。
start.subtype(classOf[SubEvent]).where(subEvent => ... /* some condition */)
复合条件
复合条件即各种条件的组合。多个顺序排列的where()
方法,代表逻辑与。逻辑或可以用or()
方法。
pattern.where(event => ... /* some condition */).or(event => ... /* or condition */)
停止条件
对于循环模式(例如使用了oneOrMore()
的),可以通过某些停止条件让其停止接收事件。
为了更深入理解举个例子。给定模式a+ until b
,给定以下事件序列a1, c, a2, b, a3
,则{a1, a2} {a1} {a2} {a3}
将被输出。
下表给出以上条件操作的总结:
模式操作 | 描述 |
---|---|
where(condition) |
要匹配当前的模式,就必须要满足condition 定义的条件。多个连续的where() 代表条件的逻辑与。 |
or(condition) |
添加一个与现有条件逻辑或的条件。也就是说事件至少要满足其中一个条件才能匹配。 |
until(condition) |
指定循环模式的停止条件。一旦事件满足这一条件,模式将不再接收事件。一般只与oneOrMore() 连在一起使用作为停止条件。 |
subtype(subClass) |
为当前模式定义子类型条件。只有属于此子类型的事件才能被当前模式匹配。 |
oneOrMore() |
此方法定义的条件就是字面意思,就是希望事件出现至少一次。默认情况下是宽松近邻条件。 |
timesOrMore(#times) |
希望事件出现至少#times 次。 |
times(#ofTimes) |
希望事件出现#ofTimes 次。 |
times(#fromTimes, #toTimes) |
希望事件出现#fromTimes 到toTimes 。 |
optional() |
定义该模式是可选的,即它可能发生也可能不发生。 |
greedy() |
定义该模式是贪婪的,即将改模式重复尽可能多次。当前仅支持在量词中使用。 |
复合模式
将个体模式组合起来就是复合模式。
首先,一个模式序列必须以一个初始模式开始:
val start : Pattern[Event, _] = Pattern.begin("start")
接着,用邻近条件将不同的模式连接起来。FlinkCEP支持以下的连续性条件:
- 严格邻近(Strict Contiguity):期望所有匹配事件严格地一个接一个地出现,而中间没有任何不匹配事件,通过
next()
方法实现。 - 宽松邻近(Relaxed Contiguity):允许匹配事件之间出现不匹配事件,通过
followedBy()
实现。 - 非确定宽松邻近(Non-Deterministic Relaxed Contiguity):进一步放松了连续性,允许其他匹配忽略某些匹配事件,通过
followedByAny()
实现。 - 另外,如果不希望事件之后有某些事件,可以通过
notNext()
和notFollowedBy()
实现。
// strict contiguity
val strict: Pattern[Event, _] = start.next("middle").where(...)
// relaxed contiguity
val relaxed: Pattern[Event, _] = start.followedBy("middle").where(...)
// non-deterministic relaxed contiguity
val nonDetermin: Pattern[Event, _] = start.followedByAny("middle").where(...)
// NOT pattern with strict contiguity
val strictNot: Pattern[Event, _] = start.notNext("not").where(...)
// NOT pattern with relaxed contiguity
val relaxedNot: Pattern[Event, _] = start.notFollowedBy("not").where(...)
宽松邻近意味着只有第一个成功匹配的事件会被匹配,而非确定邻近则会在同一个初始模式情况下返回多个匹配。例如,对于一个模式a b
,给定以下事件序列a, c, b1, b2
,不同的邻近条件将返回不同的结果:
- 严格邻近将返回空集;
- 宽松邻近则会返回
{a b1}
,因为宽松邻近会跳过不匹配的事件直至下一次匹配; - 非确定宽松邻近则会返回
{a b1} {a b2}
。
还可以给模式一个时间约束,使其在规定时间内有效。
next.within(Time.seconds(10))
循环模式中的近邻
在循环模式中也可以规定近邻条件。这种邻近条件将被应用于被模式接收的事件之间。例如,对于一个模式a b+ 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
和事件b
之间的事件d
被忽略了; - 非确定宽松邻近则会返回
{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}
。
对于循环模式来说,默认是宽松邻近。如果需要其余两种连续性则需要分别调用consecutive()
和allowCombinations()
方法来指定。对于模式:
Pattern.begin("start").where(_.getName().equals("c"))
.followedBy("middle").where(_.getName().equals("a"))
.oneOrMore().consecutive()
.followedBy("end1").where(_.getName().equals("b"))
事件流C D A1 A2 A3 D A4 B
将会返回{C A1 B}, {C A1 A2 B}, {C A1 A2 A3 B}
,而在没有指定严格邻近条件时,返回的是{C A1 B}, {C A1 A2 B}, {C A1 A2 A3 B}, {C A1 A2 A3 A4 B}
。
对于模式:
Pattern.begin("start").where(_.getName().equals("c"))
.followedBy("middle").where(_.getName().equals("a"))
.oneOrMore().allowCombinations()
.followedBy("end1").where(_.getName().equals("b"))
事件流C D A1 A2 A3 D A4 B
将会返回{C A1 B}, {C A1 A2 B}, {C A1 A3 B}, {C A1 A4 B}, {C A1 A2 A3 B}, {C A1 A2 A4 B}, {C A1 A3 A4 B}, {C A1 A2 A3 A4 B}
,而在没有指定严格邻近条件时,返回的是{C A1 B}, {C A1 A2 B}, {C A1 A2 A3 B}, {C A1 A2 A3 A4 B}
。
模式操作 | 描述 |
---|---|
consecutive() |
与oneOrMore() 或times() 一起使用,用于指定匹配事件之间的严格邻近条件。 |
allowCombinations() |
与oneOrMore() 或times() 一起使用,用于指定匹配事件之间的非确定宽松邻近条件。 |
模式组
模式序列也可以通过begin, followedBy, followedByAny, next
等条件形成一个模式组GroupPattern
,此时该模式序列逻辑上被视为匹配的条件。在GroupPattern
上可以指定循环条件,也可以指定邻近条件。
val start: Pattern[Event, _] = Pattern.begin(
Pattern.begin[Event]("start").where(...).followedBy("start_middle").where(...)
)
// strict contiguity
val strict: Pattern[Event, _] = start.next(
Pattern.begin[Event]("next_start").where(...).followedBy("next_middle").where(...)
).times(3)
// relaxed contiguity
val relaxed: Pattern[Event, _] = start.followedBy(
Pattern.begin[Event]("followedby_start").where(...).followedBy("followedby_middle").where(...)
).oneOrMore()
// non-deterministic relaxed contiguity
val nonDetermin: Pattern[Event, _] = start.followedByAny(
Pattern.begin[Event]("followedbyany_start").where(...).followedBy("followedbyany_middle").where(...)
).optional()
模式操作 | 描述 |
---|---|
begin(#name) |
定义一个起始模式。 |
begin(#pattern_sequence) |
定义一个起始模式。即可以从个体模式定义起始模式,也可以从模式序列定义起始模式。 |
next(#name) |
附加一个新模式,其满足严格邻近条件。 |
next(#pattern_sequence) |
同上。 |
followedBy(#name) |
附加一个新模式,其满足宽松邻近条件。 |
followedBy(#pattern_sequence) |
同上。 |
followedByAny(#name) |
附加一个新模式,其满足非确定宽松邻近。 |
followedByAny(#pattern_sequence) |
同上。 |
notNext() |
不希望前一事件之后有某一事件,满足严格近邻条件。 |
notFollowedBy() |
不希望前一事件之后有某一事件,满足宽松近邻条件。 |
within(time) |
定义一个与模式匹配的事件序列的最大时间间隔。如果一个未完成的事件序列超过了这个时间,它将被丢弃。 |
匹配后跳过策略
对于给定的模式,同一个事件可能被分配给多次不同的匹配。要控制一个事件将分配多少个匹配,就需要指定名为AfterMatchSkipStrategy
的跳过策略。FlinkCEP一共支持5
种跳过策略。
Function | 描述 |
---|---|
AfterMatchSkipStrategy.noSkip() |
创建一个NO_SKIP策略,即任意一次匹配都不会被跳过。 |
AfterMatchSkipStrategy.skipToNext() |
创建一个SKIP_TO_NEXT策略,即丢弃以同一事件开始的所有部分匹配。 |
AfterMatchSkipStrategy.skipPastLastEvent() |
创建一个SKIP_PAST_LAST_EVENT策略,即丢弃匹配开始后但结束之前开始的所有部分匹配。使用该策略只会有一个结果被输出。 |
AfterMatchSkipStrategy.skipToFirst(patternName) |
创建一个SKIP_TO_FIRST策略,即丢弃在匹配开始后但在指定事件第一次发生前开始的所有部分匹配。需要指定一个有效的patternName 。 |
AfterMatchSkipStrategy.skipToLast(patternName) |
创建一个SKIP_TO_LAST策略,即丢弃在匹配开始后但在指定事件最后一次发生前开始的所有部分匹配。需要指定一个有效的patternName 。 |
可以在创建模式时,在begin方法中指定一个AfterMatchSkipStrategy
,就可以将该AfterMatchSkipStrategy
应用到当前的模式中。如果没有指定,Flink会默认将AfterMatchSkipStrategy
指定为NO_SKIP。
val skipStrategy = ...
Pattern.begin("patternName", skipStrategy)
模式的检测
在指定要查找的模式序列之后,就可以将其应用到输入流中,以检测潜在的匹配。要根据模式序列运行事件流,必须创建一个PatternStream
。给定一个输入流input
、一个模式pattern
和一个可选的比较器comparator
。输入流可以是dataStream
也可以是keyedStream
。比较器用于在EventTime
事件中对具有相同时间戳的事件进行排序,或者在同一时刻到达的事件。可以通过以下代码来创建PatternStream
:
val input : DataStream[Event] = ...
val pattern : Pattern[Event, _] = ...
var comparator : EventComparator[Event] = ... // 可选
val patternStream: PatternStream[Event] = CEP.pattern(input, pattern, comparator)
匹配事件提取
创建PatternStream
后,可以用select()
或flatselect()
方法,从检测到的事件序列中提取事件了。
select()
方法需要输入一个select fuction为参数,每个成功匹配的事件都会调用它。select()
方法以一个Map[String, Iterable[IN]]
来接收匹配到的事件序列,其中key是每个模式的名称,而value是所有接收到的事件的Iterable类型。需要注意的是,select fuction每次调用只会返回一个结果。
def selectFn(pattern: Map[String, Iterable[IN]]): OUT = {
val startEvent = pattern.get("start").get.next
val endEvent = pattern.get("end").get.next
OUT(startEvent, endEvent)
}
也可以通过flat select fuction来提取匹配事件。flat select fuction与select fuction类似,不过flat select fuction使用Collector
作为返回结果的容器,因此每次调用可以返回任意数量的结果。
def flatSelectFn(pattern: Map[String, Iterable[IN]]): collector: COLLECTOR[OUT] = {
val startEvent = pattern.get("start").get.next
val endEvent = pattern.get("end").get.next
for (i <- 0 to startEvent.getValue){
collector.collect(OUT(startEvent, endEvent))
}
}
超时事件提取
对于模式中的事件,如果没有及时处理或者超过了within
规定的时间,就会成为超时事件。为了对超时事件进行处理,Pattern API也提供了select和flatSelect两个方法来对超时事件进行处理。
超时处理程序会接收到目前为止由模式匹配到的所有事件,由一个OutputTag
定义接收到的超时事件序列。同样地,超时事件处理中也有select()
方法和flatselect()
方法。
val patternStream: PatternStream[Event] = CEP.pattern(input, pattern)
// 创建一个OutputTag并命名为late-data
val lateDataOutputTag = OutputTag[String]("late-data")
val result = patternStream.select(lateDataOutputTag){
// 提取超时事件
(pattern: Map[String, Iterable[Event]], timestamp: Long) => TimeOutEvent()
}{
pattern: Map[String, Iterable[Event]] => ComplexEvent()
}
// 调用getSideOutput将超时事件输出
val lateData = result.getSideOutput(lateDataOutputTag)
下面是使用flatselect()
方法的代码示例。
val patternStream: PatternStream[Event] = CEP.pattern(input, pattern)
// 创建一个OutputTag并命名为side-output
val outputTag = OutputTag[String]("side-output")
val result: SingleOutputStreamOperator[ComplexEvent] = patternStream.flatSelect(outputTag){
// 提取超时事件
(pattern: Map[String, Iterable[Event]], timestamp: Long, out: Collector[TimeoutEvent]) =>
out.collect(TimeoutEvent())
} {
(pattern: mutable.Map[String, Iterable[Event]], out: Collector[ComplexEvent]) =>
out.collect(ComplexEvent())
}
// 调用getSideOutput将超时事件输出
val timeoutResult: DataStream[TimeoutEvent] = result.getSideOutput(outputTag)
代码示例
以下是一段示例代码。
import java.util
import org.apache.flink.cep.PatternSelectFunction
import org.apache.flink.cep.scala.CEP
import org.apache.flink.cep.scala.pattern.Pattern
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala._
// 定义输入事件的样例类
case class UserAction(userName: String, eventType: String, eventTime: Long)
// 定义输出事件的样例类
case class ClickAndBuyAction(userName: String, clickTime: Long, buyTime: Long)
object UserActionDetect {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
val dataList = List(
UserAction("Adam", "click", 1558430815185L),
UserAction("Adam", "buy", 1558430815865L),
UserAction("Adam", "order", 1558430815985L),
UserAction("Berry", "buy", 1558430815988L),
UserAction("Adam", "click", 1558430816068L),
UserAction("Berry", "order", 1558430816074L),
UserAction("Carl", "click", 1558430816151L),
UserAction("Carl", "buy", 1558430816641L),
UserAction("Dennis", "buy", 1558430817128L),
UserAction("Carl", "click", 1558430817165L),
UserAction("Ella", "click", 1558430818652L),
)
// 1. 创建输入事件流
val userLogStream = env.fromCollection(dataList)
.assignAscendingTimestamps(_.eventTime)
.keyBy(_.userName)
// 2. 用户自定义模式
val userActionPattern = Pattern.begin[UserAction]("begin")
.where(_.eventType == "click")
.next("next")
.where(_.eventType == "buy")
// 3. 调用CEP.pattern方法寻找与模式匹配的事件
val patternStream = CEP.pattern(userLogStream, userActionPattern)
// 4. 输出结果
val result = patternStream.select(new ClickAndBuyMatch())
result.print()
env.execute()
}
}
// 重写select方法
class ClickAndBuyMatch() extends PatternSelectFunction[UserAction, ClickAndBuyAction] {
override def select(map: util.Map[String, util.List[UserAction]]): ClickAndBuyAction = {
val click: UserAction = map.get("begin").iterator().next()
val buy: UserAction = map.get("next").iterator().next()
ClickAndBuyAction(click.userName, click.eventTime, buy.eventTime)
}
}
pom.xml
文件如下:
<project>
<groupId>cn.edu.xmu.dblab</groupId>
<artifactId>simple-project</artifactId>
<modelVersion>4.0.0</modelVersion>
<name>Simple Project</name>
<packaging>jar</packaging>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep-scala_2.12</artifactId>
<version>1.11.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_2.12</artifactId>
<version>1.11.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.flink/flink-streaming-scala -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_2.12</artifactId>
<version>1.11.2</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients_2.12</artifactId>
<version>1.11.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.4.6</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>