Flink-1.12.0 CEP详解与实战

什么是CEP

CEP(Complex Event Processing),复杂事件处理,一个或多个简单事件构成的事件流通过一定的规则匹配,然后输出用户想得到的数据,满足规则的复杂事件。

Flink CEP简介

Flink CEP是在flink中实现的复杂事件处理库,也就是说搭配Flink实时处理的能力,FLink CEP能够在流处理的场景去做一些实时的复杂事件匹配,特点是能够作用于一个无限的数据流上,这就意味着它可以将某种规则的数据匹配一直保持下去;
举个例子:
下图中原始流包含了各种形状的图形,并且是杂乱无序的。原始流会和规则模式进行匹配,如果满足正方形开头圆形结尾的事件,会被提取到输出流中。
在这里插入图片描述

Flink CEP的应用场景

前面介绍了什么是FLink CEP,也做了一个简单的介绍,那么Flink CEP具体有哪些应用场景呢?

  • 实时反作弊和风控
  • 实时营销
  • 实时网络攻击检测
  • 等等。。。

Flink CEP原理

在这里插入图片描述

Flink CEP内部是用NFA(非确定有限自动机:对每个状态和输入符号可以有多个可能的下一个状态的有限状态自动机)来实现的,由点和边组成的一个状态图,以一个初始状态作为起点,经过一系列的中间状态,达到终态。点分为起始状态中间状态最终状态,边分为takeignoreproceed三种。

  • take:必须存在一个条件判断,当到来的消息满足take边条件判断时,把这个消息放入结果集,将状态转移到下一个状态
  • ignore:当消息到来时,可以忽略这个消息,将状态自旋在当前不变,是一个自己到自己的状态转移
  • proceed:又叫做状态的空转移,当前状态可以不依赖于消息到来而直接转移到下一状态

NFA的组成我们已经了解了,那么Flink如何又是如何实现转换的呢?

我们把上面处理事件的规则叫做模式,Flink的每个模式包含多个状态,模式匹配的过程就是状态转换的过程,每个状态(state)可以理解成由Pattern构成,为了从当前的状态转换成下一个状态,用户可以在Pattern上指定条件,用于状态的过滤和转换。

也就是为了实现Flink CEP,我们需要创建一个个pattern,然后通过链表前后串在一起,构成模式匹配的逻辑表达。然后需要利用NFACompiler,将模式进行拆分,创建出NFA对象,NFA包含了该次模式匹配的各个状态和状态间转换的表达式。整个示意图如下:
在这里插入图片描述

Pattern API

通过上面我们已经知道Flink是通过Pattern编译转换为NFA实现的CEP,我们通过Pattern API去定义流数据中匹配事件的Pattern,每个复杂Pattern是由多个简单的Pattern组成的,下面我们就来看看怎么使用Pattern API。
Pattern根据模式的组合,分为了三种

个体模式

组成复杂规则的每一个单独的模式定义,就是“个体模式”

start.where(condition: F => Boolean)

量词

个体模式根据接收同一种事件的次数又可以分为"单例模式"和"循环模式",我们通过一个"量词"来指定接收同一种事件的数量。

 //期望符合的事件出现 4 次
 start.times(4);

 //期望符合的事件不不出现或者出现 4 次
 start.times(4).optional();

 //期望符合的事件出现 2 次或者 3 次或者 4 次
 start.times(2,4);

 //期望出现 2 次、3 次或 4 次,并尽可能多地重复
 start.times(2,4).greedy();

 //期望出现一个或多个事件
 start.oneOrMore();

 //期望出现 2 次或更更多的事件,并尽可能多地重复或者不不出现
 start.timesOrMore(2).optional().greedy();

条件

个体模式的条件,可以在一个个体模式上使用多个条件,只有当条件都满足的情况下才算匹配成功。

pattern.where(condition)  //模式的条件为condition
pattern.where(condition1).or(condition) //模式条件为condition1或condition2
pattern.oneOrMore().until(condition) //模式发生一次或多次,直至condition满足为止

条件可以是IterativeConditions(迭代条件:调用上下文对前边接收的事件进行处理)或SimpleConditions(简单条件),如果是简单条件可以像下面这样使用

pattern.where(
        new SimpleCondition<Event>() {
            @Override
            public boolean filter(Event event) {
                return event.getName() == "xicent";
            }
        }
    )

组合模式

多个个体模式组成形成一个完整的模式序列,称为组合模式。
模式序列由一个初始模式作为开头

Pattern<Event, ?> start = Pattern.<Event>begin("start");

