Flink CEP Pattern API、连续性、跳过策略、超时与迟到数据一篇讲透

1. 快速开始:依赖与基本骨架

FlinkCEP 不是 Flink binary 的默认组件(集群跑时需要把 jar 链接/分发到集群),你的项目里通常先加 Maven 依赖:

xml 复制代码
<dependency>
  <groupId>org.apache.flink</groupId>
  <artifactId>flink-cep</artifactId>
  <version>2.2.0</version>
</dependency>

一个最小可运行的 CEP 骨架是:

  1. 输入流 DataStream<Event>(建议 event time + watermark)
  2. 定义 Pattern(模式图)
  3. CEP.pattern(input, pattern) 得到 PatternStream
  4. process(...) / select(...) 把匹配输出成业务结果流

另外有个容易忽略的要求:参与匹配的事件对象需要正确实现 equals() / hashCode(),因为 CEP 内部会拿它们做比较和匹配。

2. Pattern 的本质:你在画一张"状态机图"

官方说"pattern sequence is a graph",你可以把它理解为:

你用 begin("start") -> next("middle") -> followedBy("end") 这些 API,在画一个事件序列状态机;当事件流跑过来时,CEP 会维护大量"部分匹配"(partial match),直到完整走完图,才输出一个 match。

每个 pattern 节点必须有唯一名字(后面从 Map<String, List<Event>> 里取匹配结果就靠这个名字)。注意:pattern 名字不能包含 :

3. 单个 Pattern 怎么写:条件与类型约束

3.1 where / or:事件是否能"进入"这个节点

  • where(...):必须满足条件才能被该节点接受
  • 多次 where() 连续调用是 AND
  • or(...) 把条件变成 OR
java 复制代码
start.where(SimpleCondition.of(e -> e.getName().startsWith("foo")))
     .or(SimpleCondition.of(e -> e.getId() == 42));

3.2 SimpleCondition vs IterativeCondition:要不要看"历史"

  • SimpleCondition:只看当前事件本身(最快、最简单)
  • IterativeCondition:可以访问同一个 partial match 中先前已接受的事件(功能强,但要注意性能)
java 复制代码
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 prev : ctx.getEventsForPattern("middle")) {
        sum += prev.getPrice();
      }
      return sum < 5.0;
    }
  });

经验建议:ctx.getEventsForPattern(...) 的成本会随匹配复杂度上涨,能不用就不用,或者尽量减少遍历次数。

3.3 subtype:类型过滤

如果你的输入流类型是 Event,但某个节点只接受 SubEvent

java 复制代码
pattern.subtype(SubEvent.class)
       .where(SimpleCondition.of(se -> se.getVolume() >= 10.0));

4. 量词 Quantifier:一次、N 次、范围、可选、贪婪

CEP 里"单节点"默认只接收 1 条事件(singleton)。如果你要让一个节点接收多条(looping pattern),就用量词:

  • oneOrMore():至少 1 次(b+)
  • times(n):恰好 n 次
  • times(from, to):范围次数
  • timesOrMore(n):至少 n 次
  • optional():可出现 0 次
  • greedy():尽可能多地吃(当前只支持量词节点,不支持 group 贪婪)

你可以把它当成正则里的 + ? {m,n}

java 复制代码
start.times(2, 4).optional().greedy();

重要提醒:对 looping pattern(oneOrMore/times)强烈建议搭配 within()until() 来清理状态,不然在高吞吐长时间运行里,partial match 会持续增长,状态压力会很大。

5. Pattern 之间的连续性:next / followedBy / followedByAny

这是 CEP 最"容易写错"的点,因为写出来都能跑,但输出差别巨大。

5.1 next:严格连续(Strict Contiguity)

next("b") 要求 b 必须紧挨着 a,中间不能有任何不匹配事件。

5.2 followedBy:宽松连续(Relaxed Contiguity)

允许中间插入无关事件,语义更像"跳过不匹配直到下一个匹配"。

5.3 followedByAny:非确定性宽松(Non-deterministic Relaxed)

不仅允许插入无关事件,还会产生更多组合匹配(同一个 start 可以对应多个 middle/end),匹配数量可能爆炸式增长。

经典对比:pattern "a b",输入 a, c, b1, b2

  • next:无匹配(c 破坏连续)
  • followedBy:只匹配 {a b1}
  • followedByAny:匹配 {a b1}{a b2}

5.4 NOT 模式:notNext / notFollowedBy

  • notNext("x"):紧接着不能出现 x,否则丢弃该 partial match
  • notFollowedBy("x"):在两段之间任意位置不能出现 x

注意两条限制:

  • pattern sequence 如果末尾是 notFollowedBy(),必须配 within()
  • NOT pattern 不能跟在 optional pattern 后面

6. looping pattern 内部连续性:consecutive 与 allowCombinations

当你写 oneOrMore() 这种"多次"节点时,节点内部默认是 relaxed contiguity。

如果你希望"这些 repeated 事件必须紧挨着",用 consecutive()

