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。

posted @ 2019-11-29 11:00  HeCCXX  阅读(661)  评论(2编辑  收藏  举报