4.流式计算

来源:互联网 发布:鹅绒被和蚕丝被 知乎 编辑:程序博客网 时间:2024/05/17 21:55

流式计算,spark streaming 之前有spark core开发的积累,直接使用spark streaming来进行流式计算开发是比较节省开发成本的。

业界同样还有优秀的流式计算框架,简单介绍一下

1、Storm

响应快,纯流式,底层全是无锁编程,想做汇聚,想搞个中间状态,需要借助外部存储。

 

2、Samza

kafka上接了MR,使用yarn来管理集群,Topic取下来,samza处理(MR),输出放入topic

 

3、Flink

为了抗衡spark streaming而诞生的。之后就是双方的相互借鉴,相互学习,相互进步了。

flink与spark的主要区别在与:

flink可以按条数来进行流式处理,spark暂时还是只能按照时间来进行流式处理;

flink是纯流式,spark straming是微批处理。

 

 

 

简单介绍下spark streaming

说了这么多,我们来介绍介绍我们今天的主角。这里不说谁好谁差,最终是要看业务场景和具体需求,和开发成本的。

通俗点说吧,spark streaming 就是给spark core加了个时间机制,将计算间隔缩短到了秒级别的微批处理。

 

光说也说体现不出什么,需要对比才能知道最终的需求。

我们与storm对比一下吧

 

1,处理速度方面,因为strom的底层是无锁编程,spark的底层是批处理

 

2,Spark Streaming是Spark的核心子框架之一。

说到Spark核心,那就不得不说RDD了。

Spark Streaming作为核心的子框架,对RDD的操作支持肯定是杠杠的,这又说明了什么?

Spark Streaming可以通过RDD和Spark上的任何框架进行数据共享和交流,这就是Spark的野心,一个堆栈搞定所有场景

 

3,基本会了spark core 短暂学习就可以上手spark streaming,可以结合之前spark core的开发,保证有两套计算,在线计算,离线计算,节省开发成本。

 

4,Spark Streaming支持多语言编程,并且各个语言之间的编程模型也是类似的,strom是java主力开发,spark是scala主力开发。函数式与指令式自行百度。

 

5,Spark Streaming的容错机制。Spark Streaming在读取流数据进入内存的时候会保存两个副本,计算只用一个,当出现问题的时候可以快速的切换到另外一个副本;在规定的时间内进行数据的固化;由于支持RDD操作,所以RDD本身的容错处理机制也被继承(这算是spark的优势,也是劣势吧,劣势会占用太多资源)

 

 

spark streaming基本执行流程

1,以时间片为单位划分形成数据流形成RDD(DStream),这一点要细说一下。

在spark core中,常用sc.textFile去读取数据,在spark streaming中是将读取的数据创建Dstream数据流

例如代码spark是 val RDD = sc.textFile***

spark streaming见下图,例如创建一个socket的数据流

2,对每个划分数来的RDD以批处理的方式处理数据

3,每个划分出来的RDD地处理都会提交成Job

4,后面的流程基本与spark core的流程一样了。

 

 

 

 

第一步 思路解析

 

1.1简单实现

首先我们要实现,spark能从kafka中消费数据。因为我们这套流程使用的spark2.1.0(scala2.11.8)+ kafka0.10.2.0。

国内没有资源讲解spark2.1 + kafka010,先啃官方文档

http://spark.apache.org/docs/latest/streaming-kafka-0-10-integration.html

 

创建一个简单的流去消费kafka数据

几个注意的地方

引的kafka的包是kafka010,以前的版本都是直接kafka没有后面的数字

需要配置个kafkaParams参数

使用KafkaUtils.createDirectStream方式来创建数据流

 

1.2保证消费高可用

网上能查到的资料,基本是两极分化

第一种是开启spark的WAL机制,这种就是将数据读两份,一份计算,另一份进行容错重算使用,这样太耗资源了,暂不考虑

第二种是使用kafka的低级api,手动维护spark消费kafka数据的偏移度,来保证消费的高可用,但是网上能查到的版本全是kafka0.8 kafka0.9的。代码完全不能拿来用。

 

继续啃文档

文中有说到,可以根据偏移度,创建kafka的数据流。但是我怎么获得偏移度呢?

 

继续看

找到个,可以获得偏移度的方法

 

文档中差不多就这2个了。根据偏移度创建流,再获得偏移度。

我们来撸一撸思路。

 

思路1:

根据最后计算偏移度读取kafka数据

  ||

创建流

  ||

业务代码计

  ||

计算成功,保存偏移度

  ||

计算结果,存至外部

 

 

思路暂时这样没错,但是我怎么去实现,根据最后偏移度读取数据呢?我怎么知道是最后的偏移度?

保存偏移度,保存到哪呢?

 

我们先试试输出偏移度

这里使用的println,会在spark的标准输出stdout中出现。

我们去看看

 

偏移度打出来。

