【Flink_CEP】MySQL 动态规则 + Kafka 实时流 + Flink CEP 后缀收集的实战方案

摘要

很多团队把 Flink CEP 只用在"是否命中"这一层,导致命中后上下文不足、调查链条断裂。本文基于一套真实工程方案,讲解如何用 MySQL 维护可运营规则、Kafka 提供实时事件流,再通过 PatternFactory 同时支持"标准序列匹配"和 collectallafter"命中后后缀收集"两种语义,并结合二次收敛策略减少重复输出。读完后你可以直接复用文中的核心代码片段,快速搭建可上线的规则检测链路。

关键词

MySQL 动态规则、Kafka、Flink CEP、PatternFactory、collectallafter、告警收敛、实时检测、攻击链研判

这篇文章面向"不拿完整项目代码也能看懂"的场景,直接给出可复制的关键代码片段,并用案例讲透 Flink CEP 两种匹配方式:

  • 标准序列匹配(默认)
  • collectallafter 后缀收集匹配(重点)

1. 业务背景与链路概览

目标是做一条实时安全检测链路:

  1. 从 Kafka 持续消费告警事件
  2. 从 MySQL 读取自定义规则
  3. 用 Flink CEP 进行模式匹配
  4. 命中后输出告警案例到 Kafka

主链路可以抽象成:

MySQL规则 -> PatternFactory编译 -> CEP匹配Kafka事件 -> 输出Case


2. MySQL 作为规则中心:规则如何落到 CEP

2.1 规则表读取

sql 复制代码
SELECT pattern_name, pattern_json, updated_at, window_time FROM pattern_config;

字段含义:

  • pattern_name:规则名
  • pattern_json:规则明细,反序列化为 List<SingleConfig>
  • window_time:规则窗口秒数

2.2 规则加载核心代码(可直接复用)

java 复制代码
public static List<PatternConfig> fetchRulesFromMySQL() {
    List<PatternConfig> ruleList = new ArrayList<>();
    if (tryFetchRulesFromMySQL(ruleList)) {
        return ruleList;
    }
    return fetchRulesFromFile();
}

private static boolean tryFetchRulesFromMySQL(List<PatternConfig> ruleList) {
    String jdbcUrl = ConfigLoader.getStringValue("mysql","url");
    String password = ConfigLoader.getStringValue("mysql","pwd");
    String user = "root";
    String sql = "SELECT pattern_name, pattern_json, updated_at, window_time FROM pattern_config;";
    try (Connection conn = DriverManager.getConnection(jdbcUrl, user, password);
         PreparedStatement stmt = conn.prepareStatement(sql);
         ResultSet rs = stmt.executeQuery()) {
        ObjectMapper mapper = new ObjectMapper();
        while (rs.next()) {
            PatternConfig config = new PatternConfig();
            config.setPatternName(rs.getString("pattern_name"));
            config.setSingleConfigs(mapper.readValue(
                    rs.getString("pattern_json"),
                    new TypeReference<List<PatternConfig.SingleConfig>>() {}));
            config.setWindowTime(rs.getInt("window_time"));
            ruleList.add(config);
        }
        return true;
    } catch (Exception e) {
        return false;
    }
}

这段逻辑体现两个设计点:

  • 规则中心在 MySQL,便于运营侧动态维护
  • DB 异常时回退文件规则,保证任务可运行

3. Kafka 消费:事件如何进入 CEP

3.1 Source 配置代码片段

