1. Spark常规作业
1.1 任务背景
对每天的产生的日志进行曝光,点击等行为的PV和UV的计算,同时需要区分新老用户,然后将不同的类别的PV和UV以一列的形式展示
原始日志:
userId, itemId, userType, action
处理完后需要不同天数统计结果,每个天数集合都是以下形式, 并将所有天数集合的数据放入同一个表格中:
new_click_pv, new_click_uv, old_click_pv, old_click_uv
,
最后的结果
itemId, day1, day2, day3
1.2 解决方案
- 首先需要对每天的日志进行收集和统计,不过由于uv必须是统计时间范围内不重复的userId,因此,需要把指定时间范围的日志全部union出来之后才能进行统计, 时间通过filter函数来过滤,然后将不同时间段的数据union一下
- 获取数据后,按照itemId进行groupby,然后count(userId)计算pv值,但此时的count需要一些条件,比如action=‘click’,userType=‘new’
count(when(col("userType") === "new" && col("action") === "expose", col("deviceId"))).cast("int").as("new_expose_pv"),
以这种形式统计所有的行为,会增加列的数量 - 由于这一统计结果为某一个时间段的整体数据,需要做一个结构,统计为统一成为一个列,使用struct()将各列整合到一起
- 然后再加入一列时间
.withColumn("day_type", lit(date))
- 到这里,这个新的表格机会产生三列
"book_id", "stats_day", "day_type"
- 仍然无法满足需求,没有做到让日期作为列名,因此需要做一个转置的操作
.groupBy("book_id").pivot("day_type").agg(collect_set("stats_day")(0)).na.fill("")
pivot就是一个行列转换的方法.至此,解决
2. Spark Steaming作业
2.1 任务背景
通过实时统计Item在线上曝光次数,如果达到一个阈值,比如1000次,就更新推荐的召回队列,将没有曝光过得item进行曝光.
任务涉及的文件有:
- 一个包含全部item的文件, allItems.txt,里面有两列,itemid和category
- 一个实时的数据流
- 一个实时更新的线上item统计文件,包含两列,itemId和exposeTime
- 需要落盘两个文件,一个是持续的统计数据,一个是最新的召回队列(去除曝光次数大于阈值的item,加入新的item)
2.2 解决方案
-
这个流失作业的处理,需要每隔五分钟滑动一次,且滑动的步长是5,相当于一个滚动窗口,保证数据的不重复计算; 但是时间或许会随需求发生改变,需要将其作为一个参数传递,因此使用了case class
case class TaskParams(duration: Int = 5, slide: Int = 5, maxExposePv: Int = 200, maxItemCount: Int = 10) object TaskParams { def apply(data: String): TaskParams = { val json = new JSONObject(data) TaskParams( duration = json.optInt("窗口大小"), slide = json.optInt("步长"), maxExposePv = json.optInt("最大曝光阈值"), maxItemCount = json.optInt("每次曝光的最大item数量")) } }
-
由于streaming需要去读取静态的allItems.txt,但是偶尔也会新增或者修改一部分Item,不能仅仅读取到就可以,需要每次处理流式数据是重新加载最新的文件;
2.1 针对第一个问题,因为目前是调用的StreamingContext,如果使用这个上下文读的话,数据读取的格式是DStream的格式,另外就是无法做到实时能加载到最新的数据,因此调用的StreamingContext的上一层封装的SparkContext去读取数据,使用getOrCreate
方法或者如果是一个StreamingContext
的参数ssc的话,直接可以ssc.sparkContext.textFile
获取
2.2针对第二个问题,如何能保证每次读取的allItems.txt都是最新的呢,由于spark的惰性机制,只有在使用到的时候才会根据DAG图追溯计算,那么就可以将一个处理这个静态文件的action操作放到,流式数据处理的函数.foreachRDD
中,如allItems.collectAsMap
这样就能保证每次获取的数据都是最近更新的数据. -
将流式数据中的item做一个统计,统计曝光是一个持续的过程,从这个作业启动开始到解说应该是一直累加的过程,同时需要做到每隔一段时间做一次落盘数据,这时就需要一个函数
.updateStateByKey
,本案例中是每隔5分钟需要累加一下item的曝光次数,相当于一个词频统计,注意:在使用状态更新函数是,需要指定一个存放checkpoint的路径//对每个窗口中的状态进行累加更新 val updateFunc = (values: Seq[Int], state: Option[Int]) => { val currentCount = values.sum val previousCount = state.getOrElse(0) Some(currentCount + previousCount) } val updateFuncByKey = (iterator: Iterator[(String, Seq[Int], Option[Int])]) => { iterator.flatMap(t => updateFunc(t._2, t._3).map(s => (t._1, s))) } .updateStateByKey(updateFuncByKey, new HashPartitioner(ssc.sparkContext.defaultParallelism), rememberPartitioner = true, initialRDD = ssc.sparkContext.emptyRDD[(String, Int)])
-
对于需要落盘的两个数据,均写在
foreachRDD
中即可.repartition(1) .saveAsTextFile("your path")
-
整体解决思路:
5.1 创建case class传入参数;
5.2 调用StreamingContext的sparkContext获取静态数据allItems.txt(因为一个作业不允许有两个上下文的存在),将读取的文件整理成RDD形式的(itemId, category)以备用,记为allItems
5.3 获取流式数据stream, 在map中整理成自己需要的结构体
5.4 调用窗口函数.window(Minutes(taskParams.duration), Minutes(taskParams.slide))
5.5 调用状态更新函数.updateStateByKey(updateFuncByKey, new HashPartitioner(ssc.sparkContext.defaultParallelism), rememberPartitioner = true, initialRDD = ssc.sparkContext.emptyRDD[(String, Int)])
5.5 调用.foreachRDD()
将allItems进行action做操作allItemsId= allBooks.collectAsMap
转为Map,用于过滤出包含在allItems中的item.流里面其他id不关注,
5.6 生成召回队列newRecallData
, 将allItemsId
转为list之后进行map,如果在流中存在的itemId就记为已经更新的曝光次数,没有出现的(待出现的)记为0,用于生成新的召回队列
5.7 按照类别category进行groupby之后按照曝光倒排,获取召回队列数据
6.整个过程可以放心使用savedAsText方法,不会报文件已经存在的异常.