那我们可以把那个结束偏移度保存起来,每次让DStream从这个偏移度去读新的数据,进行计算,计算完之后再获取一次偏移度,将最新的偏移度保存起来。

 

突然想起个问题,如果是一个新的kafka topic,我都还没有计算过呀,怎么得到最后的偏移量呢?

那我们是不是可以,在创建数据流的之前做个判断,如果是个新的topic的时候,我们以默认为0的偏移度创建是否可行呢?

 

最后,我们这个外部存储用什么呢?这里我们用zk,看能不能实现,但是印象中我记得zk是不能很好的支持高并发的读写,如果流式处理数据量上来,秒级别的对zk的读写,肯定会出现问题。

不过我们可以先以zk来做测试,如果zk都可以读写,其他的外部存储肯定没问题。

 

修改后的思路2:

判断zk中是否有保存过该计算的偏移量

如果有就接着计算,没有的话创建个新的,从0开始算

  ||

创建流

  ||

业务代码计算

  ||

计算成功,保存偏移度

  ||

计算结果,存至外部

 

 

 

 

第二步 代码讲解

 

2.1 maven引包

<properties>  <scala.version>2.11.8</scala.version>  <spark.version>2.1.0</spark.version>  <hadoop.version>2.6.0</hadoop.version></properties><dependencies>  <dependency>    <groupId>org.apache.spark</groupId>    <artifactId>spark-streaming_2.11</artifactId>    <version>${spark.version}</version>  </dependency>  <dependency>    <groupId>org.scala-lang</groupId>    <artifactId>scala-library</artifactId>    <version>${scala.version}</version>  </dependency>  <dependency>    <groupId>org.apache.spark</groupId>    <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>    <version>${spark.version}</version>  </dependency>  <dependency>    <groupId>org.apache.kafka</groupId>    <artifactId>kafka_2.11</artifactId>    <version>0.10.2.0</version>  </dependency>  <dependency>    <groupId>org.apache.kafka</groupId>    <artifactId>kafka-clients</artifactId>    <version>0.10.2.0</version>  </dependency>  <dependency>    <groupId>com.101tec</groupId>    <artifactId>zkclient</artifactId>    <version>0.10</version>  </dependency>  <dependency>    <groupId>org.apache.zookeeper</groupId>    <artifactId>zookeeper</artifactId>    <version>3.4.9</version>  </dependency>




2.2 scala代码

