摘要
很多团队把 Flink CEP 只用在"是否命中"这一层,导致命中后上下文不足、调查链条断裂。本文基于一套真实工程方案,讲解如何用 MySQL 维护可运营规则、Kafka 提供实时事件流,再通过 PatternFactory 同时支持"标准序列匹配"和 collectallafter"命中后后缀收集"两种语义,并结合二次收敛策略减少重复输出。读完后你可以直接复用文中的核心代码片段,快速搭建可上线的规则检测链路。
关键词
MySQL 动态规则、Kafka、Flink CEP、PatternFactory、collectallafter、告警收敛、实时检测、攻击链研判
这篇文章面向"不拿完整项目代码也能看懂"的场景,直接给出可复制的关键代码片段,并用案例讲透 Flink CEP 两种匹配方式:
- 标准序列匹配(默认)
collectallafter后缀收集匹配(重点)
1. 业务背景与链路概览
目标是做一条实时安全检测链路:
- 从 Kafka 持续消费告警事件
- 从 MySQL 读取自定义规则
- 用 Flink CEP 进行模式匹配
- 命中后输出告警案例到 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);
}
4. Flink CEP 两种匹配方式(重点 + 案例)
下面是项目 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):
10:00:01 端口扫描10:00:10 异常登录10:00:20 权限提升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();
}
行为变化:
- skip 策略改为
skipPastLastEvent - 在关键节点前允许"吞噪声事件"
- 在末尾追加
__tail__,把后续事件尽量收集进 Case
事件案例
输入序列(同一 key):
10:00:01 异常登录10:00:05 心跳包10:00:08 DNS查询10:00:15 执行命令10:00:20 下载工具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 步:
- 先做标准模式规则,确认链路正确
- 再给重点规则加
collectallafter,提升上下文信息量 - 加入 burst 收敛,控制输出重复
- 用 MySQL 管理
pattern_json + window_time,形成可运营规则中心
6. 一句话结论
同样是 Flink CEP,标准序列匹配解决"准不准",collectallafter 解决"全不全";两者组合,再加收敛策略,才能同时满足检测、研判、归并三种真实业务诉求。
- 规则放在 MySQL,是为了让检测能力可运营;语义放在 PatternFactory,是为了让检测能力可进化。
- 标准 CEP 解决"发现问题",
collectallafter解决"讲清问题"。 - 实时检测不是只要命中率,更要给出足够上下文让研判和处置能闭环。