4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]

流式计算,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的数据流

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第1张图片

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方式来创建数据流

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第2张图片

 

1.2保证消费高可用

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

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

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

 

继续啃文档

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

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第3张图片

 

继续看

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

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第4张图片

 

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

我们来撸一撸思路。

 

思路1:

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

  ||

创建流

  ||

业务代码计

  ||

计算成功,保存偏移度

  ||

计算结果,存至外部

 

 

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

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

 

我们先试试输出偏移度

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第5张图片

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

我们去看看

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第6张图片

 

偏移度打出来。

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

 

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

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

 

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

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

 

修改后的思路2:

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

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

  ||

创建流

  ||

业务代码计算

  ||

计算成功,保存偏移度

  ||

计算结果,存至外部

 

 

 

 

第二步 代码讲解

 

2.1 maven引包


  2.11.8
  2.1.0
  2.6.0




  
    org.apache.spark
    spark-streaming_2.11
    ${spark.version}
  

  
    org.scala-lang
    scala-library
    ${scala.version}
  

  
    org.apache.spark
    spark-streaming-kafka-0-10_2.11
    ${spark.version}
  

  
    org.apache.kafka
    kafka_2.11
    0.10.2.0
  

  
    org.apache.kafka
    kafka-clients
    0.10.2.0
  

  
    com.101tec
    zkclient
    0.10
  

  
    org.apache.zookeeper
    zookeeper
    3.4.9
  




2.2 scala代码

import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import 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 direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第7张图片

 

4.开启spark streaming 计算

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第8张图片

 

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

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第9张图片

 

 

3.3 模拟spark宕机

造1000W数据

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第10张图片

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第11张图片

 

模式宕机

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第12张图片

 

等个若干秒,再启动程序

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第13张图片

 

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

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第14张图片

 

检查spark收到的条数

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第15张图片4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第16张图片

 

一共是

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

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

4.流式计算 - spark direct方式计算手动控制kafka偏移度[spark straming2.1 + kafka0.10.2.0]_第17张图片

 

 

3.4 待解决问题

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

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

 


源代码已上传了github

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

你可能感兴趣的:(spark,streaming)