背景:最近工作中碰到一个需求,需要使用一个spark job进行离线数据同步,将每天所有车的心跳HB数据中的指标A进行统计,得到响应一个统计结果,然后入库,对于1个完全没有接触过spark的人来说,要在一周内完成这个job,可以说真是充满了挑战。
job的逻辑的核心算法,是这样:每一台车约有550条HB,以15条为宽度,以5条为间距,移动的去统计计算,即1-15条数据进行一次算法判断得到一个结果,6-20条进行一次算法判断得到一个结果,11-25条数据进行一次算法判断得到一个结果…直至第550条。
难点:看到这个算法描述,首先想到的是使用滑动窗口,来解决此问题。然后去对spark进行技术调研,发现spark在进行离线计算时,现在的主流是将数据读取成DataFrame
或者DataSet
结构去进行操纵,我这个需求中,选择的是使用DataFrame
。但是深入调研(google+baidu)发现,针对rdd/DataFrame/DataSet
,都没有现成的技术能够直接实现我算法的核心逻辑;倒是Spark Streaming
中的window
可以满足的需求,但是只能用于实时计算,不能用于离线计算。
只能在DataFrame中的唯一window:Time Window
上多动心思。
关于DataFrame
的Time Window
大家可以搜到很多相关的文章,概括起来就是,如果想使用这个窗口,你的数据中首先得要有一个类型为TimeStamp
的列,然后每条数据的这一列都是等距的,比如间隔是1s,1min或者1hour;如果满足这个条件,就可以设置一个。
我的数据虽然理论上是HB数据,数据都是带有数据发送时间,而且是都以1min为间距的等距数据,但是实际上车子在发送HB时会有数据丢失,或者因为网络延迟造成的一些误差,因此没法直接使用。
最后我想到了去给我的数据追加上一列,使得数据能够满足Time Window的使用要求。
我的原始数据大概是这个样子:
车辆编号(vid) | 心跳数据发送时间(send_time) | 发动机温度(temperature) |
---|---|---|
111 | 2020-05-24 00:00:00 | 80 |
111 | 2020-05-24 00:01:01 | 76 |
111 | 2020-05-24 00:03:06 | 78 |
111 | 2020-05-24 00:07:00 | 81 |
111 | 2020-05-24 00:10:00 | 77 |
111 | 2020-05-24 00:11:00 | 82 |
… | … | … |
222 | 2020-05-24 00:00:00 | 80 |
222 | 2020-05-24 00:01:01 | 76 |
222 | 2020-05-24 00:03:06 | 78 |
222 | 2020-05-24 00:07:00 | 81 |
222 | 2020-05-24 00:10:00 | 77 |
222 | 2020-05-24 00:11:00 | 82 |
… | … | … |
相关的实现代码:
//读取文件得到DataFrame,数据格式类似于上述数据
val dfData = spark.read.orc(filePath)
.select("vin", "send_time", "temperature")
//并且为了能使用time window,基于row_number()函数给每行添加一个类型为TimestampType的标记
val dfDataWithTag = dfData
.withColumn("TAG", row_number().over(Window.partitionBy("vin").orderBy(col("send_time"))).cast(TimestampType))
追加之后的数据变为
车辆编号(vid) | 心跳数据发送时间(send_time) | 发动机温度(temperature) | TAG |
---|---|---|---|
111 | 2020-05-24 00:00:00 | 80 | 1970-01-01 08:00:01.0 |
111 | 2020-05-24 00:01:01 | 76 | 1970-01-01 08:00:02.0 |
111 | 2020-05-24 00:03:06 | 78 | 1970-01-01 08:00:03.0 |
111 | 2020-05-24 00:07:00 | 81 | 1970-01-01 08:00:04.0 |
111 | 2020-05-24 00:10:00 | 77 | 1970-01-01 08:00:05.0 |
111 | 2020-05-24 00:11:00 | 82 | 1970-01-01 08:00:06.0 |
… | … | … | … |
222 | 2020-05-24 00:00:00 | 80 | 1970-01-01 08:00:01.0 |
222 | 2020-05-24 00:01:01 | 76 | 1970-01-01 08:00:02.0 |
222 | 2020-05-24 00:03:06 | 78 | 1970-01-01 08:00:03.0 |
222 | 2020-05-24 00:07:00 | 81 | 1970-01-01 08:00:04.0 |
222 | 2020-05-24 00:10:00 | 77 | 1970-01-01 08:00:05.0 |
222 | 2020-05-24 00:11:00 | 82 | 1970-01-01 08:00:06.0 |
… | … | … | … |
每条记录都追加上了一个间隔为1s的TAG字段
然后就可以对数据使用time window进行滑动统计了
val resDf = dfDataWithTag
.groupBy(col("vid"),functions.window(col("TAG"),"15seconds","5 seconds","1 seconds"))
.agg(udafFunction(col("temperature")).as("resData"))
.sort("vid","window.start")
.select("vid","window.start","window.end","resData")
此处的udafFunction
我使用的是自定义的聚合函数UDAF
,关于UDAF
的部分大家也可以自行查看相关内容,如果是比较简单的业务场景,也可以直接使用spark中现成的一些agg聚合函数。
以上就是Time Window实现Count Window的相关代码了。
在此次编码中,将遇到的其他的spark中block住我比较久的一些东西,再另写(水)一篇博客进行记录。