接下来,我们就可以增加更多的模式到模式序列中并制定它们之间所需的连续条件。Flink CEP支持事件之间如下形式的连续策略:

  1. 严格连续:期望所有匹配的事件严格的一个接一个出现,中间没有任何不匹配的事件。方法:next()
  2. 松散连续:忽略匹配的事件之间的不匹配的事件。方法:followedBy()
  3. 不确定的松散连续:更进一步的松散连续,允许忽略掉一些匹配事件的附加匹配。方法:followedByAny()
// 严格连续
Pattern<Event, ?> strict = start.next("middle").where(...);

// 松散连续
Pattern<Event, ?> relaxed = start.followedBy("middle").where(...);

// 不确定的松散连续
Pattern<Event, ?> nonDetermin = start.followedByAny("middle").where(...);

// 严格连续的NOT模式
Pattern<Event, ?> strictNot = start.notNext("not").where(...);

// 松散连续的NOT模式
Pattern<Event, ?> relaxedNot = start.notFollowedBy("not").where(...);

说到这里可能还是有一些小伙伴对松散连续和不确定的松散连续有些疑惑,下面给大家举个例子就比较好理解了。
模式:a b
给定事件序列:a c b1 b2
不同的模式会产生不同的结果:

  1. a和b之间严格连续:{}(没有匹配),a之后的c导致a被丢弃
  2. a和b之间松散连续:{a b1},松散连续会"跳过不匹配的事件直到匹配上的事件"
  3. a和b之间不确定的松散连续:{a b1},{a b2},匹配事件被再次使用

也可以为模式定义一个有效事件约束。通过pattern.within()指定一个模式应该在10秒内发生。支持处理事件和事件事件。
注意:一个模式序列只能有一个时间限制。如果限制了多个时间在不同的单个模式上,会使用最小的那个时间限制。

模式组

将一个组合模式作为条件嵌套在个体模式里,成为一组模式。具体来讲就是可以定义一个模式序列作为begin,followedBy,followedByAny和next的条件,此时会返回一个GroupPattern。
例如:

Pattern<Event, ?> start = Pattern.begin(
    Pattern.<Event>begin("start").where(...).followedBy("start_middle").where(...)
);

// 严格连续
Pattern<Event, ?> strict = start.next(
    Pattern.<Event>begin("next_start").where(...).followedBy("next_middle").where(...)
).times(3);

// 松散连续
Pattern<Event, ?> relaxed = start.followedBy(
    Pattern.<Event>begin("followedby_start").where(...).followedBy("followedby_middle").where(...)
).oneOrMore();

// 不确定松散连续
Pattern<Event, ?> nonDetermin = start.followedByAny(
    Pattern.<Event>begin("followedbyany_start").where(...).followedBy("followedbyany_middle").where(...)
).optional();

匹配后跳过策略

对于一个给定的模式,同一个事件可能会分配到多个成功的匹配上。为了控制一个事件会分配到多少个匹配上,你需要指定跳过策略AfterMatchSkipStrategy。有五种跳过策略,如下:

  • NO_SKIP:每个成功的匹配都会被输出。
  • SKIP_TO_NEXT:丢弃以相同事件开始的所有部分匹配
  • SKIP_PAST_LAST_EVENT:丢弃起始在这个匹配的开始和结束之间的所有部分匹配
  • SKIP_TO_FIRST:丢弃在匹配开始后但在指定事件第一次发生前开始的所有部分匹配。需要指定一个有效的patternName
  • SKIP_TO_LAST:丢弃在匹配开始后但在指定事件最后一次发生前开始的所有部分匹配。需要指定一个有效的patternName

举个例子:给定一个模式b+ c和一个数据流b1 b2 b3 c,不同的跳过策略结果如下:

跳过策略结果描述
NO_SKIP{b1 b2 b3 c},{b2,b3,c},{b3,c}输出所有匹配结果
SKIP_TO_NEXT{b1 b2 b3 c},{b2,b3,c},{b3,c}匹配b1 b2 b3 c之后,没有找到以b1开始的其他匹配进行丢弃
SKIP_PAST_LAST_EVENT{b1 b2 b3 c}匹配到b1 b2 b3 c之后,会丢弃其他所有的部分匹配
SKIP_TO_FIRST[b]{b1 b2 b3 c},{b2,b3,c},{b3,c}匹配到b1 b2 b3 c之后,没有找到以b1之前开始的部分匹配进行丢弃
SKIP_TO_LAST[b]{b1 b2 b3 c},{b3,c}匹配到b1 b2 b3 c之后,尝试丢弃所有在b3之前开始的部分匹配,此时b2 b3 c被丢弃

这五种跳过策略一般不太容易理解的是SKIP_TO_NEXT和SKIP_TO_FIRST,下面我们分别拿它们和NO_SKIP(全匹配输出)作对比,看看差异在哪里。
模式:(a | b | c) (b | c) c+.greedy d
输入:a b c1 c2 c3 d
结果:

