基于Spark的交互式大数据预处理系统设计与实现(六)  集群流数据处理

  流数据处理概述

在数据流模拟生并写入kafka(topic:resource,即kafka下自定义名为resource的topic,下同)后,接下来的工作便是从该数据源中获取数据及运用sparkStream进行数据流的一系列实时处理,并将数据处理结果实时写入到kafka(topic:dist)中进入下一阶段的运用。集群流数据处理是本系统的核心所在,在数据转换中需要依赖于iprules(IP规则,由本系统一阶段离线数据批处理所得,详细见后文)。为充分运用语言特性加快开发进程,在此采用java+scala混合编程,即使用scala对数据进行一系列的核心处理操作,java用于系统的辅助性功能开发。在本功能模块中,为节约数据库资源及考虑到数据展示实时性的需求,并未主动将数据进行持久化,而是采用sparkStream+kafka消息中间件的形式形成了一条主要基于内存的数据处理工作流[]。本系统sparkStream流计算任务运行时,spark UI监控图如下图5-2所示:

 

图5-2 sparkStream Web UI

结合sparkStream的工作原理可以清楚地观察到数据流被按照自定义时间间隔划分成了一个个批次(batch,本系统以5秒划分为一个batch)进行处理,每个批次的处理本质上就是一个小的spark批处理。其中batch数据处理的DAG划分如下图5-3所示:

 

图5-3 SparkStream batch DAG

  ipRules生成

上文所述ipRules(ip与地理位置映射数据集,以下简称ipRules或ip规则)需要系统经过一阶段数据提取从原始数据获取,原始数据包含多个详细字段,而我们仅需要其中一部分数据就可生成本系统需要的ipRules,ip规则原始数据如下图5-4所示:

 

图5-4 原始ipRules文本数据集样本

关于ip规则映射需要说明的是:由于ip是由四个字段间隔三个点号组成的字符串,并且在实际的场景中ISP运营商往往是将连续的ip分给某个机构,这个连续往往是体现在低位字段的连续分配,加之ip掩码的存在可以使ip地址划分非常灵活,故而我们很难直接对原始ip字符串进行区间判断得到地理位置映射。面对上述问题,一般情况下会选择先将ip地址转化为某个唯一的Long型数字,而后根据数字进行ip映射。从图5-4可知第3、4个字段(以‘|’符号进行字符串切割)即为对应的ip区间转换而成的Long型数字区间。同样的,在提取得到ipRules后的数据处理阶段,对应于每一个流数据batch提取的ip数据同样需要进行数字转换,本系统中该方法名为ipToLong,代码如下所示:

//(this function coming from xiaoniu Bigdata institution)

  def  ipToLong(ip: String): Long = {

    val fragments = ip.split("[.]")

    var ipNum = 0L

    for (i <- 0 until fragments.length){

      ipNum =  fragments(i).toLong | ipNum << 8L

    }

    ipNum

  }

ipRules的获取属于集群数据处理核心的第一阶段部分,该数据集记录约为十万行,在此利用spark离线批处理大约一分钟能够处理完。需要注意的是spark启用多个节点对数据进行计算,虽然面向开发人员只有一个RDD(弹性分布式数据集),但RDD由多个partition(分区)组成,或许每个分区都不在同一台机器上,每个partition仅有一部分数据,在单个分区里最终提取得到的ipRules也只是部分数据。而在下一阶段的ip映射中需要完整的ipRules才能得到正确的映射结果,因此在完成ipRules的提取生成后还需要将其collect收集到Driver端进一步广播到每个执行ip映射partition的处理节点上,如此每个运算节点便得到了完整的ipRules。同样的在前面所述异常数据生成时的34所省级单位亦需要进行广播扩散。ipRules生成及广播代码如下:

def getIpRulesBro( spark: SparkSession )={

 //1.get ipRules from ETL and broadcast

 //get data from fileSystem of windows or other to ETL proces

val ipt: RDD[String] = spark.sparkContext

.textFile("hdfs://chdp01:9000/weblog/ip.txt")

 import spark.implicits._

 val ipRules: RDD[Row] =ipt.map(line=>{

      val words=line.split("[|]")

      val begin=words(2).toLong

      val end=words(3).toLong

      val province=words(6)

      Row(begin,end,province)

    })

//because of distributed Rdd,

there are need to broadcast integrated iprules to every node

val ipbro: Broadcast[Array[Row]] =spark.sparkContext.

broadcast(ipRules.collect())

      ipbro

}

  流数据获取

有了ipRules若再有了需要处理的数据源就可以进行正式的数据处理了。经过流数据模拟生成阶段已经有了实时数据流,直接使用kafka工具从kafka(topic:resource)获取即可。需要注意的是若在在流数据处理阶段需要进行累加聚合则需要设置kafka checkpoint目录。此外将批次获取时间间隔设置为5秒(数据上下流处理频率一一对应)。本系统将checkpoint目录设置在hdfs文件系统下(跟随spark程序),具体代码如下所示:

