全生命周期大数据处理系列

全生命周期大数据处理系列

任何一件复杂的事物,简化它的方法就是分而治之,只是这个法,万变不离其宗,可能会因人因事而大同小异而已.我在车联网大数据的处理实践中不断沉淀,在公司产品的迭代升级中逐步升华,深有感触略有所得,分享出来抛砖引玉,希望能引起共鸣,共同进步.

我把数据的生命周期分为如下几个阶段,而这么划分的标准是什么呢?我认为是"价值",数据的不断处理,是价值的不断提炼,其目的是为了获取其潜在的价值,这个价值的体现可能是引导了决策走向,展现了数据全貌,给出了时间趋势,挖掘出了不易发现的规律,预测了可能发生的事件等等.任何计算框架,无论什么工具产品,抑或是数据处理的策略和方法论,都是为了这个目标而产生的.

这个系列的讲解也是为了达成这一目标,怎么从最初产生的大数据,应用一系列的方法和工具,最后萃取出有价值的高纯度的小数据,我们可以称这个过程为数据掘金,也可以说是数据挖掘.

第一阶段: 数据采集

数据采集是由"米"到"炊"的最早阶段,这个阶段一般来说技术难度不大,但是有一定的规划难度.

通常也称这个阶段为数据埋点,"埋点"是数据采集阶段常用的术语,"埋"有设置和隐藏的含义.

以新能源电动汽车为例,司机在车辆使用甚至是停车的过程当中,有大量的数据产生,行驶过程中电池在放电,就会有电压,电流,电池单体温度,电阻,故障信号,车速,加速度等等,充电过程中电池在充电,就会有充电桩位置经纬度,充电电流,时长,室外温度,是否过充信号等等,而且这些信号在以一定的频率产生,短则毫秒级,长则秒级.这些信号数量数以千级万级,很多信号还是二维数组甚至多维数组,同时采集频率也很高,因为加速度的计算故障的分析尤其是碰撞事故发生时的诊断,对数据密度有较高的要求.这样的数据事实要求我们,数据埋点要规划好:

我们要采集哪些信号? 过多会产生无效传输和存储,过少不能满足分析需要.

采集频率应该多大? 这个采集频率是指车端的数据发送到服务器端的频率,而不是车端的采集频率,在车辆端采集频率可以很高,信号基本是全部采集,但是往往只存储一段时间,过后即会被清除.把这些数据发送到服务器端的频率,除了数据分析的最低要求外,还要考虑到存储和传输成本,所以往往存在两个采集频率,比如平时以1秒的频率传输,在有信号故障发生时则以50毫秒的频率传输.

采用什么协议? 协议即约定好的数据格式和标准,以便在发送端和接收端能做出一致的解释.在新能源领域,有国家规定的标准即国标,它定义了必须采集和传输的数据信号及频率,但其定义往往比较宽泛和基础,不同的车企还有各自的数据分析需求,便产生了众多的企业标准即企标.定义这个协议关键是定义数据结构,比如从哪个字节开始是温度字段,是单值还是数组,数组的每个温度值,应该采用几个字节,值域范围是多大,采用什么字节顺序,物理单位是什么,信号字段往往只是一个逻辑值怎么才能节约存储空间等等.

第一目标: 数据采集

数据采集
车辆端采集的数据经网络传输到服务器端,一般以分布式的消息队列的存储形式接收比如Kafka,用它的主要好处是可以弹性扩展,即可以随时增减存储节点资源以应对变化的采集数据体积和速度,不至于数据堆积也不至于存储浪费,但是Kafka的数据往往不能直接用于计算,虽然它本身也是支持流数据计算的,通常的做法是把它当作缓冲的存储,在我们的实际应用中,它的数据有3个应用方向,一方面用于实时的故障诊断计算,另一方面把轨迹及信号数据转储于HBase用于数据回溯,还有一方面把数据存为离线数据用于数据仓库指标体系建设.

接收到的数据是二进制非结构化的,是不能用于常规的数据分析的,这个时候就要涉及到数据解析了,底层的数据解析是字节级别的操作,面对那么多的信号,各种数据格式,让很多数据开发人员望而却步,调试难易出错,而且解析代码在不同企标之间往往不能移植,即一次性代码,这大大违背了程序的可重用的本质.所以相应的工具产品也就应运而生了,这就是Mouth.你可以通过文档来深入了解它.

第二目标: 数据解析

