Flink CEP

79 阅读13分钟

1.介绍

(1) 功能

它是一组(多条)数据的匹配,比如data1来了符合模板1,然后data2来了符合模板2,data2紧跟data1后面,然后data3来了符合模板3,这样三条数据都符合一个大的pattern才认为是匹配的

  1. 模式匹配 支持定义基于事件属性、顺序或时间窗口的复杂规则,例如检测“连续三次登录失败”“A事件后5分钟内发生B事件”等模式。
  2. 灵活的时间窗口 可在事件时间(Event Time)或处理时间(Processing Time)上定义滑动窗口、滚动窗口等,适应乱序数据流。
  3. 精确一次处理语义 提供端到端的一致性保障,确保事件不重复或遗漏。
  4. 动态规则支持 最新版本支持动态更新规则(如修改阈值、条件),无需重启作业,适用于实时调整策略的场景。

(2) 应用

  1. 金融风控
  • 欺诈检测:识别短时间内多次大额转账、异地登录等异常交易行为。
  • 信用评估:监控用户还款行为,如连续逾期触发风险预警。
  1. 网络安全
  • 攻击检测:检测短时间内大量登录失败事件(如暴力破解)。
  • DDoS防御:发现同一IP高频访问请求并触发拦截4。
  1. 物联网(IoT)
  • 设备异常监测:识别传感器数据异常(如温度骤升、连续超负荷运行)。
  • 共享设备管理:监测共享单车被骑出指定区域且未及时返回的情况。
  1. 电商与营销
  • 用户行为分析:捕捉用户“加购多次但未付款”的行为,优化营销策略。
  • 反作弊:识别虚假点击或刷单行为。
  1. 工业自动化
  • 检测生产线上的连续故障信号,及时停机维护

(3) Pattern

Flink为CEP提供了专门的Flink CEP library,它包含如下组件:Event Stream、Pattern定义、Pattern检测和生成Alert。

定义模式主要有如下 5 个部分组成:

  • pattern:前一个模式
  • next/followedBy/…:开始一个新的模式
  • start:模式名称
  • where:模式的内容
  • filter:核心处理逻辑

处理事件的规则,被叫作模式(Pattern)。 Flink CEP提供了Pattern API用于对输入流数据进行复杂事件规则定义,用来提取符合规则的事件序列。 模式大致分为三类:

① 个体模式(Individual Patterns) 组成复杂规则的每一个单独的模式定义,就是个体模式。

start.times(3).where(_.behavior.startsWith(‘fav’))

② 组合模式(Combining Patterns,也叫模式序列) 很多个体模式组合起来,就形成了整个的模式序列。 模式序列必须以一个初始模式开始:

val start = Pattern.begin(‘start’)

<1> 个体模式

个体模式包括单例模式和循环模式。单例模式只接收一个事件,而循环模式可以接收多个事件。

(1)量词 可以在一个个体模式后追加量词,也就是指定循环次数

// 匹配出现4次
start.time(4)
// 匹配出现0次或4次
start.time(4).optional
// 匹配出现2、3或4次
start.time(2,4)
// 匹配出现2、3或4次,并且尽可能多地重复匹配
start.time(2,4).greedy
// 匹配出现1次或多次
start.oneOrMore
// 匹配出现0、2或多次,并且尽可能多地重复匹配
start.timesOrMore(2).optional.greedy

(2)条件

可以通过pattern.where(),pattern.or()或pattern.until()方法指定事件属性的条件。 条件可以是IterativeConditions或SimpleConditions。

每个模式都需要指定触发条件,作为模式是否接受事件进入的判断依据。按不同的调用方式,可以分成以下几类:

① 简单条件 通过.where()方法对事件中的字段进行判断筛选,决定是否接收该事件

middle.oneOrMore().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;
    }
});

scala==> start.where(event=>event.getName.startsWith(“foo”))

② 组合条件 将简单的条件进行合并;or()方法表示或逻辑相连,where的直接组合就相当于与and。

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
    }
});

