FlinkCEP是在Flink上層實現的複雜事件處理庫。 它可以讓你在無限事件流中檢測出特定的事件模型,有機會掌握數據中重要的那部分。
官網文檔: https://ci.apache.org/projects/flink/flink-docs-stable/zh/dev/libs/cep.html
這裏給個demo,對比下不用cep和用cep的區別,
實現目標: 從目標csv中讀取模擬登錄的數據,實時檢測,如果5秒鐘之內連續登錄的次數超過2次,則馬上告警
按照之前的正常操作(非CEP實現)
實現步驟:
1、準備環境和數據源加載到內存
2、進行數據切割,轉成需要的格式(樣例類)
3、指定時間窗口watermark及事件時間取哪個字段
4、按每個用戶id進行分組,統計每個用戶id的登錄行爲(畢竟不能放一起統計吧)
5、實現具體的處理邏輯ProcessFunction
6、輸出檢測數據
準備的模擬數據 userLogin.csv:
1234,10.0.1.1,fail,1611373940
1235,10.0.1.2,fail,1611373941
1234,10.0.1.3,fail,1611373942
1234,10.0.1.3,success,1611373943
1234,10.0.1.3,fail,1611373943
1234,10.0.1.3,fail,1611373944
1236,10.0.1.4,fail,1611373945
1234,10.0.1.4,fail,1611373957
1234,10.0.1.5,fail,1611373958
1234,10.0.11.55,fail,1611373959
1236,2.2.2.2,fail,1611373960
/*
*
* @author mafei
* @date 2021/1/24
*/
package com.mafei
import org.apache.flink.api.common.state.{ListState, ListStateDescriptor, ValueState, ValueStateDescriptor}
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.scala._
import org.apache.flink.util.Collector
import scala.collection.mutable.ListBuffer
/**
* 定義一個輸入數據的樣例類
*
* @param userId 用戶id
* @param ip 客戶端的ip
* @param loginState 登錄狀態,目前只有success/fail,後期可以做擴展,所以定義爲string
* @param ts 事件的時間戳,單位秒
*/
case class userLogin(userId: Long,ip: String,loginState: String,ts: Long)
/**
* 定義一個輸出的樣例類
* @param userId 用戶id
* @param startTs 開始登錄時間
* @param endTs 觸發事件的最後一次時間
* @param loginCount 時間段內總共登錄的次數
*/
case class userLoginWarning(userId: Long, startTs: Long, endTs:Long, loginCount: Long)
object maliceLoginDetect {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //指定事件時間爲窗口和watermark的時間
env.setParallelism(1)
//從文件中讀取數據
val resource = getClass.getResource("/userLogin.csv")
val inputStream = env.readTextFile(resource.getPath)
// 轉換成樣例類,並提取時間戳watermark
val loginEventStream = inputStream
.map(d => {
val arr = d.split(",")
// 分別對應 userId ip 登錄狀態 時間戳
userLogin(arr(0).toLong, arr(1), arr(2), arr(3).toLong)
})
.assignAscendingTimestamps(_.ts * 1000L) //把秒轉爲毫秒
val loginWarningStream = loginEventStream
.keyBy(_.userId)
.process(new loginMaliceDetect(2))
loginWarningStream.print()
env.execute()
}
}
class loginMaliceDetect(warningCount: Long) extends KeyedProcessFunction[Long,userLogin,userLoginWarning]{
//定義狀態,保存當前所有的登錄事件爲list,方便後邊做數據統計
lazy val loginFailListState: ListState[userLogin] = getRuntimeContext.getListState(new ListStateDescriptor[userLogin]("loginFail-list", classOf[userLogin]))
//定義定時器的時間戳狀態,否則沒法刪定時器
lazy val timerTsState: ValueState[Long] = getRuntimeContext.getState(new ValueStateDescriptor[Long]("timerState", classOf[Long]))
override def processElement(i: userLogin, context: KeyedProcessFunction[Long, userLogin, userLoginWarning]#Context, collector: Collector[userLoginWarning]): Unit = {
//判斷,如果當前事件是登錄失敗事件,那再繼續操作
if(i.loginState == "fail"){
loginFailListState.add(i)
//如果沒有註冊定時器,那就註冊一個定時器,5秒之後觸發
if(timerTsState.value()== 0){
val timerTs = i.ts * 1000L + 5000L
context.timerService().registerEventTimeTimer(timerTs)
timerTsState.update(timerTs)
}
}
else if(i.loginState == "success"){
context.timerService().deleteEventTimeTimer(timerTsState.value())
timerTsState.clear()
loginFailListState.clear()
}
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Long, userLogin, userLoginWarning]#OnTimerContext, out: Collector[userLoginWarning]): Unit = {
// 判斷下如果登錄失敗次數超過了設置的閾值,則告警
val loginFailList: ListBuffer[userLogin] = new ListBuffer[userLogin]
val iterable = loginFailListState.get().iterator()
while (iterable.hasNext){
loginFailList += iterable.next()
}
if (loginFailList.size > warningCount){
out.collect(userLoginWarning(userId = ctx.getCurrentKey, startTs = loginFailList.head.ts, endTs = loginFailList.last.ts, loginCount = loginFailList.size))
}
loginFailList.clear()
loginFailListState.clear()
timerTsState.clear()
}
}
代碼結構及運行效果
使用flink CEP實現
上面代碼栗子是可以實現基本的登錄異常檢測了,但是如果碰到數據亂序等情況,
有3個失敗事件在時間範圍內,但是有個亂序的數據插在中間,這時候按照邏輯中間就會情況重新計算。。這時候就需要用到flink提供的cep(複雜事件檢測)的功能了
在pom.xml中增加cep的依賴
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<flink.version>1.10.1</flink.version>
<scala.binary.version>2.12</scala.binary.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep-scala_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>
/*
*
* @author mafei
* @date 2021/1/24
*/
package com.mafei
import org.apache.flink.cep.PatternSelectFunction
import org.apache.flink.cep.scala.CEP
import org.apache.flink.cep.scala.pattern.Pattern
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
import java.util
object maliceLoginDetectWithCep {
def main(args: Array[String]): Unit = {
val env = StreamExecutionEnvironment.getExecutionEnvironment
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //指定事件時間爲窗口和watermark的時間
env.setParallelism(1)
//從文件中讀取數據
val resource = getClass.getResource("/userLogin.csv")
val inputStream = env.readTextFile(resource.getPath)
// 轉換成樣例類,並提取時間戳watermark
val loginEventStream = inputStream
.map(d => {
val arr = d.split(",")
// 分別對應 userId ip 登錄狀態 時間戳
userLogin(arr(0).toLong, arr(1), arr(2), arr(3).toLong)
})
.assignAscendingTimestamps(_.ts * 1000L) //把秒轉爲毫秒
// 1、先定義匹配的模式,需求爲一個登錄失敗事件後,緊接着出現另一個失敗事件
val loginFailPattern = Pattern
.begin[userLogin]("firstFail")
.where(_.loginState == "fail")
.next("secondFail")
.where(_.loginState == "fail")
.within(Time.seconds(5))
//2、將匹配的規則應用在數據流中,得到一個PatternStream
val patternStream = CEP.pattern(loginEventStream.keyBy(_.userId), loginFailPattern)
// 3、匹配中符合模式要求的數據流,需要調用select
val loginFailWarningStream = patternStream.select(new LoginFailEventMatch())
loginFailWarningStream.print()
env.execute("login fail detect with cep")
}
}
class LoginFailEventMatch() extends PatternSelectFunction[userLogin,userLoginWarning]{
override def select(map: util.Map[String, util.List[userLogin]]): userLoginWarning = {
//前邊定義的所有pattern,都在Map裏頭,因爲map的value裏面只定義了一個事件,所以只會有一條,取第一個就可以,如果定義了多個,需要按實際情況來
val firstFailEvent = map.get("firstFail").get(0)
val secondFailEvent = map.get("secondFail").iterator().next()
userLoginWarning(firstFailEvent.userId,firstFailEvent.ts,secondFailEvent.ts,2)
}
}