怎么用 Mouth 来简单地把二进制数据解析成结构化数据呢?可以说一个XML文件足矣,你从如下的文件结构中一眼即可看出端倪,它用简单的几个指令比如string_item,byte_item描述了二进制数据中的结构,有了这个描述文件,Mouth就知道该怎么来解析二进制数据了.

    <!-- file: parser_vehicle.xml -->
<parser xmlns="https://www.rocy-data.com/parser_ex">
    <string_item name="tsp_start" byte_count="2" fixed_value="##"/><!--first read string of length=2,and its name=tsp_start -->
    <byte_item name="tsp_command"><!-- read a byte value to `tsp_command` -->
        <value_map value="1" name="register"/> <!-- for value 1 of `tsp_command`,convert it to `register` -->
        <value_map value="2" name="real info up"/>
    </byte_item>
    <byte_item name="tsp_ack">
        <value_map value="1" name="success"/>
        <value_map value="2" name="error"/>
    </byte_item>
    <string_item name="vin" byte_count="17"/>
    <dword_item name="flow_number"/><!-- read a long number and name it `flow_number` -->
    <byte_item name="tsp_encrypt" is_temp="true"/>

    <dword_item name="data.tsp_got_time"/><!-- read a long number and name it in dot struct, -->
    <byte_item name="data.info_type"/><!-- make same group with above data because they have same prefix -->
    <byte_item name="data.battery_number"/><!-- make same group with above data because they have same prefix -->

    <switch_item switch="data.info_type"> <!-- generate different data structure based on the value of `data.info_type`,`switch` is expression returning boolean that con contains expression such as `data.info_type + 2 > data.battery_number` -->
        <case value="2">
            <word_item name="dbt_probe_count"/>
            <byte_item name="db_package_count"/>
            <byte_item name="dbt_temperature_array_size"/>
            <set_variable name="index" value="0"/> <!-- set variable used to control loop operation -->
            <while_item condition="index &lt; dbt_temperature_array_size">
                <byte_item name="dbt_temperature_array[index].db_package_high_temp"/> <!--  the `[]` symbol used to construct array -->
                <byte_item name="dbt_temperature_array[index].db_package_low_temp"/>
                <byte_item name="dbt_temperature_array[index].db_package_temperature_probe_count"/>
                <array type="byte" length="dbt_probe_count" name="dbt_temperature_array[index].db_probe_temperatures"/> <!--for basic array type,its type and length is required-->
                <set_variable name="index" value="index + 1"/><!-- set variable used to control loop operation -->
            </while_item>
        </case>
    </switch_item>
</parser>

细心的读者可能已经发现了,它还有while_item,switch_item,set_variable这些指令来帮助我们描述更为复杂的灵活的数据结构,而且不管是怎样的企标数据,咱们用的都是这些指令,这便达到了可重用可移植的目的。

只需要一行代码就可以根据上述XML解析文件将接收到的字节数组转换为JSON数据,在这个步骤把数据结构化之后,就可以执行其他业务逻辑了。

    val bytes = ...// get bytes
    val json = ParserTemplate.parseAsJson(bytes.map(_.toUnsigned), "parser_vehicle.xml") // above bytes and file

输出的JSON,大概是如下示例这个样子的,其实不只如此,即便更为复杂的动态结构,也是能够处理的,除了switch_item+case,if_item也可以根据其他信号的值构成的评估表达式来决定如何解析,这使得数据结构可以是动态的,即结构并不固定,这个能力在其他类似的产品中也是很难实现的:

{
   
  "flow_number": 123,
  "data": {
   
    "info_type": 2,
    "tsp_got_time": 123456789,
    "battery_number": 2
  },
  "tsp_start": "##",
  "tsp_ack": "success",
  "dbt_temperature_array": [
    {
   
      "db_package_high_temp": 38,
      "db_probe_temperatures": [ 31.0, 32.0, 33.0, 38.0, 31.0, 32.0, 33.0, 38.0 ],
      "db_package_low_temp": 31,
      "db_package_temperature_probe_count": 8
    },
    {
   
      "db_package_low_temp": 31,
      "db_probe_temperatures": [ 31.0, 32.0, 33.0, 38.0, 31.0, 32.0, 33.0, 37.0 ],
      "db_package_temperature_probe_count": 8,
      "db_package_high_temp": 37
    }
  ],
  "dbt_temperature_array_size": 2,
  "tsp_command": "real info up",
  "dbt_probe_count": 8,
  "vin": "ROCY0123456789123",
  "db_package_count": 2
}