java 复制代码
public static KafkaSource<AlertEvent> getKafkaSource(){
    String offsets = ConfigLoader.getStringValue("kafka","offsets");
    OffsetsInitializer initializer;
    if ("latest".equalsIgnoreCase(offsets)) {
        initializer = OffsetsInitializer.latest();
    } else if ("committed_latest".equalsIgnoreCase(offsets)) {
        initializer = OffsetsInitializer.committedOffsets(OffsetResetStrategy.LATEST);
    } else if ("committed_earliest".equalsIgnoreCase(offsets)) {
        initializer = OffsetsInitializer.committedOffsets(OffsetResetStrategy.EARLIEST);
    } else {
        initializer = OffsetsInitializer.earliest();
    }
    return KafkaSource.<AlertEvent>builder()
            .setBootstrapServers(ConfigLoader.getStringValue("kafka","servers"))
            .setTopics("xds_alert_topic")
            .setGroupId(ConfigLoader.getStringValue("kafka","group"))
            .setStartingOffsets(initializer)
            .setValueOnlyDeserializer(new AlertEventDeserializationSchema())
            .build();
}

3.2 CEP 接入点代码片段

java 复制代码
List<PatternConfig> patternConfigs = RuleFetcher.fetchRulesFromMySQL();
KafkaSource<AlertEvent> source = AlertKafkaSource.getKafkaSource();
DataStream<AlertEvent> keyedStream = env.fromSource(source, WatermarkStrategy.noWatermarks(), "Kafka Source")
        .keyBy(event -> directedPairKey(event), TypeInformation.of(new TypeHint<Tuple2<String,String>>() {}));

for (PatternConfig rule : patternConfigs) {
    Pattern<AlertEvent, ?> curPattern = PatternFactory.craftComplexPattern(rule);
    PatternStream<AlertEvent> patternStream = CEP.pattern(keyedStream, curPattern);
}

下面是项目 PatternFactory 的核心决策片段:

java 复制代码
boolean collectAllAfter = isCollectAllAfter(rule);
AfterMatchSkipStrategy skipStrategy = collectAllAfter
        ? AfterMatchSkipStrategy.skipPastLastEvent()
        : AfterMatchSkipStrategy.noSkip();

只要规则 types 出现 collectallafter,就切到第二种模式。


4.1 方式一:标准序列匹配(默认)

规则示例
json 复制代码
{
  "patternName": "扫描后异常登录后提权",
  "windowTime": 300,
  "singleConfigs": [
    {"pattern": "s1", "condition": "端口扫描", "types": ["start"]},
    {"pattern": "s2", "condition": "异常登录", "types": ["followedBy"]},
    {"pattern": "s3", "condition": "权限提升", "types": ["followedBy"]}
  ]
}
编译后的核心行为
  • skip 策略:noSkip
  • 匹配结构:start -> followedBy -> followedBy
  • 窗口:within(300s)

对应的构建代码片段:

java 复制代码
case "start":
    res = Pattern.<AlertEvent>begin(uniqueStateName(singleConfig.getPattern(), stateNameCounters), skipStrategy)
            .where(getCondition(singleConfig));
    break;
case "followedby":
    res = res.followedBy(uniqueStateName(singleConfig.getPattern(), stateNameCounters))
            .where(getCondition(singleConfig));
    break;
事件案例

输入序列(同一 key):

  1. 10:00:01 端口扫描
  2. 10:00:10 异常登录
  3. 10:00:20 权限提升
  4. 10:00:30 横向移动

输出结果:

  • 命中 1 条 Case,核心事件为前 3 条
  • 第 4 条不在规则链路中,不被自动收入该 Case

适用场景:强调"是否满足固定攻击链"。


4.2 方式二:collectallafter 后缀收集匹配

这是项目里最有特点的实现:命中核心链路后,继续把后续上下文打包。

规则示例
json 复制代码
{
  "patternName": "登录后异常行为全量收集",
  "windowTime": 180,
  "singleConfigs": [
    {"pattern": "s1", "condition": "异常登录", "types": ["start", "collectallafter"]},
    {"pattern": "s2", "condition": "执行命令", "types": ["followedBy"]}
  ]
}
PatternFactory 增强代码片段(可复制)
java 复制代码
if (collectAllAfter) {
    String gapName = uniqueStateName("__gap__" + (++gapIndex), stateNameCounters);
    res = res.followedByAny(gapName)
            .where(negateCondition(getCondition(singleConfig)))
            .oneOrMore()
            .optional()
            .greedy();
}
res = res.followedBy(uniqueStateName(singleConfig.getPattern(), stateNameCounters))
        .where(getCondition(singleConfig));