import org.apache.kafka.common.TopicPartitionimport org.apache.kafka.common.serialization.StringDeserializerimport org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistentimport org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribeimport org.apache.spark.streaming.kafka010.{HasOffsetRanges, KafkaUtils, OffsetRange}import org.apache.spark.{SparkConf, SparkContext, TaskContext}import org.apache.spark.streaming.{Seconds, StreamingContext}object DealFlowBills2 {  /** ***************************************************************************************************************    * todo zookeeper 实例化,方便后面对zk的操作,zk的代码很简单,这里就不贴了,基本照抄官方api    */  val zk = ZkWork  def main(args: Array[String]): Unit = {    /** ***************************************************************************************************************      * todo 输入参数      */    val Array(output, topic, broker, group, sec) = args    /** ***************************************************************************************************************      * todo spark套路      */    val conf = new SparkConf().setAppName("DealFlowBills2")    val sc = new SparkContext(conf)    val ssc = new StreamingContext(sc, Seconds(sec.toInt))    /** ***************************************************************************************************************      * todo 1 - 准备kafka参数      */    val topics = Array(topic)    val kafkaParams = Map[String, Object](      "bootstrap.servers" -> broker,      "key.deserializer" -> classOf[StringDeserializer],      "value.deserializer" -> classOf[StringDeserializer],      "group.id" -> group,      "auto.offset.reset" -> "latest",      "enable.auto.commit" -> (false: java.lang.Boolean)    )    /** ***************************************************************************************************************      * todo 2 - 判断zk中是否有保存过该计算的偏移量      * 如果没有保存过,使用不带偏移量的计算,在计算完后保存      * 精髓就在于KafkaUtils.createDirectStream这个地方      * 默认是KafkaUtils.createDirectStream[String, String](ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams)),不加偏移度参数      * 实在找不到办法,最后啃了下源码。有个consumerStrategy消费者策略,看看里面是个什么套路;      *        * 原来可以执行topic,和offsets消费偏移度,这下派上用场了      *        *       *      */    val stream = if (zk.znodeIsExists(s"${topic}offset")) {      val nor = zk.znodeDataGet(s"${topic}offset")      val newOffset = Map(new TopicPartition(nor(0).toString, nor(1).toInt) -> nor(2).toLong)//创建以topic,分区为k 偏移度为v的map      println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")      println(s"[ DealFlowBills2 ] topic ${nor(0).toString}")      println(s"[ DealFlowBills2 ] Partition ${nor(1).toInt}")      println(s"[ DealFlowBills2 ] offset ${nor(2).toLong}")      println(s"[ DealFlowBills2 ] zk中取出来的kafka偏移量★★★ $newOffset")      println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")      KafkaUtils.createDirectStream[String, String](ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams, newOffset))    } else {      println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")      println(s"[ DealFlowBills2 ] 第一次计算,没有zk偏移量文件")      println(s"[ DealFlowBills2 ] 手动创建一个偏移量文件 ${topic}offset 默认从0号分区 0偏移度开始计算")      println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")      zk.znodeCreate(s"${topic}offset", s"$topic,0,0")      val nor = zk.znodeDataGet(s"${topic}offset")      val newOffset = Map(new TopicPartition(nor(0).toString, nor(1).toInt) -> nor(2).toLong)      KafkaUtils.createDirectStream[String, String](ssc, PreferConsistent, Subscribe[String, String](topics, kafkaParams, newOffset))    }    /** ***************************************************************************************************************      * todo 3 - 业务代码部分      * 将流中的值取出来,用于计算      */    val lines = stream.map(_.value())    lines.count().print()    val result = lines      .filter(_.split(",").length == 21)      .map {        mlines =>          val line = mlines.split(",")          (line(3), s"${line(4)},${line(2)}")      }      .groupByKey()      .map {        case (k, v) =>          val result = v            .flatMap {              fmlines =>                fmlines.split(",").toList.zipWithIndex            }            .groupBy(_._2)            .map {              case (v1, v2) =>                v2.map(_._1)            }          (k, result)      }    /** ***************************************************************************************************************      * todo 4 - 保存偏移度部分      * 计算成功后保存偏移度      */    stream.foreachRDD {      rdd =>        val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges        rdd.foreachPartition {          iter =>            val o: OffsetRange = offsetRanges(TaskContext.get.partitionId)            println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")            println(s"[ DealFlowBills2 ]  topic: ${o.topic}")            println(s"[ DealFlowBills2 ]  partition: ${o.partition} ")            println(s"[ DealFlowBills2 ]  fromOffset 开始偏移量: ${o.fromOffset} ")            println(s"[ DealFlowBills2 ]  untilOffset 结束偏移量: ${o.untilOffset} 需要保存的偏移量,供下次读取使用★★★")            println(s"[ DealFlowBills2 ] --------------------------------------------------------------------")            // 写zookeeper            zk.offsetWork(s"${o.topic}offset", s"${o.topic},${o.partition},${o.untilOffset}")          // 写本地文件系统          // val fw = new FileWriter(new File("/home/hadoop1/testjar/test.log"), true)          // fw.write(offsetsRangerStr)          // fw.close()        }    }    /** ***************************************************************************************************************      * todo 5 - 最后结果操作部分      */    result.saveAsTextFiles(output + s"/output/" + "010")    /** ***************************************************************************************************************      * todo spark streaming 开始工作      */    ssc.start()    ssc.awaitTermination()  }}


 

 

 

 

第三步 调试实现

 

3.1 场景设想

回忆一下,我们之前需要实现的两个主要功能

1.flume文件采集数据不丢失,断点续采,根据崩溃时的索引,重启程序后能继续采集

2.spark streaming 消费kafka 实现数据零丢失,避免kafka重复消费,spark streaming 手动控制 kafka消费偏移量,将消费偏移量存到zookeeper,来保证消费高可用

 

现在文件采集的高可用是实现了,只要数据生成了。flume和kafka的启动谁先,谁后,都能保证数据不丢。

 

但是spark是在数据生成之前启,还是数据生成之后启,这就有点区别了。我们看看下面的两种场景。

 

暂时想到有2种场景

场景一

1,kafka创建topic

2,开启flume采集

3,开启spark streaming 计算

4,模拟数据源生成数据

5,开始计算

 

场景二

1,kafka创建topic

2,开启flume采集

3,模拟数据源生成数据

4,开启spark streamjing计算

5,开始计算

 

场景一是肯定没问题了,spark在数据生成之后,是肯定可以接收到数据开始计算的。

我们来模拟调试一下场景二

 

3.2 场景模拟

1.创建topic

 

2.开启flume采集

 

3.模拟生成数据

 

4.开启spark streaming 计算

 

5.开始计算,因为我们刚才只生成了1w,所以这里只有1w数据,但是为什么这里会多出5条,暂时没解决,环境大家批评斧正。

 

 

3.3 模拟spark宕机

造1000W数据

 

模式宕机

 

等个若干秒,再启动程序

 

停止生成数据,这里是生成了2416634条,我们看看spark那边有多少条

 

检查spark收到的条数

 

一共是

293434+13900+93200+101000+1217700+100100+131300+131600+129100+139000+66300 = 2416634

正好与生成数据的数量相符

 

 

3.4 待解决问题

这是实时在线计算,但是如果我们要考虑历史原始数据保留的话,该怎么操作比较合理?

怎么实现远程文件采集呢?

 


源代码已上传了github

https://github.com/feloxx/SparkStreaming-DirectKafka010

1 0