第三目标: 数据回溯

数据回溯的常见需求是查询某个ID(车联网领域即是VIN)在某个时间段的数据,数据查询往往要求实时响应,HBase正好能满足这个需求,其RowKey可以这样设计:

  1. VIN+数据的时间. 这样在查询时指定VIN和时间范围即可快速查询.
  2. VIN存储为倒序形式. 其目的是让各车辆数据在各存储节点上分散存储,为什么这样就能分散存储呢? 大致有这样的公式: 数据应存储的节点位置=RowKey的Hash值%节点数,而VIN值在同一车企往往有相同的前缀,致使有相同Hash值的概率加大,从而导致数据存储倾斜.把VIN存储为倒序形式即可使数据分散存储避免倾斜.

那么具体的数据怎么存储呢?技术难度不大,但是通常的做法很麻烦,麻烦在于数据信号的数量是庞大的,数以千计,而且数据类型各种各样,存储的代码如果对信号逐一处理是枯燥无味的,Pulse把这个过程简化了:

  1. 定义TSourceParse的一个实现,用于把Kafka的数据转化为JSON,如果你已经完成了第二目标: 数据解析这一步骤,那么这一步就可以省略了,这一步对JSON的要求只是:具有vintsp_got_time这两个字段.
  2. 定义Family. 通过sink_signal_name2categories指定哪些类似的信号存储到某一个Family
  3. 定义数据从哪里来存储到哪里,即Kafka和HBase的配置.

如此而已,再不需要其他了,数据已经具备,如果需要图表展示,前端页面所需要做的是:筛选车辆,指定时间范围,筛选关心的信号量,展示趋势图或是对比图即可.

第二阶段: 质量监控

前一阶段使数据结构化,可理解,可分析,可计算,而数据质量是一切计算的基础和保障,数据质量可从整体上反映了数据的质量情况,这一过程应该尽早完成,及时避免错误数据的向下传播.在这个过程中,我们希望得到的答案或者说目标是:

第一目标 缺失情况

  1. 有多少监控对象,比如有多少车辆?
  2. 每个目标有多少记录,打点频率怎样?
  3. 有多少打点字段,每个字段空值数量及比例?

第二目标 数值错误

  1. 各数值字段值域是否合理?
  2. 数值分布是否符合期望?
  3. 数值型数组的值域及分布是否在合理区间?

第三目标 频度分布

  1. 单维度频率分布,比如车型有值model1,model2,model3,那么每个车型有多少车辆?
  2. 多维度频率分布,在单维度频率分布基础之上,比如每个省份每个年份的每个车型有多少车辆?

第四目标 质量告警

  1. 质量报告. 当数据质量问题发生时,能够以质量报告的形式发送给数据相关负责人,以便采取措施;
  2. 触发事件. 比如能够触发某个脚本的执行,这个脚本可以是任何相关任务,比如字段1的统计值异常,那么就把和字段1相关的模型任务停掉.

仍然是数据字段很庞大的原因,使得我们按照常规的方法做这些质量检查根本不可行.可以把这一过程简化的根据是:数据类型是有限的,相同类型的数据其检查方法是类似的,比如数值型做常规数理统计,枚举型做频度统计等等,Health正是这样做的.

它的输出是类似这样的结果:

{
   
  "row_count":4,
  "null_stat":[
    {
    "col1":1, "col2":1 }
  ],
  "number_stat":[
    {
    "field":"col1", "agg":"min", "agg_value":1.0 },
    {
    "field":"col1", "agg":"max", "agg_value":3.0 },
    {
    "field":"col1", "agg":"mean", "agg_value":2.0 },
    {
    "field":"col1", "agg":"avg", "agg_value":2.0 },
    {
    "field":"col1", "agg":"stddev", "agg_value":1.0 },
    {
    "field":"col1", "agg":"skewness", "agg_value":0.0 },
    {
    "field":"col1", "agg":"kurtosis", "agg_value":-1.5 },
    {
    "field":"col1", "agg":"countDistinct", "agg_value":3.0 }
  ],
  "arr_stat":[
    {
    "arrName":"col3", "statType":"mean", "value":2.5 },
    {
    "arrName":"col3", "statType":"avg", "value":2.5 },
    {
    "arrName":"col3", "statType":"kurtosis", "value":-1.2000000000000006 },
    {
    "arrName":"col3", "statType":"min", "value":1.0 },
    {
    "arrName":"col3", "statType":"countDistinct", "value":3.0 },
    {
    "arrName":"col3", "statType":"stddev", "value":1.2907110929399985 },
    {
    "arrName":"col3", "statType":"max", "value":4.0 },
    {
    "arrName":"col3", "statType":"skewness", "value":0.0 }
  ],
  "enum_stat":[
    {
    "name":"col2", "count":3, "freq":{
    "b":1, "a":2, "NULL":1 } }
  ]
}