scala==> start.where(event => …/*some condition*/).or(event => /*or condition*/)

③ 终止条件 仅限使用了oneOrMore或者oneOrMore.optional,然后使用.until()作为终止条件,以便清理状态。

pattern.oneOrMore().until(new IterativeCondition<Event>() {
    @Override
    public boolean filter(Event value, Context ctx) throws Exception {
        return ... // alternative condition
    }
});

④ 迭代条件 能够对模式之前所有接收的事件进行处理;调用.where((value,ctx) => {…}),可以调用ctx.getEventForPattern(“name”)

最后,还可以通过pattern.subtype(subClass)方法将接受事件的类型限制为初始事件类型的子类型。

start.subtype(SubEvent.class).where(new SimpleCondition<SubEvent>() {
    @Override
    public boolean filter(SubEvent value) {
        return ... // some condition
    }
});

<2> 组合模式

模式序列必须以初始模式开始

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

接下来,您可以通过指定它们之间所需的连续条件,为模式序列添加更多模式。 在上一节中,我们描述了Flink支持的不同邻接模式,即严格,宽松和非确定性宽松,以及如何在循环模式中应用它们。 要在连续模式之间应用它们,可以使用:

next() 对应严格, followedBy() 对应宽松连续性 followedByAny() 对应非确定性宽松连续性

亦或

notNext() 如果不希望一个事件类型紧接着另一个类型出现。 notFollowedBy() 不希望两个事件之间任何地方出现该事件。 注意 模式序列不能以notFollowedBy()结束。 注意 NOT模式前面不能有可选模式。

/* 严格连续
事件必须严格按照定义的顺序紧密相邻发生,中间不允许插入其他事件。
例如:模式 A.next(B) 要求 B 必须紧跟在 A 之后,中间无其他事件。
现象:不会出现重叠匹配的现象
*/
Pattern<Event, ?> strict = start.next("middle").where(...);

/* 宽松连续
允许在模式事件之间插入其他无关事件,只要目标事件按顺序出现即可。
例如:模式 A.followedBy(B) 只要 A 在 B 之前出现即可,中间可以有其他事件。
现象:会出现重叠匹配的情况
*/
Pattern<Event, ?> relaxed = start.followedBy("middle").where(...);

/* 非确定性宽松连续
类似 followedBy,但允许同一事件被多个模式共享(非确定性匹配)。
例如:若模式为 A.followedByAny(B).followedByAny(C),数据流 [A, B1, B2, C] 中,B1 和 B2 均可参与匹配,生成 A->B1->C 和 A->B2->C 两种结果。
*/
Pattern<Event, ?> nonDetermin = start.followedByAny("middle").where(...);

/* 严格否定
在严格连续的下一个位置,不允许出现指定事件。
例如:A.notNext(B) 表示 A 之后紧跟的事件不能是 B。
*/
Pattern<Event, ?> strictNot = start.notNext("not").where(...);

/*宽松否定
在模式后续所有事件中,不允许出现指定事件。
例如:A.notFollowedBy(B) 表示 A 之后直到模式结束,不允许出现 B。
*/
Pattern<Event, ?> relaxedNot = start.notFollowedBy("not").where(...);

也可以为模式定义时间约束。 例如,可以通过pattern.within()方法定义模式应在10秒内发生。 时间模式支持处理时间和事件时间。 注意模式序列只能有一个时间约束。 如果在不同的单独模式上定义了多个这样的约束,则应用最小的约束。定义事件序列进行模式匹配的最大时间间隔。 如果未完成的事件序列超过此时间,则将其丢弃:

next.within(Time.seconds(10));

2.案例

public class CEPDemo {

    public static class UserBehaviorEvent {
        public String userId;
        public String action;
        public Long timestamp;

        public UserBehaviorEvent(String userId, String action, Long timestamp) {
            this.userId = userId;
            this.action = action;
            this.timestamp = timestamp;
        }

        @Override
        public String toString() {
            return "[" + userId + ", " + action + ", " + timestamp + "]";
        }
    }

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

