Flink實時項目例程
一、項目模塊
完整例程github地址:https://github.com/HeCCXX/UserBehaviorAnalysis.git
- HotItemAnalysis 模塊 : 實時熱門商品統計,輸出Top N 的點擊量商品,利用滑動窗口,eventTime(包括本地文件數據源和kafka數據源)
- NetWorkTrafficAnalysis 模塊,實時流量統計,和上面模塊類似,利用滑動窗口,eventTime(本地文件數據源)
- LoginFailedAlarm 模塊,惡意登錄監控,原理同上兩個模塊,當檢測到用戶在指定時間內登錄失敗次數大於等於一個值,便警告 (使用Map模擬少量數據)
- OrderTimeOutAnalysis 模塊, 下單超時檢測,利用CEP(Complex Event Processing,複雜事件處理),當用戶下單後,超過15分鐘未支付則警告 (使用Map模擬少量數據)
二、數據源解析
下圖爲用戶的操作日誌,按列分別代表userId itemId categoryId behavior timestamp
,分別是用戶ID,商品ID,商品所屬類別ID,用戶行爲類型(包括瀏覽pv ,購買 buy,購物車 cart,收藏 fav),行爲發生的時間戳。
另外,流量模塊使用的數據爲web 訪問的log日誌。
三、項目搭建過程
1、創建maven工程,導入相應依賴包,具體pom 文件內容如下。
<properties>
<flink.version>1.7.2</flink.version>
<scala.binary.version>2.11</scala.binary.version>
<kafka.version>2.2.0</kafka.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_${scala.binary.version}</artifactId>
<version>${kafka.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
</dependencies>
2、子模塊創建
HotItemAnalysis 模塊 : 實時熱門商品統計的需求,每隔5分鐘輸出最近一個小時內點擊量最多的前N個商品。按照步驟則如下:
• 抽取出業務時間戳,告訴Flink框架基於業務時間做窗口
• 過濾出點擊行爲數據
• 按一小時的窗口大小,每5分鐘統計一次,做滑動窗口聚合(Sliding Window)
• 按每個窗口聚合,輸出每個窗口中點擊量前N名的商品
部分代碼流程:
(1)將讀取的數據轉換爲樣例類格式流式需要注意添加隱式轉換,否則會報錯,無法完成隱式轉換。因爲該數據源的時間戳是整理過的,是單調遞增的,所以使用assignAscendingTimestamps
指定時間戳和watermark,將每條數據的業務時間當做watermark。
(2)filter過濾行爲爲pv的數據,即用戶瀏覽點擊事件。
(3)根據商品ID分組,並設置滑動窗口爲1小時的窗口,每5分鐘滑動一次。然後使用aggregate做增量的聚合操作,在該方法中,CountAgg提前聚合數據,減少state的存儲壓力,apply方法會將窗口中的數據都存儲下來,最後一起計算,所以aggregate方法比較高效。CountAgg功能是累加窗口中數據條數,遇到一條數據就加一。WindowResultFunction是將每個窗口聚合後的結果帶上其他信息進行輸出,將<主鍵商品ID,窗口,點擊量>封裝成結果輸出樣例類進行輸出。
val dstream: DataStream[String] = env.readTextFile("E:\\JavaProject\\UserBehaviorAnalysis\\HotItemAnalysis\\src\\main\\resources\\UserBehavior.csv")
//隱式轉換
import org.apache.flink.api.scala._
//轉換爲樣例類格式流
val userDstream: DataStream[UserBehavior] = dstream.map(line => {
val split: Array[String] = line.split(",")
UserBehavior(split(0).toLong, split(1).toLong, split(2).toInt, split(3), split(4).toInt)
})
//指定時間戳和watermark
val timestampsDstream: DataStream[UserBehavior] = userDstream.assignAscendingTimestamps(_.timestamp * 1000)
//過濾用戶點擊行爲的數據
val clickDstream: DataStream[UserBehavior] = timestampsDstream.filter(_.behavior == "pv")
//根據商品ID分組,並設置窗口一個小時的窗口,滑動時間爲5分鐘
clickDstream.keyBy("itemId")
.timeWindow(Time.minutes(60),Time.minutes(5))
/**
* preAggregator: AggregateFunction[T, ACC, V],
* windowFunction: (K, W, Iterable[V], Collector[R]) => Unit
* 聚合操作,AggregateFunction 提前聚合掉數據,減少state的存儲壓力
* windowFunction 會將窗口中的數據都存儲下來,最後一起計算
*/
.aggregate(new CountAgg(),new WindowResultFunction())
.keyBy("windowEnd")
.process(new TopNHotItems(3))
.print()
//累加器
class CountAgg extends AggregateFunction[UserBehavior,Long,Long]{
override def createAccumulator(): Long = 0L
override def add(in: UserBehavior, acc: Long): Long = acc + 1
override def getResult(acc: Long): Long = acc
override def merge(acc: Long, acc1: Long): Long = acc + acc1
}
//WindowResultFunction 將聚合後的結果輸出
class WindowResultFunction extends WindowFunction[Long,ItemViewCount,Tuple,TimeWindow]{
override def apply(key: Tuple, window: TimeWindow, input: Iterable[Long], out: Collector[ItemViewCount]): Unit = {
val itemId: Long = key.asInstanceOf[Tuple1[Long]].f0
val count: Long = input.iterator.next()
out.collect(ItemViewCount(itemId,window.getEnd,count))
}
}
(4)ProcessFunction是Flink提供的一個low-level API,用於實現更高級的功能。他主要提供了定時器timer的功能(支持Eventtime或ProcessingTime)。本例程中將利用timer來判斷何時收齊了某個window下所有商品的點擊量數據。因爲watermark的進程是全局的,在processElement方法中,每當收到一條數據,就註冊一個windowEnd + 1 的定時器(Flink會自動忽略同一時間的重複註冊)。windowEnd + 1的定時器被觸發時,意味着收到了windowEnd + 1 的watermark,即收齊了該windowEnd下的所有商品窗口統計值。我們在onTimer方法中處理將收集的數據進行處理,排序,選出前N個。
class TopNHotItems(topSize : Int) extends KeyedProcessFunction[Tuple,ItemViewCount,String]{
private var itemState : ListState[ItemViewCount] = _
override def open(parameters: Configuration): Unit = {super.open(parameters)
//狀態變量的名字和狀態變量的類型
val itemsStateDesc = new ListStateDescriptor[ItemViewCount]("itemState",classOf[ItemViewCount])
//定義狀態變量
itemState = getRuntimeContext.getListState(itemsStateDesc)
}
override def processElement(i: ItemViewCount, context: KeyedProcessFunction[Tuple, ItemViewCount, String]#Context, collector: Collector[String]): Unit = {
//每條數據都保存到狀態中
itemState.add(i)
//註冊windowEnd + 1 的EventTime的 Timer ,當觸發時,說明收齊了屬於windowEnd窗口的所有數據
context.timerService.registerEventTimeTimer(i.windowEnd + 1)
}
override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Tuple, ItemViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
//獲取收到的商品點擊量
val allItems: ListBuffer[ItemViewCount] = ListBuffer()
import scala.collection.JavaConversions._
for (item <- itemState.get){
allItems += item
}
//提前清除狀態中的數據,釋放空間
itemState.clear()
//按照點擊量從大到小排序
val sortedItems: ListBuffer[ItemViewCount] = allItems.sortBy(_.count)(Ordering.Long.reverse).take(topSize)
val result = new StringBuilder
result.append("++++++++++++++++++\n")
result.append("時間:").append(new Timestamp(timestamp -1 )).append("\n")
for (i <- sortedItems.indices){
val item: ItemViewCount = sortedItems(i)
result.append("No").append(i+1).append(":")
.append(" 商品ID : ").append(item.itemId)
.append(" 點擊量 : ").append(item.count).append("\n")
}
result.append("++++++++++++++++++\n")
Thread.sleep(1000)
out.collect(result.toString())
}
}
四、輸出結果
熱門商品的輸出結果如下圖所示。
五、更換kafka源
爲了貼近實際生產環境,我們的數據流可以從kafka中獲取。主要和上述不同的代碼如下。當然我們也可以在本地寫KafkaUtils工具類將本地的數據發送到kafka集羣,再添加數據源將kafka數據進行消費讀取。
//隱式轉換
import org.apache.flink.api.scala._
val kafkasink: FlinkKafkaConsumer[String] = new KafkaUtil().getConsumer("HotItem")
val dstream: DataStream[String] = environment.addSource(kafkasink)
六、實時流量統計模塊
代碼和熱門商品統計的類似,主要區別在於web 日誌的eventtime沒有整理過,所以是無序的。所以使用的是以下代碼來爲數據流的元素分配時間戳,並定期創建watermark指示時間事件進度。其他具體代碼可以查看項目內。
.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[NetWorkLog](Time.milliseconds(1000)) {
override def extractTimestamp(t: NetWorkLog): Long = {
t.eventTime
}
})
運行截圖如下:
七、惡意登錄監控模塊
惡意登錄監控模塊,可以利用狀態編程和CEP編程實現。如果是利用CEP的話,需要引入CEP相關包,pom文件內容如下。
- 利用狀態編程實現思路,和之前熱門商品統計類似,按用戶ID分流,然後遇到登錄失敗的事件時將其保存在ListState中,然後設置一個定時器,2秒內觸發,定時器觸發時檢查狀態中的登錄失敗事件個數,如果大於等於2,則輸出報警信息。
- CEP編程思路,在狀態編程實現中,是固定的2秒內判斷是否又多次登錄失敗,而不是一次登錄失敗後,再一次登錄失敗,相當於判斷任意緊鄰的時間是否符合某種模式。我們可以使用CEP來完成。具體代碼如下所示。
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-cep-scala_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
def main(args: Array[String]): Unit = {
val environment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
environment.setParallelism(1)
import org.apache.flink.api.scala._
val dstream: DataStream[LoginEvent] = environment.fromCollection(List(
LoginEvent(1, "192.168.0.1", "400", 1558430842),
LoginEvent(1, "192.168.0.2", "400", 1558430843),
LoginEvent(1, "192.168.0.3", "400", 1558430844),
LoginEvent(2, "192.168.0.3", "400", 1558430845),
LoginEvent(2, "192.168.10.10", "200", 1558430845)
))
val keyStream: KeyedStream[LoginEvent, Long] = dstream.assignAscendingTimestamps(_.timestamp * 1000)
.keyBy(_.userId)
//定義匹配模式
val loginPattern: Pattern[LoginEvent, LoginEvent] = Pattern.begin[LoginEvent]("begin").where(_.loginFlag == "400")
.next("next").where(_.loginFlag == "400")
.within(Time.seconds(2))
//在數據流中匹配出定義好的模式
val patternStream: PatternStream[LoginEvent] = CEP.pattern(keyStream,loginPattern)
import scala.collection.Map
//select方法傳入pattern select function,當檢測到定義好的模式就會調用
patternStream.select(
(pattern : Map[String,Iterable[LoginEvent]]) =>
{
val event: LoginEvent = pattern.getOrElse("begin",null).iterator.next()
(event.userId,event.ip,event.loginFlag)
}
)
.print()
environment.execute("Login Alarm With CEP")
}
在上述代碼中,獲取輸入流後將流根據用戶ID分流,接下來定義匹配模式,並使用CEP,在數據流中匹配出定義好的模式,需要獲取匹配到的數據時,只需要調用select 方法,將匹配到的數據按要求輸出即可。
輸出結果截圖如下:
訂單支付實時監控的實現與惡意登錄監控模塊類似,具體代碼可查看具體模塊代碼。
八、總結
在利用窗口實現實時範圍統計的場景中,需要考慮好數據的時間戳和watermark。