输出这样详细的结果,只需要我们做一个简单的配置,那就是要做数据质量检查的文件路径.字段的类型是自动推断的.
当然这只是一个最基本的需求,所以它必然允许我们个性化定制,通过配置文件的详细配置,我们还可以:

  1. 指定哪些文件需要检查数据质量,而不限于1个.
  2. 每个要检查的文件都做哪些质量检查,不需要的可以不做.
  3. 可以做更丰富的交叉表检查,这样更聚焦更细致.

如果有一定的Coding基础,通过插件扩展,我们还可以:

  1. 检查其他数据源比如JDBC或者ES的数据质量;
  2. 根据质量检查结果和上次检查结果做异常发现,告警通知,执行某个脚本控制其他任务的执行等等.

第三阶段: 实时诊断

上述的质量监控,是从整体上把握数据,这一阶段的目标是要掌握数据的极端情况,极端数据并没有数据质量问题,但从业务逻辑上却属于异常情况,是需要给予特别关注的.从诊断方法方面分类,可分为如下两类目标.

第一目标 单记录诊断

以数据实例来说,在如下的记录中voltage_list是电池单体电压数据,其数据类型是数组,元素个数代表单体个数,过高的均值或者方差都是不正常的,对应的现象可能是电池充电故障或者单体故障.

{
   "voltage_list":[3.2,3.4,3.41],"tsp_got_time":1674736665000,"vin":"A0","other":1}
{
   "voltage_list":[4,3.6,3.7],"tsp_got_time":1674736675000,"vin":"A1","other":2}

Pulse 处理这类异常,无论是单记录还是多记录都能很好地处理.我们要做的就是指定异常规则,其形式如下所示:

expression=array_min(voltage_list)>3.5,eng_name=test,chinese_name=电压过高
  1. expression
    它是用来指定判定异常的逻辑表达式,在这里可以使用统计函数,也可以使用数组函数,不限于单个字段,也可以指定任意多个字段的复杂表达式
  2. rule
    上述示例指定的rule,可以指定多个,它们之间是的关系,即满足任意一条rule,即把这条数据记录视为异常.
  3. 数据读取及存储
    Pulse 的实现中,数据读自 Kafka,发现的异常数据存入Kafka. 以方便其他应用端能够方便的订阅这些异常数据.

第二目标 多记录诊断

上述单记录的实时监控,其能力还是有限的,车联网或者其他物联网的数据,往往都是实时数据,且数据的前后是存在关联的,比如车辆的碰撞事故是跟速度及加速度存在很大关系的,而这两个物理量的计算是要根据前后多条记录才能计算出来.更复杂一点的,车辆在某个时间点发生故障比如熄火,其实在这一时间点之前的一段时间内比如1分钟内或者1小时内已经有很多信号反映出异常,这在理论上是存在提前预测甚至避免故障的可能的.

下面以数据案例来举例说明:

{
   "vin":"vin1","tsp_got_time":1674736665000,"esd_volt":48}
{
   "vin":"vin1","tsp_got_time":1674736675000,"esd_volt":50}
{
   "vin":"vin1","tsp_got_time":1674736685000,"esd_volt":55}
{
   "vin":"vin1","tsp_got_time":1674736695000,"esd_volt":60}
{
   "vin":"vin1","tsp_got_time":1674736705000,"esd_volt":58}
{
   "vin":"vin1","tsp_got_time":1674736715000,"esd_volt":50}
{
   "vin":"vin1","tsp_got_time":1674736725000,"esd_volt":45}
{
   "vin":"vin2","tsp_got_time":1674736665000,"esd_volt":30}
{
   "vin":"vin2","tsp_got_time":1674736675000,"esd_volt":50}
{
   "vin":"vin2","tsp_got_time":1674736685000,"esd_volt":55}
{
   "vin":"vin2","tsp_got_time":1674736695000,"esd_volt":60}
{
   "vin":"vin2","tsp_got_time":1674736705000,
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值