        // 生成带水位线的数据流
        DataStream<UserBehaviorEvent> eventStream = env.addSource(new UserBehaviorGenerator())
                .assignTimestampsAndWatermarks(
                        WatermarkStrategy.forGenerator(ctx -> new BoundedOutOfOrdernessGenerator())
                                .withTimestampAssigner((event, timestamp) -> event.timestamp)
                );

        eventStream.print("source");

        // 原始模式:click -> cheat -> click
        Pattern<UserBehaviorEvent, ?> pattern = Pattern.<UserBehaviorEvent>begin("first")
                .where(new SimpleCondition<UserBehaviorEvent>() {
                    @Override
                    public boolean filter(UserBehaviorEvent event) {
                        return "click".equals(event.action);
                    }
                })
                .next("second")
                .where(new SimpleCondition<UserBehaviorEvent>() {
                    @Override
                    public boolean filter(UserBehaviorEvent event) {
                        return "cheat".equals(event.action);
                    }
                })
                .next("third")
                .where(new SimpleCondition<UserBehaviorEvent>() {
                    @Override
                    public boolean filter(UserBehaviorEvent event) {
                        return "click".equals(event.action);
                    }
                })
                .within(Time.seconds(10)); // 缩短窗口时间

        // 应用原始模式
        PatternStream<UserBehaviorEvent> patternStream = CEP.pattern(
                eventStream.keyBy(e -> e.userId),
                pattern
        );

        patternStream.select(new PatternSelectFunction<UserBehaviorEvent, String>() {
            @Override
            public String select(Map<String, List<UserBehaviorEvent>> match) {
                return "匹配到序列:" + match.get("first").get(0)
                        + " → " + match.get("second").get(0)
                        + " → " + match.get("third").get(0);
            }
        }).print("match");


        env.execute("CEP Demo");
    }

    // 自定义水位线生成器
    private static class BoundedOutOfOrdernessGenerator implements WatermarkGenerator<UserBehaviorEvent> {
        private final long maxOutOfOrderness = 5000; // 5秒
        private long currentMaxTimestamp;

        @Override
        public void onEvent(UserBehaviorEvent event, long eventTimestamp, WatermarkOutput output) {
            currentMaxTimestamp = Math.max(currentMaxTimestamp, eventTimestamp);
//            System.out.println("生成水位线: " + (currentMaxTimestamp - maxOutOfOrderness));
        }

        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            // 定期生成水位线
            output.emitWatermark(new Watermark(currentMaxTimestamp - maxOutOfOrderness));
        }
    }

    // 事件生成器(增加日志)
    private static class UserBehaviorGenerator implements SourceFunction<UserBehaviorEvent> {
        private volatile boolean running = true;
        private long currentTime = System.currentTimeMillis();
        private final Random random = new Random();

        @Override
        public void run(SourceContext<UserBehaviorEvent> ctx) throws Exception {
            while (running) {
                String userId = "user_" + (random.nextInt(3) + 1);
                String action = random.nextDouble() > 0.4 ? "click" : "cheat"; // 60% click
                currentTime += 500; // 每500毫秒生成一个事件

                ctx.collectWithTimestamp(
                        new UserBehaviorEvent(userId, action, currentTime),
                        currentTime
                );

//                System.out.println("生成事件: " + userId + ", " + action + ", " + currentTime);

                TimeUnit.MILLISECONDS.sleep(500);
            }
        }

        @Override
        public void cancel() {
            running = false;
        }
    }
}

3.核心原理--NFA

NFA(Non-deterministic Finite Automaton) 是一种计算模型,用于识别符号序列中的模式。其特点包括:

  • 状态集合:包含初始状态、中间状态和接受状态。
  • 状态转移:基于输入符号或条件,可能转移到多个状态(非确定性)。
  • 多路径处理:允许同时存在多个活跃状态,处理不同的潜在匹配路径。