跳过策略结果描述
NO_SKIP{a b c1 c2 c3 d},{b c1 c2 c3 d},{c1 c2 c3 d}所有匹配都输出
SKIP_TO_FIRST[c*]{a b c1 c2 c3 d},{c1 c2 c3 d}找到a b c1 c2 c3 d之后,会丢弃所有在c1之前开始的部分匹配,因此b c1 c2 c3 d被丢弃。

模式:a b+
输入:a b1 b2 b3
结果:

跳过策略结果描述
NO_SKIP{a b1},{a b1 b2},{a b1 b2 b3}所有匹配都输出
SKIP_TO_NEXT{a b1}找到匹配a b1之后,会丢弃所有以a开始的部分匹配,因此{a b1 b2},{a b1 b2 b3}都被丢弃

检测Pattern

当我们编写好Pattern之后,接下来就是要和数据流做匹配了。我们需要通过CEP.pattern()创建一个PatternStream,传入一个输入流(input),一个模式(pattern)和一个可选的比较器(comparator,当事件时间戳相等或同时到达时的排序)

DataStream<Event> input = ...
Pattern<Event, ?> pattern = ...
EventComparator<Event> comparator = ... // 可选的

PatternStream<Event> patternStream = CEP.pattern(input, pattern, comparator);

输入流根据使用场景可以使keyed或者non-keyed。
注意:在non-keyed流上使用模式将会使作业的并行度被设为1

从模式中选取

拿到PatternStream之后,可以通过select或flatSelect方法从匹配到的事件流中提取。如果使用的是select方法,需要传入一个PatternSelectFunction的实现作为参数,每个匹配的事件序列都会调用其select方法,该方法的参数Map<String,List>,key是pattern的名字,value是所有接收到的事件的Iterable类型。如果使用的是flatSelect方法,则需要传入一个PatternFlatSelectFunction的实现作为参数,这个和PatternSelectFunction不一致地方在于它可以返回多个结果。

CEP.pattern(eventDataStream,pattern).select(new PatternSelectFunction<Event, String>() {
@Override
public String select(Map<String, List<Event>> p) throws Exception { 

	return ...
 }

}).print();

CEP.pattern(eventDataStream,pattern).flatSelect(new PatternFlatSelectFunction<Event, String>() {
@Override
    public void flatSelect(Map<String, List<Event>> map, Collector<String>
collector) throws Exception {
	collector.collect(...)
}
}).print();

如果您使用Flink版本是1.8.0之后的,那么更推荐使用process方法。需要传入一个PatternProcessFunction的实现作为参数,并重写processMatch方法。

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对象,也就可以访问时间上下文,例如可以获取当前匹配的timestamp或currentProcessingTime。也可以通过这个context将部分结果输出到侧输出。

处理超时数据

当一个模式上通过within加上窗口长度后,部分匹配的事件序列可能会因为超过窗口长度而被丢弃,这部分数据被认为超时。可以使用TimedOutPartialMatchHandler接口来处理超时数据,它提供了processTimedOutMatch方法,这个方法对每个超时的部分匹配都会调用,和上面提到的processMatch方法搭配可以灵活处理各种场景

class MyPatternProcessFunction<IN, OUT> extends PatternProcessFunction<IN, OUT> implements TimedOutPartialMatchHandler<IN> {
    @Override
    public void processMatch(Map<String, List<IN>> match, Context ctx, Collector<OUT> out) throws Exception;
        ...
    }

    @Override
    public void processTimedOutMatch(Map<String, List<IN>> match, Context ctx) throws Exception;
        IN startEvent = match.get("start").get(0);
        ctx.output(outputTag, T(startEvent));
    }
}

实战

下面给出一个例子,使用Flink 1.12.0版本。业务场景是提取出5秒内连续登录失败两次数据,并将超时的部分匹配输出到侧输出。

加入依赖

	<properties>
        <flink.version>1.12.0</flink.version>
    </properties>
    <dependencies>
		<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-cep_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.11</artifactId>
            <version>${flink.version}</version>
        </dependency>
   </dependencies>
package com.xicent.flink.cep;