java 复制代码
.oneOrMore().consecutive()

如果你希望"重复节点内部也产生更多组合"(类似 followedByAny 的组合爆炸),用 allowCombinations()

java 复制代码
.oneOrMore().allowCombinations()

工程上要谨慎:allowCombinations() 很容易导致匹配结果数量急剧上升,尤其在高基数 key 或热点 key 下会放大状态与 CPU。

7. within:给整个 pattern sequence 加时间窗口

within(Duration.ofSeconds(10)) 表示:从该 partial match 开始到完成匹配,必须在 10 秒内,否则丢弃(并且你可以捕获"超时 partial match",后面会讲)。

一个 pattern sequence 只能有一个时间约束,如果你在不同节点上写多个,最终会取最小的那个。

8. AfterMatchSkipStrategy:控制"一个事件被复用到多少个匹配"

CEP 的默认行为是:同一条事件可以参与多个成功匹配。为了控制结果数量与业务语义,需要 skip strategy。

常用五种:

  • noSkip():全输出(最多)
  • skipToNext():输出一个 match 后,丢掉"和这个 match 共享同一起点事件"的其他 partial match(适合避免同起点产生多结果)
  • skipPastLastEvent():输出一个 match 后,丢掉"在该 match 覆盖范围内启动的所有 partial match"(最激进,结果最少)
  • skipToFirst("patternName"):跳到某节点第一次出现的位置
  • skipToLast("patternName"):跳到某节点最后一次出现的位置

设置方式:

java 复制代码
AfterMatchSkipStrategy skip = AfterMatchSkipStrategy.skipPastLastEvent();
Pattern<Event, ?> pattern = Pattern.begin("start", skip)
    .where(...)
    .followedBy("middle").where(...)
    .followedBy("end").where(...);

实战建议:

  • 你只想要"最典型的一条告警",别让同一起点产生一堆结果:优先考虑 skipToNext()
  • 你只想要"完全不重叠的匹配":优先考虑 skipPastLastEvent()
  • 如果你的模式里有 oneOrMore(),默认 noSkip() 可能会让结果量很夸张,务必明确选择策略

9. 输出与处理:推荐用 PatternProcessFunction(并处理超时)

9.1 processMatch:每次完整匹配触发一次

processMatch 收到的是:

Map<String, List<IN>> match

key 是 pattern 名字,value 是该节点接收的事件列表(因为 looping 节点可能接收多条)。

java 复制代码
DataStream<Alert> result = patternStream.process(
  new PatternProcessFunction<Event, Alert>() {
    @Override
    public void processMatch(Map<String, List<Event>> pattern,
                             Context ctx,
                             Collector<Alert> out) {
      Event start = pattern.get("start").get(0);
      Event end   = pattern.get("end").get(0);
      out.collect(new Alert(start, end));
    }
  }
);

Context 里还能拿到时间信息(processing time / timestamp 等),并支持 side output。

9.2 超时 partial match:TimedOutPartialMatchHandler(用 side output 旁路)

只要你用了 within(...),就可能发生"开始了但没完成就超时"的 partial match。可以用 mixin 方式实现 TimedOutPartialMatchHandler

java 复制代码
OutputTag<TimeoutEvent> timeoutTag = new OutputTag<>("timeout"){};

SingleOutputStreamOperator<Alert> main = patternStream.process(
  new PatternProcessFunction<Event, Alert>() implements TimedOutPartialMatchHandler<Event> {
    @Override
    public void processMatch(Map<String, List<Event>> match, Context ctx, Collector<Alert> out) {
      out.collect(createAlert(match));
    }

    @Override
    public void processTimedOutMatch(Map<String, List<Event>> match, Context ctx) {
      Event start = match.get("start").get(0);
      ctx.output(timeoutTag, new TimeoutEvent(start));
    }
  }
);

DataStream<TimeoutEvent> timeoutStream = main.getSideOutput(timeoutTag);

注意:processTimedOutMatch 不能写主输出,只能用 side output。

9.3 旧 API:select / flatSelect 仍可用,但底层会转成 PatternProcessFunction

新项目建议直接用 process(...),逻辑更直观,能力也更完整。

10. Event Time 下迟到数据:CEP 假设 watermark 正确

CEP 对 event time 的处理逻辑是:

  • 元素先进入 buffer,按 timestamp 排序
  • watermark 到来时,处理 timestamp < watermark 的元素
  • timestamp 小于"最后看到的 watermark"的事件,被认为是 late element,不再参与匹配

如果你不想让迟到数据悄悄丢掉,可以用 sideOutputLateData

java 复制代码
OutputTag<Event> lateTag = new OutputTag<>("late-data"){};

SingleOutputStreamOperator<Alert> out = patternStream
  .sideOutputLateData(lateTag)
  .select(new PatternSelectFunction<Event, Alert>() { ... });

DataStream<Event> late = out.getSideOutput(lateTag);