Flink CEP 的核心任务是在数据流中检测用户定义的复杂事件模式。其实现依赖于NFA来高效跟踪和匹配事件序列:

  • 模式定义:用户通过Flink CEP API定义事件序列模式(如连续三次登录失败)。
  • NFA构建:Flink将模式编译为一个NFA实例,每个状态对应模式中的一个步骤,转移条件对应事件间的约束。
  • 运行时匹配:数据流中的每个事件会驱动NFA状态转移,最终达到接受状态时触发匹配结果。

image.png

场景:检测连续三次温度超过40℃的事件(模式:High → High → High)。

  1. 初始化
  • NFA初始状态为START
  1. 事件1(温度42℃)
  • 转移到状态S1,记录事件1和时间戳。
  1. 事件2(温度41℃)
  • S1转移到S2,更新时间为事件2的时间戳。
  1. 事件3(温度43℃)
  • S2转移到S3(接受状态),触发匹配结果。
  1. 超时处理
  • 若事件2和事件3间隔超过窗口时间,路径S1→S2被丢弃。

4.动态CEP--扩展

例如一个CEP作业初始规则是转账用户在一分钟内连续进行3次转账后将其认为是风险操作。而在特殊场景,预期转账次数会多一点,一分钟3次的转账次数阈值可能不合适,在当前开源FlinkCEP实现下,没法做到使用户无感的转换,只能重新编写Java代码,然后重启作业,以使最新的规则生效。这样的操作带来时间成本较高和重启作业代价高的问题。因为要走一遍完整的代码开发和打包上线流程对于对时间延迟敏感程度高的银行风控领域是难以接受的,且规则引擎里通常会维护很多不同的规则,如果简单的规则修改都需要较长的时间窗口,会影响其他人的使用,维护起来也比较困难。Flink动态cep很好的降低了传统规则引擎较高的时间成本并做到无需重启作业就能丝滑更新规则

以下是Flink动态CEP解决的主要问题:

① 动态规则更新:传统规则引擎在规则变更时需要重新部署和启动作业,这会导致服务中断,影响系统的实时性和可用性。而Flink动态CEP允许在不中断服务的情况下动态加载和更新CEP规则,这意味着可以在运行时修改模式匹配逻辑,而无需重启整个Flink作业。

② 多规则支持:在静态场景下使用多条规则时,传统FlinkCEP需要创建多个CepOperator(CEP算子),这会导致数据的额外拷贝,增加处理开销。Flink动态CEP支持在一个Operator(算子)中处理多条规则,减少了数据拷贝,提高了处理效率。

③ 参数化Condition支持:Flink动态CEP支持在Json格式规则描述中定义参数化的Condition,提高了自定义Condition的拓展性,解决了动态添加新的Condition类实现的需求。

在Flink动态CEP中复用了Flink的OperatorCoordinator(算子协调器)机制,用它来负责协调FLink作业中的各个operator(算子)。OperatorCoordinator在JobManager中运行,会给TaskManager的Operator发送事件,DynamicCEPOperatorCoordinator(动态CEP算子协调器)是OperatorCoordinator的实现类,它是JobManager中运行的线程,负责调用PatternProcessorDiscoverer(模式处理器探查器)接口拿到最新的PatternProcessor。Flink动态CEP的整体架构图如下所示:

image.png

上图展示的是从数据库中读取序列化后的PatternProcessor的过程。可以看到

  1. OperatorCoordinator会调用PatternProcessorDiscoverer接口从数据库中拿到最新的且序列化后的PatternProcessor
  2. 拿到后它会发送给和它关联的DynamicCEPOp(动态cep算子)。
  3. DynamicCEPOp接收到发送的事件进行解析和反序列化后,最终生成要使用的PatternProcessor并构造相应的NFA(非确定有限状态机)。 之后即可使用新构造的NFA来处理上游发生的事件,并最终输出到下游。基于这样的方式,可以做到不停机的规则更新,且只有OperatorCoordinator和规则数据库交互,可以减少对数据库的访问,并利用Flink的特性保证下游sub_task中使用规则的一致性。