import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.java.functions.KeySelector;
import org.apache.flink.cep.CEP;
import org.apache.flink.cep.PatternSelectFunction;
import org.apache.flink.cep.PatternStream;
import org.apache.flink.cep.PatternTimeoutFunction;
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.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
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 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 CepDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        env.enableCheckpointing(1000);

        //输入数据,提取事件时间
        KeyedStream<LoginEvent, Integer> loginEventKeyedStream = env.fromElements(
                new LoginEvent(1, "success", 1575600181000L),
                new LoginEvent(2, "fail1", 1575600182000L),
                new LoginEvent(2, "fail2", 1575600183000L),
                new LoginEvent(3, "fail1", 1575600184000L),
                new LoginEvent(3, "fail2", 1575600189000L)
        ).assignTimestampsAndWatermarks(
                //注册watermark方法和旧版稍有不同
                WatermarkStrategy.<LoginEvent>forBoundedOutOfOrderness(Duration.ofSeconds(0))
                        .withTimestampAssigner((event,timestamp)-> event.getEventTime()))
                .keyBy(new KeySelector<LoginEvent, Integer>() {
            @Override
            public Integer getKey(LoginEvent loginEvent) throws Exception {
                return loginEvent.getUserId();
            }
        });

        //定义Pattern
        Pattern<LoginEvent, LoginEvent> pattern = Pattern.<LoginEvent>begin("begin").where(new SimpleCondition<LoginEvent>() {
            @Override
            public boolean filter(LoginEvent loginEvent) throws Exception {
                return "fail1".equals(loginEvent.getEventType());
            }
        }).<LoginEvent>next("next").where(new SimpleCondition<LoginEvent>() {
            @Override
            public boolean filter(LoginEvent loginEvent) throws Exception {
                return "fail2".equals(loginEvent.getEventType());
            }
        }).within(Time.seconds(5));

        //检测模式
        PatternStream<LoginEvent> patternStream = CEP.pattern(loginEventKeyedStream, pattern);

        //侧输出标志
        OutputTag<LoginEvent> outputTag = new OutputTag<LoginEvent>("timeout") {};

        //process方式提取数据
        SingleOutputStreamOperator<Warning> process = patternStream.process(new MyPatternProcessFunction(outputTag));
        process.print("process login failed twice");
        //提取超时数据
        process.getSideOutput(outputTag).print("process timeout");

        //select方式提取数据
//        SingleOutputStreamOperator<Warning> outputStreamOperator = patternStream
//                .select(
//                        outputTag,
//                        new PatternTimeoutFunction<LoginEvent, LoginEvent>() {
//                    @Override
//                    public LoginEvent timeout(Map<String, List<LoginEvent>> map, long l) throws Exception {
//
//                        return map.get("begin").iterator().next();
//                    }
//                },
//                        new PatternSelectFunction<LoginEvent, Warning>() {
//                    @Override
//                    public Warning select(Map<String, List<LoginEvent>> map) throws Exception {
//                        LoginEvent begin = map.get("begin").iterator().next();
//                        LoginEvent next = map.get("next").iterator().next();
//
//                        return new Warning(begin.getUserId(), begin.getEventTime(), next.getEventTime(), "Login failed twice");
//                    }
//                });

        //提取超时的数据
//        DataStream<LoginEvent> timeoutDataStream = outputStreamOperator.getSideOutput(outputTag);
//        timeoutDataStream.print("timeout");

        //提取匹配数据
//        outputStreamOperator.print("Login failed twice");


        env.execute();
    }


    //TimedOutPartialMatchHandler提供了另外的processTimedOutMatch方法,这个方法对每个超时的部分匹配都会调用。
    static class MyPatternProcessFunction extends PatternProcessFunction<LoginEvent, Warning> implements TimedOutPartialMatchHandler<LoginEvent> {
        private OutputTag<LoginEvent> outputTag;

        public MyPatternProcessFunction(OutputTag<LoginEvent> outputTag) {
            this.outputTag = outputTag;
        }

        @Override
        public void processMatch(Map<String, List<LoginEvent>> map, Context context, Collector<Warning> collector) throws Exception {
            LoginEvent begin = map.get("begin").iterator().next();
            LoginEvent next = map.get("next").iterator().next();

            collector.collect(new Warning(begin.getUserId(), begin.getEventTime(), next.getEventTime(), "Login failed twice"));
        }

        @Override
        public void processTimedOutMatch(Map<String, List<LoginEvent>> map, Context context) throws Exception {
            context.output(outputTag,map.get("begin").iterator().next());
        }
    }
}

结果分析

从输入结果来看正常匹配的应该是userId为2的,它在5秒内失败了两次。userId为3的fail1将会出现超时,因为间隔5秒,不满足5秒内连续失败两次。
结果输出:
在这里插入图片描述

注意:部分小伙伴会对超时有误解,这里解释一下超时的定义。在Flink CEP中只有等到下一条数据来了,才会判断上一条数据是否超时,而不是等到事件窗口到了,就立即判断这条数据是否超时。拿上面的例子说就是当userId为3,时间戳为1575600189000的事件到来时,判断没有匹配上第二个条件,因此事件1575600184000为超时。

小结

今天给大家分享了什么是CEP,Flink CEP的原理与使用方法,并以Flink 1.12.0为例,给出一个实战例子,可以说是干货满满。
如果对大数据感兴趣可以和我一起探讨喔~

微信公众号:喜讯Xicent

image

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值