//.get province from webLog data of Kafka topic(resource) base on ipRules

//create Stream

val ssc =new StreamingContext(spark.sparkContext,Milliseconds(5000))

ssc.checkpoint("./checkpoint1")

//get resource String SparkStream data

val kcs: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc,

      "chdp01:2181,chdp02:2181,chdp03:2181","g02",

Map("resource"->2),StorageLevel.MEMORY_ONLY)

//get String of topic

val ks=kcs.map(_._2)

集群源数据获取测试输出结果如下图5-5所示:

 

图5-5 集群获取kafka(topic:resource)数据截图

  流数据处理

简单地说此处就是利用sparkStream根据ipRules将从kafka(topic:resource)获取的数据流经过一系列清洗、规约、聚合等操作转换成目标数据类型,而后将结果写回kafka(topic:dist)数据流。此处为系统的核心所在,之前所有的操作都是为此铺垫。此外本系统针对数据流处理开发了两种执行模式:accumulation、noaccumulation。顾名思义区别在于stream数据统计是否对当前计算批次数据之前的batch处理结果数据进行累加。在此用到的核心方法为 streamEtlToKafka,以下对该方法及所用到的依赖方法进行详细论述。

首先该方法需采用ipToLong方法将获取得到batch中的每条ip段数据转换为对应Long型数字(详见上述)。而后需要在numToProvince方法中根据ipRules进行数据转换得到对应省份,在此采用的是简单的拓展二分法查找,具体代码不再展示。在上述两个方法下将ip型数据映射转换为对应34所省级单位。当然,这是在理想情况下的最简单流程,不用考虑各种异常数据的处理。而一个健壮的数据处理系统应该能够对多种来源的数据进行处理,可以应对各种异常数据。显然,本系统开发了一系列的方法来提高数据异常判断处理能力。对于关键数据异常中的脏数据,本系统直接丢弃,当然其中一种异常数据来源于数据类型的不一致,即ip段数据替换成了省份以模拟不同数据源(详细见5.1节所述)。

本系统开发了一个核心处理方法对源数据进行一系列判断,方法签名:judgeDataOfIpArea(data:String,pros:Array[String]),同样的该方法依赖于一系列辅助方法。此处的data数据类型提取自原始数据切分后的第二个字段,该字段在原始数据多样性模拟下可能为这些数据类型:null、error data、 province(如上海)、 ip(192.168.33.1)等,如此需要对data进行类型判断以确定需要进行何种处理。因为null、error data、 province等都是在实际环境中的无数种数据异常中的小部分而已,不可能进行具体的判断(异常是难于穷举的)。而像province,ip是数据采集中可以确定的采集对象(目标),故在此仅需要判断data数据类型是否属于province或ip类型,若不是则直接舍弃。一个略显偏激的说法是:不在预期可接受范围内的数据皆可判断为异常数据。

judgeDataOfIpArea处理策略展开如下:

  1. Province判断:在此province为中文省份、直辖市或自治区全称,采用广播34省级单位进行包含判断即可。值得注意的是,此处包含判断基于查找算法,不同于ipRules映射时采用的二分法查找,由于此处为无序且关键字唯一的数据集,故而在实际运用之前将34所省级单位数据集转换为HashSet类型以提高包含判断查找效率。合理的数据结构与算法选择将会大大提高系统效率,大数据环境下尤其如此。
  2. Ip判断(依赖方法:isLegalIp):若province判断未通过则进行ip判断,根据ip(如:192.168.33.1)数据特征,我们不难发现ip数据类型为num.num.num.num(0<=num<=255)。而实际上在数据处理下一阶段进行ipLocation(根据ipRules将ip映射转换为对应省份)处理时对于num不在范围的ip完全可以被可滤掉;但这样可行吗?或者说符合数据预处理规范吗?显然不行,根据前文我们的ipLocation方法具体实现可知,在ipLocation阶段需要根据十万行级的ipRules进行ip定位查找,虽然此处采用二分法查找效率相对较高。但如此策略不仅消耗了不必要的运算资源,而且浪费了网络传输资源(在分布式环境下,错误ip本可以在上一阶段就被过滤却要通过shuffle传送到数据处理的下一阶段),在大数据场景下这是开发者不希望看到的,也是现实运用场景所不能接受的。故而在此处将ip段的四个数据段num提取判断其是否为合法ip,仅有合法ip才能进入下一阶段进行处理。isLegalIp方法为简单的逻辑判断,具体代码不再展示,其参考返回值及对应后续处理方法如下:

-1:异常数据(error data,null,非法ip,other),丢弃。

0:34个省级单位,直接跳过ipLocation阶段。

1:合法ip,进入ipLocation阶段转换成34个省级单位名称。

