Flink CEP实战:流上的正则表达式与模式匹配

发布时间:2026/10/17 12:11:35
Flink CEP实战:流上的正则表达式与模式匹配 我最初被CEP这个缩写绕了很久。刚接触Flink那阵子我以为它就是处理窗口统计、累加器这类聚合计算的直到某次在生产环境排查告警漏报时同事丢给我一段处理登录风险的模式匹配代码我盯着Pattern、next、within这些API第一反应是这不就是正则表达式换了个长相本质上**Flink复杂事件处理CEP**就是给事件流写正则表达式文本正则在一串字符里找符合规则的字串CEP在一条源源不断的事件流里找符合逻辑关系的事件组合。这篇就顺着这个类比把CEP的模式定义、匹配引擎、时间语义和实战里那些容易翻车的坑拆开聊清楚适合刚接触Flink、想弄明白CEP到底能干吗的读者也适合已经写过几个CEP Demo、想在线上用得更稳的人。1. 为什么说CEP是“流上的正则表达式”1.1 从一次告警漏报说起很多业务场景里我们关心的不是一小时有多少用户登录而是一个用户是否在30分钟内连续失败登录三次。前者是典型的窗口聚合后者是事件与事件之间的因果关系、时序关系和次数关系。我当时遇到的问题是一条业务告警要求设备在2分钟内先后出现A相过温、B相过温、断电信号才允许触发保护流程用普通DataStream API写就要自己维护一条存储历史状态的缓存还要考虑状态过期、消息乱序逻辑一复杂就全是洞。CEP这个组件恰恰是为了这类问题设计的。1.2 正则表达式与CEP的逐项对照我试过给新同学讲CEP最有效的办法就是让他先回忆正则。正则表达式的核心是字符、位置和重复量词CEP的核心是事件、事件间关系和次数窗口正则表达式CEP对应概念作用字符类[abc]事件的where条件筛选满足条件的事件连接隐式next()/followedBy()约束前后事件的衔接关系..*任意匹配followedBy()之间允许无关事件表达中间可以夹别的或必须紧接着a{3}重复次数同一模式上多次next/where表达连续出现N次^$锚定begin()/ 流边界确定匹配从哪里开始分组Pattern的组合嵌套组织复杂的子规则\\d{2}-\\d{2}分段多个Pattern连接表达业务上的多阶段序列打个比方文本正则让字符串里的可见规律变成可被计算机识别的语法CEP让事件流里的业务规律变成可以被实时识别的规则。两者的底层思维模型完全同源——都是先定义模式然后对带顺序的输入做自动机匹配。1.3 CEP在Flink中的定位CEP在Flink里不是一个独立的数据源也不是单独的存储它本质上是DataStream API之上的一套模式匹配库。它消费的仍然是你用DataStream API构造的流匹配结果也会以DataStream的形式输出所以能和旁路输出、侧输出流、各种sink无缝衔接。这也是我喜欢它的原因不需要把事件导到外部规则引擎再回传一条链上就能把发现异常和触发处置做完。2. CEP Pattern API白话拆解一个模式是怎么搭出来的2.1 四个最常用的连接词写CEP绝大多数时间是在写Pattern。这个对象用起来和拼正则很像先用begin(名称)定义起点然后用一系列条件约束这个事件字符长什么样再用连接词把这些条件串起来。最常用的连接词有四个含义差异非常关键next()表示两个事件紧挨着出现中间不允许插入任何其他事件。对应正则里的紧邻匹配。比如先失败再成功这种严格顺序。followedBy()表示两个事件先后出现但中间可以插入无关事件。对应.*这种宽松匹配。适合描述在这个用户会话中先加购后下单这类不要求贴脸挨着的逻辑。followedByAny()表示不要求顺序严格出现在同一分区事件串的邻近位置允许匹配到非严格连续的事件常用于只要出现过的场景。notNext()/notFollowedBy()否定语义表示某个事件没有出现。比如连续三次失败登录后紧接着没有出现成功登录才算风险这就要用到否定连接词。记住一个口诀next是贴贴followedBy是隔空not是排除。顺序约束越严格状态机需要记录的信息越少性能也越好。能用next不用followedBy能用followedBy不用followedByAny这条经验在CEP调优里非常实用。2.2 次数约束与条件过滤的表达条件过滤用where它接收一个SimpleCondition返回布尔值。重复次数不用写循环Flink提供了一套量词语法像times(n)、times(2, 4)、oneOrMore()、optional()。这些词的含义和正则里的{n}、{n,m}、、?几乎一一对应。PatternUserEvent, ? pattern Pattern .UserEventbegin(collect) .where(new SimpleConditionUserEvent() { Override public boolean filter(UserEvent event) { return event.getAction().equals(addCart); } }) .times(3) .followedBy(pay) .where(new SimpleConditionUserEvent() { Override public boolean filter(UserEvent event) { return event.getAction().equals(pay); } }) .within(Time.minutes(30));这段模式表达的是一个用户30分钟内连续加购3次之后产生了支付动作。注意times(3)只作用于begin里的collect条件followedBy(pay)是它后面单独的一段。能这么写的前提是两条事件都在同一条流上并且按键分区能保证同一用户的事件进到同一个并行实例。2.3 完整Demo连续三次失败登录把上面的概念串起来一个典型demo长这样。假设数据源里每一条记录是一次登录尝试字段有userId、status和timestampDataStreamUserLogin loginStream env .fromSource(source, WatermarkStrategy .UserLoginforBoundedOutOfOrderness(Duration.ofSeconds(10)) .withTimestampAssigner((event, ts) - event.getTimestamp()), login-source); PatternUserLogin, ? pattern Pattern .UserLoginbegin(first) .where(login - fail.equals(login.getStatus())) .next(second) .where(login - fail.equals(login.getStatus())) .next(third) .where(login - fail.equals(login.getStatus())) .within(Time.minutes(30)); PatternStreamUserLogin patternStream CEP.pattern( loginStream.keyBy(UserLogin::getUserId), pattern); DataStreamString riskStream patternStream.process( new PatternProcessFunctionUserLogin, String() { Override public void processMatch( MapString, ListUserLogin match, Context ctx, CollectorString out) throws Exception { UserLogin third match.get(third).get(0); out.collect(用户 third.getUserId() 连续三次登录失败,时间: new Timestamp(third.getTimestamp())); } });运行结果会按用户维度输出一条风险记录。这里有几个值得留意的点keyBy(userId)对应正则里的按分组匹配同一用户的事件才可能进入同一个匹配序列within的窗口是一个最大时间跨度超出后会终结未完成的匹配PatternProcessFunction拿到的match是一个Map键是你在模式里起的名字值是命中的事件列表。3. 匹配引擎跑起来了NFA状态机与一次匹配的一生3.1 白话NFA不背状态机的自动机都是空谈CEP能实时匹配的核心是Flink在内部实现了一个NFA非确定性有限自动机。正则引擎匹配字符串时用的就是NFA/DFACEP也是同类思路每到来一个事件就把它喂给当前所有正在等待该事件的状态能推进的状态就推进推不动的就丢弃或等待。NFA的非确定性体现在它可能同时跟踪多条路径——因为流上的事件顺序可能交叉CEP需要同时维护多个潜在匹配分支。这种同时跟踪的分支数量本质上就是CEP状态量的主要来源。我见过有人把CEP理解成来一个事件判断一次那是错的。它是来一个事件要判断它能不能推进所有未完成的匹配分支。分支越多计算开销越大。3.2 一次匹配的完整执行路线拿前面三次失败登录的例子匹配的大致过程是用户A第一次失败登录到达first状态迎头匹配记录事件并推进到等待second。用户A第二次失败登录到达此时模式已经推进到second匹配成功再推进到等待third。用户A插入一条成功登录事件。因为模式用的是next(third)要求second之后紧跟着third事件所以这条成功事件直接让当前匹配分支终结。用户A第三次失败登录到达但前面的分支已经终结所以这条事件只能尝试开启一个新的first匹配。如果用户A在30分钟内完成了三次连续的失败登录且没有成功事件打断第三次失败登录到达后所有命名事件都齐了产生一条匹配输出结果。如果你把next换成followedBy步骤3里的成功登录就不会打断匹配第三次失败登录仍然会命中。这个差异在实际业务里极其重要——很多漏报其实不是程序bug而是连接词选错了。3.3 匹配完成后事件还能用吗匹配完成后这些事件就被消费掉了。如果有另一个模式也想匹配相同事件需要借助匹配策略后文会讲或把原始流广播给多套模式。从语义上讲CEP不是把事件流存起来再查而是事件经过模式的NFA边沿时实时推进整个过程的吞吐取决于分支数和状态数而不是历史事件的总量。这也是它能做长时间模式比如1小时窗口而不至于全量缓存流数据的原因。4. 时间才是真正的裁判watermark与within的纠缠4.1 事件时间、处理时间与水位线CEP里时间不是配角。within(Time.minutes(30))这个时间窗口用的是事件时间还是处理时间取决于你流上设置的时间语义。生产环境里几乎都应该用事件时间因为数据可能因为网络抖动、上游重试而乱序到达。设置事件时间后Flink会通过watermark来判定事件时间进度到哪里了。watermark的计算在这里有个隐性影响如果某个用户的失败登录事件乱序晚到而watermark已经越过了within的边界这个事件就参加不了匹配甚至可能作为迟到的孤立事件开启一个刚初始化就面临超时的新匹配。你看到的漏报有一半是watermark和within的交互造成的。4.2 超时事件到底去哪了within窗口不是到点就清空这么简单。一个已经开始但还没凑齐所有事件的匹配在窗口结束时会被判定为超时。如果不加处理超时的部分匹配就静默丢弃了。想捕获这种超时事件要同时实现PatternProcessFunction和TimedOutPartialMatchHandlerprivate static class RuleProcessFunction extends PatternProcessFunctionUserLogin, String implements TimedOutPartialMatchHandlerUserLogin { Override public void processMatch( MapString, ListUserLogin match, Context ctx, CollectorString out) throws Exception { // 命中完整模式 } Override public void processTimedOutMatch( MapString, ListUserLogin match, Context ctx) throws Exception { // 窗口内只有部分事件满足走到这里 } }这个机制很实用。比如已成功登录但尚未完成交易配置这类半途模式超时后你可以输出一条待人工介入的记录而不是眼看着它消失。4.3 迟到事件导致重复匹配还有一个容易忽略的问题事件时间下watermark已经推进迟到的历史事件还会被CEP接收并参与匹配。如果下游是告警系统要自行设计去重逻辑比如按事件ID去重否则会把同一逻辑事件重复触发多次。我踩过的坑是upstream Kafka分区数变化后重放了部分数据CEP把同一次失败登录匹配了两遍直接导致重复告警。后来我在进入CEP之前加了一个append-only去重按业务唯一键过滤掉重复事件问题才消失。5. 实战踩坑重叠匹配、状态膨胀与反序列化5.1 afterMatchSkipStrategy四种策略怎么选一个模式匹配成功后接下来的事件该从哪个状态继续这个策略直接决定你会不会收到一堆套娃式的重复匹配。Flink提供了几种AfterMatchSkipStrategy我在项目里主要用前两种策略行为适用场景NO_SKIP匹配成功后从当前事件流位置继续可能和下一次匹配产生重叠需要捕捉所有包含关系的模式SKIP_TO_NEXT跳过紧邻的下一个事件避免相邻两次匹配共享起始事件连续告警去重多数业务的默认选择SKIP_PAST_LAST_EVENT跳到本次匹配最后一个事件之后互不重叠只关心互斥匹配SKIP_TO_FIRST/SKIP_TO_LAST跳到指定模式名对应的事件位置复杂模式需要精确定位建议线上默认先考虑SKIP_PAST_LAST_EVENT或SKIP_TO_NEXT除非你要做统计所有包含关系的分析。NO_SKIP在长窗口、高并发下会产生大量输出很容易把下游打爆。5.2 状态膨胀与checkpoint内存告警时先查谁CEP的状态存放的是每个分组keyBy维度当前未完成的匹配分支以及事件内容。如果定义的模式包含了followedByAny和oneOrMore()状态可能以肉眼可见的速度膨胀。一个小技巧在进入CEP之前把事件裁剪成模式需要的字段不要原样把整个JSON对象塞进去。另一个更实用的方法是用Pattern.where的条件尽早过滤掉无关事件减少进入NFA的事件量。状态膨胀还跟checkpoint配置强相关。CEP的状态都在ValueState/MapState这类managed state里checkpoint间隔越长、模式窗口越长恢复时重放的事件越多。建议先看operator state的大小和rocksdb的状态情况再决定是精简模式还是提高within的约束强度。5.3 泛型擦除反序列化异常的常见来源CEP的Pattern通常和DataStream API一样要处理类型信息。如果你用自定义POJO注意类要有无参构造和可访问的字段getter/setter。很多反序列化问题不是函数式逻辑错误而是类结构不满足Flink的序列化器要求。一个非常常见的报错是org.apache.flink.api.common.typeinfo.TypeInformation缺失或GenericType导致的低效序列化。我处理这个问题有一套固定流程先开env.disableOperatorChaining()在本地调试看CEP算子接收的事件类型是否和DataStream侧不一致然后检查Pattern中的每个where条件是否都能拿到正确字段最后确认within的时间单位没有被简写混淆——Time.milliseconds(1)和Time.seconds(1)是完全不同的。这种逐项排查比直接看堆栈高效得多。6. CEP不是银弹什么时候绕开它6.1 窗口聚合能搞定的事没必要CEP很多人上手CEP后容易形成路径依赖遇事不决就上Pattern。但CEP是为事件组合设计的不是为统计设计的。如果你只需要过去5分钟内登录失败次数大于3一个TumblingEventTimeWindow加ProcessWindowFunction就够了不需要引入模式匹配的开销。CEP的高明之处在于处理事件间的序而不是聚合成一个标量。6.2 状态编程与CEP的分工用MapState手写匹配逻辑能在阅读性和灵活性上按需定制但性能和正确性非常考验功力。CEP把NFA逻辑封装好了代价是你需要按它的语义组织模式。实际选型我一般这样看规则相对固定、状态维度多、需要处理超时和重叠匹配就用CEP规则频繁变化、需要动态加载规则的则更适合把Pattern预编译成脚本或放到规则引擎里。6.3 硬上CEP的三道成本第一道是状态成本长窗口多分支意味着状态量大第二道是CPU成本每来一个事件都要喂给所有未完成分支第三道是运维成本Pattern的语义在团队里不是谁都懂排错门槛比普通DataStream高。我在一个项目里曾试图把十几个业务规则全部堆到一个CEP算子里面结果状态和背压双双告警。后来按规则优先级拆成多个CEP算子和多条流每个算子的责任单一吞吐反而上去了——单一职责这件事在CEP这格外重要。我的实际体会是CEP最合适的位置不是把所有复杂逻辑都往里塞而是在事件流中精准划出一条业务雷区的边界。把模式设计得越短小、越明确状态越容易控制告警越不容易误报。如果你刚开始尝试CEP建议从小范围规则做起先跑通连续两事件匹配再加上within最后才上多分支和skip策略。这样每一步需要排查的问题都是可控的。等你在生产环境里连续稳定跑上一两个星期再来回看这个流上的正则表达式的类比你会发现它比任何文档都更能帮你理解CEP的边界。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询