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。