isLegalIp方法用于在34所省级单位判断后对剩余的数据做进一步判断,此时剩余数据依旧糅杂含有各种异常数据,虽然isLegalIp本身具体逻辑简单,但依旧需要注意,展开论述如下:

首先判断该数据是否为ip模式(见上述),在确定为正确ip模式后取出ip对应的四个字段进行进一步的判断处理。在此有一个细节需要处理,即在将ip的四个字段转换为数字之前需要判断该字段是否可以转换为数字,试想若该字符串并不是由数字组成而硬要把它转换为数字那就不是得抛出异常了吗。按基本编程规范,对于这种可预见的异常我们是需要预先处理的,故而在此需要在转换前利用正则表达式对待处理字符串进行判断是否可以转换为数字,而后作进一步的转换判断。

在streamEtlToKafka中完成数据ip域字段的判断后,得到当前数据流batch的当前处理记录数据ip段的判断结果,若判断结果为异常数据,则将province的值设置为某个唯一标识即可,而在完成本次流数据batch的map转换处理后将进行filter操作对该标识数据进行过滤。至此variety of data to ETL(parkStream+kafka处理多样化数据来源)步骤初步完成。其中流处理中的每个批次预处理及异常数据过滤前后对比数据结果如下图5-6所示:

图5-6 集群batch数据处理结果截图

处理累加聚合后的最终结果数据展示如下图5-7所示(该数据作为web端数据源):

图5-7 集群数据流最终处理结果截图

其中streamEtlToKafka核心代码如下所示:

def streamEtlToKafka(accessType:String)={

// create sparkSession

val spark: SparkSession =SparkSession.builder().appName("streamEtlToKafka").

master("spark://chdp01:7077").getOrCreate()//

//get ipBroadcast

val ipbro: Broadcast[Array[Row]] =getIpRulesBro(spark)

//get province broadcast

    val probro: Broadcast[Array[String]] =getProvinceBro(spark)

    //transform Broadcast[Array[String]] to hashSet

    var hs: mutable.Set[String] =new mutable.HashSet[String]()

    probro.value.foreach(pro=>{

      hs.add(pro)

    })

    //2.get province from webLog data of Kafka topic(resource) base on ipRules

    //create Stream

    val ssc =new StreamingContext(spark.sparkContext,Milliseconds(5000))

    ssc.checkpoint("./checkpoint1")

    //get resource String SparkStream data

val kcs: ReceiverInputDStream[(String, String)] = KafkaUtils.

createStream(ssc, "chdp01:2181,chdp02:2181,chdp03:2181","g02",

      Map("resource"->2),StorageLevel.MEMORY_ONLY)

    //get String of topic

    val ks=kcs.map(_._2)

    //operator as rdd

    val provincesWithOnepre: DStream[(String, Int)] = ks.map(line=>{

      val wordslog=line.split("[|]")

      val ipa=wordslog(1)

      var province=""

      /*need to judge the type of ip (ip,province or other)

      -1:exception data,give up

      0:34 provinces,step up ipLocation(numToProvince)

      1:ip

      in this section,a lot of data are belong of data that we are expect

      so we using huffman theory to optimize our program code

      */

      if(judgeDataOfIpArea(ipa,hs)==0 ){

        province=ipa

      }

      else if(judgeDataOfIpArea(ipa,hs)==1){

        val ipNum=ipToLong(ipa)

        province=numToProvince(ipNum,ipbro.value)

      }

      else {

//exception data marked

        province="Exception"

      }

      (province,1)

    })

      

    //filter e of province value(from exception data)

    //keeping data if satisfy param

val provincesWithOne= provincesWithOnepre.filter(_._1!="Exception")    

//test out put

    provincesWithOne.foreachRDD(rdd=>{

        println("provincesWithOne:")

        rdd.collect().foreach{println}

      })

    //3.select access base on accessType(parameter),acc default

    //than as same as wordCount

    var wc=if(accessType=="acc"){

      provincesWithOne.updateStateByKey(upateFunc,new  HashPartitioner(ssc.sparkContext.defaultParallelism),true)

    }

    else if(accessType=="noacc"){

      provincesWithOne.reduceByKey(_+_)

    }

    else{//access accumulation computing default

      provincesWithOne.updateStateByKey(upateFunc,new HashPartitioner(ssc.sparkContext.defaultParallelism),true)

    }

//4.in this block,rewrite data to kafka 'dist' of topic after ETL,

play a role of producer,and call java function



    val topic = "dist"

    wc.foreachRDD(rdd=>{

      rdd.foreachPartition(part=>{

        //move computor but data

        // use java function  wc: DStream[(String, Int)] =>part[(String,Int)]

        val arrays: Array[(String, Int)] = part.toArray

        KafkaTool.writeResultTokafka(topic, arrays)

      })

    })

    ssc.start()

    ssc.awaitTermination()

  }

 

  • 1
    点赞
  • 3
    收藏
  • 打赏
    打赏
  • 1
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论 1

打赏作者

尘客.

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值