Flink實時項目例程

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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章