阿里云实时计算Flink版定义了一套JSON格式的规则描述,详情请参加阿里云文档——动态CEP中规则的JSON格式定义):

idversionpatternfunction
11{"name":"end","quantifier":{"consumingStrategy}…xxxpackage.dynamic.cep.core.DemoPatternProcessFunction

将 pattern 的 JSON 字符串解析后,展示如下:

{
    "name": "mixedContinuityPattern",
    "quantifier": {
        "consumingStrategy": "NO_SKIP",
        "properties": [
            "SINGLE"
        ],
        "times": null,
        "untilCondition": null
    },
    "condition": null,
    "nodes": [
        {
            "name": "A",
            "quantifier": {
                "consumingStrategy": "NO_SKIP",
                "properties": [
                    "SINGLE"
                ],
                "times": null,
                "untilCondition": null
            },
            "condition": {
                "className": "com.example.ACondition",
                "type": "CLASS"
            },
            "type": "ATOMIC"
        },
        {
            "name": "B",
            "quantifier": {
                "consumingStrategy": "NO_SKIP",
                "properties": [
                    "SINGLE"
                ],
                "times": null,
                "untilCondition": null
            },
            "condition": {
                "className": "com.example.BCondition",
                "type": "CLASS"
            },
            "type": "ATOMIC"
        },
        {
            "name": "C",
            "quantifier": {
                "consumingStrategy": "SKIP_TILL_NEXT",
                "properties": [
                    "SINGLE"
                ],
                "times": null,
                "untilCondition": null
            },
            "condition": {
                "className": "com.example.CCondition",
                "type": "CLASS"
            },
            "type": "ATOMIC"
        }
    ],
    "edges": [
        {
            "source": "A",
            "target": "B",
            "type": "TAKE"  // A和B严格连续,对应next()
        },
        {
            "source": "B",
            "target": "C",
            "type": "SKIP_TILL_NEXT"  // B和C宽松连续,对应followedBy()
        }
    ],
    "window": { // 事件间隔的窗口大小:10s,就是A事件触发后10s内,B事件要来,C事件也要来
        "type": "TIME",
        "size": 10000,
        "timeUnit": "MILLISECONDS"
    },
    "afterMatchStrategy": { // 匹配后的策略,SKIP_PAST_LAST_EVENT:跳过最后一个事件(C)之前的所有事件,从C的下一个事件开始重新检测匹配模式,若事件序列为 A→B→C→A→B→C,第一次匹配A→B→C后,从第二个A开始重新检测下一个模式。
        "type": "SKIP_PAST_LAST_EVENT",
        "patternName": null
    },
    "type": "COMPOSITE",
    "version": 1
}

模式包含 3 个原子节点(ABC),每个节点定义了事件的条件和消费策略。

节点类型消费策略属性条件(Condition)说明
AATOMICNO_SKIPSINGLE:整个模式只需匹配一次com.example.ACondition必须严格匹配单个事件,不跳过任何事件;需单个事件满足A条件。
BATOMICNO_SKIPSINGLE:整个模式只需匹配一次com.example.BCondition必须严格匹配单个事件,不跳过任何事件;需单个事件满足B条件。
CATOMICSKIP_TILL_NEXTSINGLE:整个模式只需匹配一次com.example.CCondition允许跳过中间事件,仅需匹配下一个满足条件的事件;需单个事件满足C条件。

“com.example.BCondition”这个类定义的事件,这个条件由开发者定义,例如:

public class BCondition extends SimpleCondition<Event> {

    @Override
    public boolean filter(Event value) throws Exception {

        return value.getAction() != 1;
    }
}
JSON 策略类型Java 操作符严格连续性允许中间事件示例配置
SKIP_TILL_NEXTfollowedBy()A → (任意事件) → B
TAKEnext()A → B(中间不能有其他事件)
SKIP_TILL_ANYfollowedByAny()A → (任意事件) → B(更宽松)
TAKE_ANYnextAny()匹配任意后续事件(慎用)