依赖
想要在代码中使用 Flink CEP,需要在项目的 pom 文件中添加相关依赖:
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
为了精简和避免依赖冲突,Flink 会保持尽量少的核心依赖。所以核心依赖中并不包括任何的连接器(conncetor)和库,这里的库就包括了 SQL、CEP 以及 ML 等等。所以如果想要在 Flink 集群中提交运行 CEP 作业,应该向 Flink SQL 那样将依赖的 jar 包放在/lib 目录下。
从这个角度来看,Flink CEP 和 Flink SQL 一样,都是最顶层的应用级 API。
一个简单实例
我们考虑一个具体的需求:检测用户行为,如果连续三次登录失败,就输出报警信息。很显然,这是一个复杂事件的检测处理,我们可以使用 Flink CEP 来实现。
我们首先定义数据的类型。这里的用户行为不再是之前的访问事件 Event 了,所以应该单独定义一个登录事件 POJO 类。具体实现如下:
public class LoginEvent {
public String userId; //用户ID
public String ipAddress; //IP地址
public String eventType; //状态
public Long timestamp; //时间戳
public LoginEvent(String userId, String ipAddress, String eventType, Long timestamp) {
this.userId = userId;
this.ipAddress = ipAddress;
this.eventType = eventType;
this.timestamp = timestamp;
}
public LoginEvent() {
}
@Override
public String toString() {
return "LoginEvent[" +
"userId='" + userId + '\'' +
", ipAddress='" + ipAddress + '\'' +
", eventType='" + eventType + '\'' +
", timestamp=" + timestamp +
']';
}
}
接下来就是业务逻辑的编写。Flink CEP 在代码中主要通过 Pattern API 来实现。之前我们已经介绍过,CEP 的主要处理流程分为三步,对应到 Pattern API 中就是:
(1)定义一个模式(Pattern);
(2)将Pattern应用到DataStream上,检测满足规则的复杂事件,得到一个PatternStream;
(3)对 PatternStream 进行转换处理,将检测到的复杂事件提取出来,包装成报警信息输出。
具体代码实现如下:
public class LoginFailDetectExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
// 获取登录事件流,并提取时间戳、生成水位线
SingleOutputStreamOperator<LoginEvent> stream = env.fromElements(
new LoginEvent("user_1", "192.168.0.1", "fail", 2000L),
new LoginEvent("user_1", "192.168.0.2", "fail", 3000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 4000L),
new LoginEvent("user_1", "171.56.23.10", "fail", 5000L),
new LoginEvent("user_2", "192.168.1.29", "success", 6000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 7000L),
new LoginEvent("user_2", "192.168.1.29", "fail", 8000L)
).assignTimestampsAndWatermarks(WatermarkStrategy.<LoginEvent>forBoundedOutOfOrderness(Duration.ZERO)
.withTimestampAssigner(new SerializableTimestampAssigner<LoginEvent>() {
@Override
public long extractTimestamp(LoginEvent element, long recordTimestamp) {
return element.timestamp;
}
}));
//定义模式,连续三次登陆失败
Pattern<LoginEvent, LoginEvent> pattern = Pattern.<LoginEvent>begin("first")
.where(new SimpleCondition<LoginEvent>() {
@Override
public boolean filter(LoginEvent value) throws Exception {
return value.eventType.equals("fail");
}
})
.next("second") //紧跟着第二次失败事件
.where(new SimpleCondition<LoginEvent>() {
@Override
public boolean filter(LoginEvent value) throws Exception {
return value.eventType.equals("fail");
}
})
.next("third") //紧跟着第三次失败事件
.where(new SimpleCondition<LoginEvent>() {
@Override
public boolean filter(LoginEvent value) throws Exception {
return value.eventType.equals("fail");
}
});
//将模式应用到数据流上,检测复杂事件
PatternStream<LoginEvent> patternStream = CEP.pattern(stream.keyBy(event -> event.userId), pattern);
//将检测到的复杂事件提取出来,进行处理得到报警信息输出
SingleOutputStreamOperator<String> warningStream = patternStream.select(new PatternSelectFunction<LoginEvent, String>() {
@Override
public String select(Map<String, List<LoginEvent>> map) throws Exception {
LoginEvent firstStream = map.get("first").get(0);
LoginEvent secondStream = map.get("second").get(0);
LoginEvent thirdStream = map.get("third").get(0);
return firstStream.userId + "连续三次登陆失败,登录时间:" +
firstStream.timestamp + "," +
secondStream.timestamp + "," +
thirdStream.timestamp;
}
});
//输出
warningStream.print();
//执行
env.execute();
}
}
在上面的程序中,模式中的每个简单事件,会用一个.where()方法来指定一个约束条件,指明每个事件的特征,这里就是 eventType 为“fail”。
而模式里表示事件之间的关系时,使用了 .next() 方法。next 是“下一个”的意思,表示紧挨着、中间不能有其他事件(比如登录成功),这是一个严格近邻关系。第一个事件用.begin()方法表示开始。所有这些“连接词”都可以有一个字符串作为参数,这个字符串就可以认为是当前简单事件的名称。所以我们如果检测到一组匹配的复杂事件,里面就会有连续的三个登录失败事件,它们的名称分别叫作“first”“second”和“third”。
在 第 三 步 处 理 复 杂 事 件 时 , 调 用 了 PatternStream 的 .select() 方 法 , 传 入 一 个PatternSelectFunction 对检测到的复杂事件进行处理。而检测到的复杂事件,会放在一个 Map中;PatternSelectFunction 内.select()方法有一个类型为 Map<String, List>的参数map,里面就保存了检测到的匹配事件。这里的 key 是一个字符串,对应着事件的名称,而 value是 LoginEvent 的一个列表,匹配到的登录失败事件就保存在这个列表里。最终我们提取 userId和三次登录的时间戳,包装成字符串输出一个报警信息。