java 复制代码
if (collectAllAfter) {
    res = res.followedBy(uniqueStateName("__tail__", stateNameCounters))
            .where(alwaysTrueCondition())
            .oneOrMore()
            .greedy();
}

行为变化:

  1. skip 策略改为 skipPastLastEvent
  2. 在关键节点前允许"吞噪声事件"
  3. 在末尾追加 __tail__,把后续事件尽量收集进 Case
事件案例

输入序列(同一 key):

  1. 10:00:01 异常登录
  2. 10:00:05 心跳包
  3. 10:00:08 DNS查询
  4. 10:00:15 执行命令
  5. 10:00:20 下载工具
  6. 10:00:28 横向探测

输出结果:

  • 标准模式通常只会锁定 异常登录 -> 执行命令
  • collectallafter 模式会把 5、6 这类后续行为一并进入同一个告警案例

适用场景:强调"命中后完整上下文",便于研判和溯源。


4.3 为什么 collectallafter 还要二次收敛

因为后缀收集会自然产生重叠候选,所以项目在 TrafficDetection 加了第二段处理:

java 复制代码
if (PatternFactory.isCollectAllAfter(rule)) {
    OutputTag<AlertCase> timeoutTag = new OutputTag<AlertCase>("cep_timeout_" + curName) {};
    SingleOutputStreamOperator<AlertCase> selected = patternStream
            .process(new TimeoutOnlyPatternProcessFunction(curName, timeoutTag));
    result = selected
            .union(selected.getSideOutput(timeoutTag))
            .keyBy(TrafficDetection::collectAllAfterBurstKey)
            .process(new LongestCaseWithinBurstFunction(1500L));
}

收敛策略非常实用:

  • 正常命中和超时命中都纳入候选
  • 同一 burst 内只留 alert_set 最大的一条
  • 显著减少重复 Case 输出

5. 最小化落地建议

如果你要把这套方案快速迁移到自己的系统,建议按这 4 步:

  1. 先做标准模式规则,确认链路正确
  2. 再给重点规则加 collectallafter,提升上下文信息量
  3. 加入 burst 收敛,控制输出重复
  4. 用 MySQL 管理 pattern_json + window_time,形成可运营规则中心

6. 一句话结论

同样是 Flink CEP,标准序列匹配解决"准不准",collectallafter 解决"全不全";两者组合,再加收敛策略,才能同时满足检测、研判、归并三种真实业务诉求。

  • 规则放在 MySQL,是为了让检测能力可运营;语义放在 PatternFactory,是为了让检测能力可进化。
  • 标准 CEP 解决"发现问题",collectallafter 解决"讲清问题"。
  • 实时检测不是只要命中率,更要给出足够上下文让研判和处置能闭环。
相关推荐
ANii_Aini2 小时前
mysql数据库保姆级安装教程-mac(一站式服务,提供资源)
数据库·sql·mysql·navicat
丸辣,我代码炸了2 小时前
如何手搓序列化器(以java为例)
java·开发语言·kafka
ego.iblacat2 小时前
MySQL 全量、增量备份与恢复
数据库·mysql
画堂秋2 小时前
云原生-Mysql
运维·mysql·云原生
qq_2837200511 小时前
MySQL技巧(九): Binlog 完整格式解析(ROW 模式,默认)
mysql·binlog·数据恢复
Java面试题总结12 小时前
MySQL篇 索引失效
数据库·mysql
last demo13 小时前
mysql
运维·数据库·mysql·oracle
花间相见15 小时前
【MySQL面试题】—— MySQL面试高频问题汇总:从原理到实战,覆盖90%考点
数据库·mysql·面试
qq_3660862217 小时前
sql server OUTER APPLY使用
数据库·sql·mysql