• 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("++++++++++++++++++
    ")
        result.append("时间:").append(new Timestamp(timestamp -1 )).append("
    ")
        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("
    ")
        }
        result.append("++++++++++++++++++
    ")
    
        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。

  • 相关阅读:
    How Google TestsSoftware
    How Google TestsSoftware
    How Google TestsSoftware
    How Google TestsSoftware
    How Google Tests Software
    月薪3万的程序员都避开了哪些坑
    关于BUG率的计算和它的实际意义的思考
    fastJSON☞JSONParameters☞时区的修改☞时间最后有一个"Z"
    基础知识系列☞C#中数组Array、ArrayList和List三者的区别
    WCF--提示:异常消息为“传入消息的消息格式不应为“Raw”。此操作的消息格式应为 'Xml', 'Json'。
  • 原文地址:https://www.cnblogs.com/hecxx/p/11959831.html
Copyright © 2020-2023  润新知