工程建议:如果业务允许一定乱序,一定要把 watermark 策略和 allowed lateness 设计好;CEP 本身是"以 watermark 为分界线"的。

11. 性能与内存:SharedBuffer cache 参数什么时候有用

CEP 内部维护 SharedBuffer 来保存 partial matches 与事件引用。官方给了三项 cache 配置:

  • pipeline.cep.sharedbuffer.cache.entry-slots
  • pipeline.cep.sharedbuffer.cache.event-slots
  • pipeline.cep.sharedbuffer.cache.statistics-interval

关键点是:这些 cache 主要在 state backend = RocksDB 时用于限制纯内存占用,超过 cache 的部分会被"换出"到 RocksDB state 里。

反过来,如果你不是 RocksDB(例如 heap hashmap state),开启 cache 反而可能拖慢性能(copy-on-write 等开销变重)。

一句话策略:

  • RocksDB:可以用 cache slots 控制内存并换取可控的吞吐
  • 非 RocksDB:谨慎开启,先压测再决定

12. 一个更完整的示例:keyBy + within + 超时 + 迟到旁路

下面示意一个常见告警:同一个 id 的事件流中,出现 error -> critical,要求 10 秒内完成。

java 复制代码
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

DataStream<Event> input = ...; // 建议设置 watermark

DataStream<Event> keyed = input.keyBy(Event::getId);

AfterMatchSkipStrategy skip = AfterMatchSkipStrategy.skipPastLastEvent();

Pattern<Event, ?> pattern = Pattern.<Event>begin("start", skip)
  .next("middle")
  .where(SimpleCondition.of(e -> "error".equals(e.getName())))
  .followedBy("end")
  .where(SimpleCondition.of(e -> "critical".equals(e.getName())))
  .within(Duration.ofSeconds(10));

PatternStream<Event> ps = CEP.pattern(keyed, pattern);

OutputTag<Event> lateTag = new OutputTag<>("late"){};

OutputTag<TimeoutEvent> timeoutTag = new OutputTag<>("timeout"){};

SingleOutputStreamOperator<Alert> alerts = ps
  .sideOutputLateData(lateTag)
  .process(new PatternProcessFunction<Event, Alert>() implements TimedOutPartialMatchHandler<Event> {

    @Override
    public void processMatch(Map<String, List<Event>> match, Context ctx, Collector<Alert> out) {
      Event err = match.get("middle").get(0);
      Event cri = match.get("end").get(0);
      out.collect(new Alert(err, cri));
    }

    @Override
    public void processTimedOutMatch(Map<String, List<Event>> match, Context ctx) {
      Event err = match.get("middle").get(0);
      ctx.output(timeoutTag, new TimeoutEvent(err));
    }
  });

DataStream<Event> late = alerts.getSideOutput(lateTag);
DataStream<TimeoutEvent> timeouts = alerts.getSideOutput(timeoutTag);

你可以按业务需要,把 next/followedBy 调整为更严格或更宽松的连续性,并且用 skip strategy 控制输出爆炸。

13. 迁移提示:老版本 savepoint

如果你需要从 Flink <= 1.5 的 savepoint 恢复,官方策略是:先迁移到 1.6--1.12,重新打 savepoint,再用 Flink >= 1.13 恢复(因为 1.13 起不再兼容 <=1.5 的 savepoint)。

结尾:写 CEP 最容易翻车的 3 件事

  1. 连续性没想清楚:followedByAny 直接把输出量放大好几倍
  2. looping pattern 不加 within() / until():状态长期累积
  3. event time watermark 设计不当:迟到数据悄悄被 CEP 当 late 丢掉(建议 sideOutputLateData 兜底)
相关推荐
极海拾贝4 小时前
GeoScene解决方案中心正式上线!
大数据·人工智能·深度学习·arcgis·信息可视化·语言模型·解决方案
qq_262496097 小时前
Elasticsearch 核心参数调优指南
大数据·elasticsearch
OpenCSG8 小时前
AgenticOps 如何重构企业 AI 的全生命周期管理体系
大数据·人工智能·深度学习
阿里云大数据AI技术8 小时前
漫画说:为什么你的“增量计算”越跑越慢?——90%的实时数仓团队都踩过的坑,藏在这几格漫画里
大数据·人工智能
电商API_180079052478 小时前
批量获取电商商品数据的主流技术方法全解析
大数据·数据库·人工智能·数据分析·网络爬虫
Zoey的笔记本9 小时前
敏捷与稳定并行:Scrum看板+BPM工具选型指南
大数据·前端·数据库·python·低代码
俊哥大数据9 小时前
【项目7】 基于Flink新闻资讯大数据推荐系统
大数据·flink
Coder_Boy_10 小时前
基于SpringAI的在线考试系统-企业级软件研发工程应用规范实现细节
大数据·开发语言·人工智能·spring boot
Hello.Reader10 小时前
Flink State Processor API 读写/修复 Savepoint,把“状态”当成可查询的数据
大数据·flink