大数据之Spark(九):Spark Streaming 概述

一、流式计算简介

1.1 流式计算

        理解流式计算,最形象的例子,就是小明的往水池中放(入)水又放(出)水的案例。流式计算就像水流⼀样, 数据连绵不断的产生,并被快速处理,所以流式计算拥有如下⼀些特点:

  • 数据是无界的(unbounded)
  • 数据是动态的
  • 计算速度是非常快的
  • 计算不止一次
  • 计算不能终⽌
  • 反过来看看⼀下离线计算有哪些特点:
  • 数据是有界的(Bounded)
  • 数据静态的
  • 计算速度通常较慢
  • 计算只执行一次
  • 计算终会终止

        在大数据计算领域中,通常所说的流式计算分为实时计算准实时计算。所谓实时计算就是来一条记录(⼀个事件 Event)启动⼀次计算;而准实时计算则是介于实时计算和离线计算之间的⼀个计算,所以每次处理的是⼀个微小的批次。

1.2 常见的离线和流式计算框架

常见的离线计算框架

  1. mapreduce
  2. spark-core
  3. flink-dataset

常见的流式计算框架

1. storm(jstorm)

第⼀代的流式处理框架,每⽣成⼀条记录,提交⼀次作业。实时流处理,延迟低。

2. spark-streaming

第⼆代的流式处理框架,短时间内⽣成mirco-batch,提交⼀次作业。准实时,延迟略⾼,秒级或者亚秒级延迟。

3. flink-datastream(blink)

第三代的流式处理框架,每⽣成⼀条记录,提交⼀次作业。实时,延迟低。

1.3 SparkStreaming简介

SparkStreaming,和SparkSQL⼀样,也是Spark生态栈中非常重要的一个模块,主要是用来进行流式计算的框架。流式计算框架,从计算的延迟上面,又可以分为纯实时流式计算和准实时流式计算,SparkStreaming属于准实时计算框架

所谓纯实时的计算,指的是来⼀条记录(event事件),启动⼀次计算的作业;离线计算,指的是每次计算⼀个非常大的⼀批(比如几百G,好几个T)数据;准实时计算,介于纯实时和离线计算之间的⼀种计算⽅式。显然不是每⼀条记录就计算⼀次,显然比起离线计算数据量小的多,使用Micro-batch(微小的批次)来表示。

SparkStreaming是SparkCore的api的⼀种扩展,使用DStream(discretized stream or DStream)作为数据模型, 基于内存处理连续的数据流,本质上还是RDD的基于内存的计算。

DStream,本质上是RDD的序列。SparkStreaming的处理流程可以归纳为下图:

大数据之Spark(九):Spark Streaming 概述_第1张图片

 1.4 SparkStreaming基本工作原理

接收实时输入数据流,然后将数据拆分成多个batch,⽐如每收集1秒的数据封装为⼀个batch,然后将每个batch交给Spark的计算引擎进⾏处理,最后会⽣产出⼀个结果数据流,其中的数据,也是由⼀个⼀个的batch所组成的。

Spark Streaming提供了⼀种⾼级的抽象,叫做DStream,英⽂全称为Discretized Stream,中⽂翻译为“离散 流”,它代表了⼀个持续不断的数据流。DStream可以通过输⼊数据源来创建,⽐如Kafka、Flume、ZMQ和 Kinesis;也可以通过对其他DStream应⽤⾼阶函数来创建,⽐如map、reduce、join、window。

DStream的内部,其实⼀系列持续不断产⽣的RDD。RDD是Spark Core的核心抽象,即分布式弹性数据集。DStream中的每个RDD都包含了⼀个时间段内的数据。

大数据之Spark(九):Spark Streaming 概述_第2张图片

对DStream应⽤的算⼦,⽐如map,其实在底层会被翻译为对DStream中每个RDD的操作。⽐如对⼀个 DStream执⾏⼀个map操作,会产⽣⼀个新的DStream。但是,在底层,其实其原理为,对输⼊DStream中每个 时间段的RDD,都应⽤⼀遍map操作,然后⽣成的新的RDD,即作为新的DStream中的那个时间段的⼀个RDD。 底层的RDD的transformation操作。

还是由Spark Core的计算引擎来实现的。Spark Streaming对Spark Core进⾏了⼀层封装,隐藏了细节,然后对 开发⼈员提供了⽅便易⽤的⾼层次的API。

大数据之Spark(九):Spark Streaming 概述_第3张图片

 1.5 Storm V.S. SparkStreaming V.S. Flink

大数据之Spark(九):Spark Streaming 概述_第4张图片

 1.6 如何选择一款合适的流式处理框架

  • 对于Storm来说:
    1、建议在需要纯实时,不能忍受1秒以上延迟的场景下使用,比如实时计算系统,要求纯实时进行交易和分析时。
    2、在实时计算的功能中,要求可靠的事务机制和可靠性机制,即数据的处理完全精准,⼀条也不能多,一条也不能少,也可以考虑使用Storm,但是Spark Streaming也可以保证数据的不丢失。
    3、如果我们需要考虑针对⾼峰低峰时间段,动态调整实时计算程序的并⾏度,以最大限度利⽤集群资源(通常是在小型公司,集群资源紧张的情况),我们也可以考虑用Storm
  • 对于Spark Streaming来说:
    1、不满⾜上述3点要求的话,我们可以考虑使⽤Spark Streaming来进⾏实时计算。
    2、考虑使⽤Spark Streaming最主要的⼀个因素,应该是针对整个项⽬进⾏宏观的考虑,即,如果⼀个项目除了实时计算之外,还包括了离线批处理、交互式查询、图计算和MLIB机器学习等业务功能,⽽且实时计算中,可能还会牵扯到⾼延迟批处理、交互式查询等功能,那么就应该⾸选Spark⽣态,⽤Spark Core开发离线批处理,⽤Spark SQL开发交互式查询,⽤Spark Streaming开发实时计算,三者可以⽆缝整合,给系统提供⾮常⾼的可扩展性。
  • 对于Flink来说:
    ⽀持⾼吞吐、低延迟、⾼性能的流处理
    ⽀持带有事件时间的窗⼝(Window)操作
    ⽀持有状态计算的Exactly-once语义
    ⽀持⾼度灵活的窗⼝(Window)操作,支持基于time、count、session,以及data-driven的窗⼝操作
    ⽀持具有Backpressure功能的持续流模型
    ⽀持基于轻量级分布式快照(Snapshot)实现的容错
    ⼀个运⾏时同时⽀持Batch on Streaming处理和Streaming处理
    Flink在JVM内部实现了⾃⼰的内存管理
    ⽀持迭代计算
    ⽀持程序⾃动优化:避免特定情况下Shuffle、排序等昂贵操作,中间结果有必要进⾏缓存

 二、SparkStreaming实时处理入门

 2.1 工程创建

 导入Maven依赖


 org.apache.spark
 spark-streaming_2.11
 2.2.2


 org.apache.spark
 spark-streaming-kafka-0-10_2.11
 2.2.2

 2.2 入口类StreamingContext

 SparkStreaming的入口类为StreamingContext,实际上其底层仍然需要依赖SparkContext。

object _01SparkStreamingWordCountOps {
 def main(args: Array[String]): Unit = {
 /*
 StreamingContext的初始化,需要⾄少两个参数,SparkConf和BatchDuration
 SparkConf不⽤多说
 batchDuration:提交两次作业之间的时间间隔,每次会提交⼀个DStream,将数据转化batch---
>RDD
 所以说:sparkStreaming的计算,就是每隔多⻓时间计算⼀次数据
 */
 val conf = new SparkConf()
 .setAppName("SparkStreamingWordCount")
 .setMaster("local[*]")
 val duration = Seconds(2)
 val ssc = new StreamingContext(conf, duration)
 //业务
 
 
 //为了执⾏的流式计算,必须要调⽤start来启动
 ssc.start()
 //为了不⾄于start启动程序结束,必须要调⽤awaitTermination⽅法等待程序业务完成之后调⽤stop
⽅法结束程序,或者异常
 ssc.awaitTermination()
 }
}

2.3 业务编写

代码实现

object _01SparkStreamingWordCountOps {
 def main(args: Array[String]): Unit = {
 if(args == null || args.length < 2) {
 println(
 """
 |Usage:  
 """.stripMargin)
 System.exit(-1)
 }
 val Array(hostname, port) = args
 /*
 StreamingContext的初始化,需要⾄少两个参数,SparkConf和BatchDuration
 SparkConf不⽤多说
 batchDuration:提交两次作业之间的时间间隔,每次会提交⼀个DStream,将数据转化batch---
>RDD
 所以说:sparkStreaming的计算,就是每隔多⻓时间计算⼀次数据
 */
 val conf = new SparkConf()
 .setAppName("SparkStreamingWordCount")
 .setMaster("local[*]")
 val duration = Seconds(2)
 val ssc = new StreamingContext(conf, duration)
 //接⼊数据
 val lines:ReceiverInputDStream[String] = ssc.socketTextStream(hostname,
port.toInt)
// lines.print()
 val retDStream:DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_,
1)).reduceByKey(_+_)
 retDStream.print()
 //为了执⾏的流式计算,必须要调⽤start来启动
ssc.start()
 //为了不⾄于start启动程序结束,必须要调⽤awaitTermination⽅法等待程序业务完成之后调⽤stop
⽅法结束程序,或者异常
 ssc.awaitTermination()
 }
}

使用netcat进行测试(需要安装)

...

三、SparkStreaming与Kafka整合

3.1 整合简述

kafka是做消息的缓存,数据和业务隔离操作的消息队列,⽽sparkstreaming是⼀款准实时流式计算框架,所以二者的整合,是大势所趋。

二者的整合,主要有两大版本。

大数据之Spark(九):Spark Streaming 概述_第5张图片

 3.2 Direct的方式

 编码

//基于direct⽅式整合kafka
object _03SparkStreamingWithKafkaDirectOps {
 def main(args: Array[String]): Unit = {
 Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
 Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
 Logger.getLogger("org.spark_project").setLevel(Level.WARN)
 val conf = new SparkConf()
 .setAppName("SparkStreamingWithKafkaDirect")
 .setMaster("local[*]")
 val duration = Seconds(2)
 val ssc = new StreamingContext(conf, duration)
 val kafkaParams = Map[String, String](
 "bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
 "group.id" -> "g_1903_2",
 "auto.offset.reset" -> "largest"
 )
 val topics = "spark".split(",").toSet
 val messages: InputDStream[(String, String)] =
KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc,
kafkaParams, topics)
 messages.foreachRDD((rdd, bTime) => {
 if(!rdd.isEmpty()) {
 val offsetRDD = rdd.asInstanceOf[HasOffsetRanges]
 val offsetRanges = offsetRDD.offsetRanges
 for(offsetRange <- offsetRanges) {
 val topic = offsetRange.topic
 val partition = offsetRange.partition
 val fromOffset = offsetRange.fromOffset
 val untilOffset = offsetRange.untilOffset
 
println(s"topic:${topic}\tpartition:${partition}\tstart:${fromOffset}\tend:${until
Offset}")
 }
 rdd.count()
 }
 })
 ssc.start()
 ssc.awaitTermination()
 }
}

说明

  1. 简化的并行性:不需要创建多个输入Kafka流并将其合并。 使用directStream,Spark Streaming将创建与使⽤Kafka分区⼀样多的RDD分区,这些分区将全部从Kafka并⾏读取数据。 所以在Kafka和RDD分区 之间有⼀对⼀的映射关系。
  2. 效率:在第⼀种⽅法中实现零数据丢失需要将数据存储在预写⽇志中,这会进⼀步复制数据。这实际 上是效率低下的,因为数据被有效地复制了两次:⼀次是Kafka,另⼀次是由预先写⼊⽇志(Write Ahead Log)复制。这个第⼆种⽅法消除了这个问题,因为没有接收器,因此不需要预先写⼊⽇志。 只要Kafka数据保留时间⾜够⻓。
  3. 正好一次(Exactly-once)语义:第⼀种⽅法使⽤Kafka的⾼级API来在Zookeeper中存储消耗的偏移 量。传统上这是从Kafka消费数据的⽅式。虽然这种⽅法(结合提前写⼊⽇志)可以确保零数据丢失(即 ⾄少⼀次语义),但是在某些失败情况下,有⼀些记录可能会消费两次。发⽣这种情况是因为Spark Streaming可靠接收到的数据与Zookeeper跟踪的偏移之间的不⼀致。因此,在第⼆种⽅法中,我们使 ⽤不使⽤Zookeeper的简单Kafka API。在其检查点内,Spark Streaming跟踪偏移量。这消除了Spark Streaming和Zookeeper/Kafka之间的不⼀致,因此Spark Streaming每次记录都会在发⽣故障的情况下 有效地收到⼀次。为了实现输出结果的⼀次语义,将数据保存到外部数据存储区的输出操作必须是幂等的,或者是保存结果和偏移量的原⼦事务。

 四、SparkStreaming常见transformation算子

4.1 常见的算子操作

大数据之Spark(九):Spark Streaming 概述_第6张图片

cogroup简要说明:cogroup就是groupByKey的另外⼀种变体,groupByKey是操作⼀个K-V键值对, 而cogroup⼀次操作两个,类似于join,不同之处在于返回值结果:

val ds1:DStream[(K, V)]
val ds2:DStream[(K, w)]
val cg:DStream[(K, (Iterable[V], Iterable[W]))] = ds1.cogroup(ds1)

 4.2 transform

transform是⼀个transformation算⼦,转换算⼦。

DStream上述提供的所有的transformation操作,都是DStream-2-DStream操作,没有⼀个DStream和 RDD的直接操作,⽽DStream本质上是⼀系列RDD,所以RDD-2-RDD操作是显然被需要的,所以此时官⽅api中提 供了⼀个为了达成此操作的算⼦——transform操作。

最经典的实现就是DStream和rdd的join操作,还有dstream重分区(分区减少,coalesce)。

也就是说transform主要就是⽤来⾃定义官⽅api没有提供的⼀些操作。

案例:

动态黑名单过滤

广告计费系统,是电商必不可少的⼀个功能点。为了防⽌恶意的⼴告点击(假设商户A和B同时在某电商做了⼴告,A和B为竞争对⼿,那么如果A使⽤点击机器⼈进⾏对B的⼴告的恶意点击,那么B的⼴告费⽤将很快被⽤完),必须对⼴告点击进⾏⿊名单过滤。⿊名单的过滤可以是ID,可以是IP等等,⿊名单就是过滤的条件,利⽤SparkStreaming的流处理特性,可实现实时⿊名单的过滤实现。可以使⽤leftouter join 对⽬标数据和⿊名单数据进⾏关联,将命中⿊名单的数据过滤掉。

 代码实现

/**
 * 在线⿊名单过滤
 *
 * 类名起名规范
 * ⾸字⺟⼤写,多单词,采⽤驼峰
 * ⼀律名词,不能动词
 * 并且单数不能复数
 * ⽅法名起名规范
 * ⾸字⺟⼩写,多单词,采⽤驼峰
 * ⼀般采⽤动宾短语(动词+名词)
 * 尽量少⽤⼀些汉语拼⾳,中⽂
 *
 * 需求:
 * 从⽤户请求的nginx⽇志中过滤出⿊名单的数据,保留⽩名单数据进⾏后续业务统计。
 * data structure
 * 27.19.74.143##2016-05-30 17:38:20##GET /static/image/common/faq.gif
HTTP/1.1##200##1127
110.52.250.126##2016-05-30 17:38:20##GET /data/cache/style_1_widthauto.css?y7a
HTTP/1.1##200##1292
 */
object _01OnlineBlacklistFilterOps {
 def main(args: Array[String]): Unit = {
 Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
 Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
 Logger.getLogger("org.spark_project").setLevel(Level.WARN)
 val conf = new SparkConf()
 .setAppName("OnlineBlacklistFilter")
.setMaster("local[*]")
 val duration = Seconds(2)
 val ssc = new StreamingContext(conf, duration)
 //⿊名单RDD
 val blacklistRDD:RDD[(String, Boolean)] = ssc.sparkContext.parallelize(List(
 ("27.19.74.143", true),
 ("110.52.250.126", true)
 ))
 //接⼊外部的数据流
 val lines:DStream[String] = ssc.socketTextStream("bigdata01", 9999)
 //⿊名单过滤
// 110.52.250.126##2016-05-30 17:38:20##GET /data/cache/style_1_widthauto.css?
y7a HTTP/1.1##200##1292
 val ip2OtherDStream:DStream[(String, String)] = lines.map(line => {
 val index = line.indexOf("##")
 val ip = line.substring(0, index)
 val other = line.substring(index + 2)
 (ip, other)
 })
 val filteredDStream:DStream[(String, String)] = ip2OtherDStream.transform(rdd
=> {
 val join = rdd.leftOuterJoin(blacklistRDD)
 join.filter{case (ip, (left, right)) => {
 !right.isDefined
 }}.map{case (ip, (left, right)) => {
 (ip, left)
 }}
 })
 filteredDStream.print()
 //重分区
// filteredDStream.transform(_.coalesce(8))
 ssc.start()
 ssc.awaitTermination()
 }
}

4.3 updateStateByKey

updateStateByKey(func) 根据于key的前置状态和key的新值,对key进⾏更新,返回⼀个新状态的Dstream。

统计截⽌到⽬前为⽌key的状态。

通过分析,我们需要清楚:在这个操作中需要两个数据,⼀个是key的前置状态,⼀个是key的新增(当前批次的 数据);还有历史数据(前置状态)得需要存储在磁盘,不应该保存在内存中。

同时key的前置状态可能有可能没有。

案例:wordcount

/**
 * 统计,截⽌到⽬前为⽌出现的每⼀个key的次数
 */
object _02WordCountUpdateStateByKeyOps {
 def main(args: Array[String]): Unit = {
 Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
 Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
 Logger.getLogger("org.spark_project").setLevel(Level.WARN)
 val conf = new SparkConf()
 .setAppName("WordCountUpdateStateByKey")
 .setMaster("local[*]")
 val duration = Seconds(2)
 val ssc = new StreamingContext(conf, duration)
 ssc.checkpoint("file:/E:/data/out/1903/chk")
 val lines:DStream[String] = ssc.socketTextStream("bigdata01", 9999)
 val pairs:DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_, 1))
 val usb:DStream[(String, Int)] = pairs.updateStateByKey(updateFunc)
 usb.print()
 ssc.start()
 ssc.awaitTermination()
 }
 /*
 状态更新函数
 根据key的前置状态和key的最新值,聚合得到截⽌到⽬前为⽌key的状态
 seq:为当前key的状态
 option为key对应的历史值
 */
 def updateFunc(seq: Seq[Int], option: Option[Int]): Option[Int] = {
 println("option:" + option + "> seq: " + seq.mkString("[", ",", "]"))
// var sum = 0
// for(i <- seq) sum += i
// if(option.isDefined) {
// sum += option.get
// }
// Option(sum)
 Option(seq.sum + option.getOrElse(0))
 }
}

4.4 window

window操作就是窗口函数。Spark Streaming提供了滑动窗⼝操作的⽀持,从⽽让我们可以对⼀个滑动窗⼝内 的数据执⾏计算操作。每次掉落在窗⼝内的RDD的数据,会被聚合起来执⾏计算操作,然后⽣成的RDD,会作为 window DStream的⼀个RDD。⽐如下图中,就是对每三秒钟的数据执⾏⼀次滑动窗⼝计算,这3秒内的3个RDD会 被聚合起来进⾏处理,然后过了两秒钟,⼜会对最近三秒内的数据执⾏滑动窗⼝计算。所以每个滑动窗⼝操作,都 必须指定两个参数,窗⼝⻓度以及滑动间隔,⽽且这两个参数值都必须是batch间隔的整数倍。

大数据之Spark(九):Spark Streaming 概述_第7张图片

  1. 红色的矩形就是一个窗口,窗口hold的是⼀段时间内的数据流。
  2. 这⾥⾯每⼀个time都是时间单元,在官⽅的例⼦中,每隔window size是3 time unit, ⽽且每隔2个单位时间, 窗⼝会slide⼀次。所以基于窗⼝的操作,需要指定2个参数:
    window length - The duration of the window (3 in the figure)
    slide interval - The interval at which the window-based operation is performed (2 in the figure).
  3. 窗口大小,一段时间内数据的容器。
  4. 滑动间隔,可以理解为cron表达式
  • /**
     * 统计,截⽌到⽬前为⽌出现的每⼀个key的次数
     * window窗⼝操作,每个多⻓M时间,通过过往N⻓时间内产⽣的数据
     * M就是滑动⻓度sliding interval
     * N就是窗⼝⻓度window length
     */
    object _03WordCountWindowsOps {
     def main(args: Array[String]): Unit = {
     Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
     Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
     Logger.getLogger("org.spark_project").setLevel(Level.WARN)
     val conf = new SparkConf()
     .setAppName("WordCountUpdateStateByKey")
     .setMaster("local[*]")
     val batchInterval = 2
     val duration = Seconds(batchInterval)
     val ssc = new StreamingContext(conf, duration)
    val lines:DStream[String] = ssc.socketTextStream("bigdata01", 9999)
     val pairs:DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_, 1))
     val ret:DStream[(String, Int)] = pairs.reduceByKeyAndWindow(_+_,
     windowDuration = Seconds(batchInterval * 3),
     slideDuration = Seconds(batchInterval * 2))
     ret.print()
     ssc.start()
     ssc.awaitTermination()
     }
     /*
     状态更新函数
     根据key的前置状态和key的最新值,聚合得到截⽌到⽬前为⽌key的状态
     seq:为当前key的状态
     option为key对应的历史值
     */
     def updateFunc(seq: Seq[Int], option: Option[Int]): Option[Int] = {
     println("option:" + option + "> seq: " + seq.mkString("[", ",", "]"))
    // var sum = 0
    // for(i <- seq) sum += i
    // if(option.isDefined) {
    // sum += option.get
    // }
    // Option(sum)
     Option(seq.sum + option.getOrElse(0))
     }
    }

    4.5 SparkSQL和SparkStreaming的整合案例

Spark最强大的地方在于,可以与Spark Core、Spark SQL整合使用,之前已经通过transform、foreachRDD等算子看到,如何将DStream中的RDD使用Spark Core执行批处理操作。现在就来看看,如何将DStream中的RDD 与Spark SQL结合起来使⽤。

案例:top3的商品排序(最新的top3)

这⾥就是基于updatestateByKey,统计截⽌到⽬前为⽌的不同品类下的商品销量top3

/**
 * SparkStreaming整合SparkSQL的案例之,热⻔品类top3排⾏
 */
object _04StreamingIntegerationSQLOps {
 def main(args: Array[String]): Unit = {
 Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
 Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.spark_project").setLevel(Level.WARN)
 val conf = new SparkConf()
 .setAppName("StreamingIntegerationSQL")
 .setMaster("local[*]")
 val batchInterval = 2
 val duration = Seconds(batchInterval)
 val spark = SparkSession.builder()
 .config(conf)
 .getOrCreate()
 val ssc = new StreamingContext(spark.sparkContext, duration)
 ssc.checkpoint("file:/E:/data/out/1903/chk-1")
 val lines:DStream[String] = ssc.socketTextStream("bigdata01", 9999)
 //001 mi moblie
 val pairs:DStream[(String, Int)] = lines.map(line => {
 val fields = line.split("\\s+")
 if(fields == null || fields.length != 3) {
 ("", -1)
 } else {
 val brand = fields(1)
 val category = fields(2)
 (s"${category}_${brand}", 1)
 }
 }).filter(t => t._2 != -1)
 val usb:DStream[(String, Int)] = pairs.updateStateByKey(updateFunc)
 usb.foreachRDD((rdd, bTime) => {
 if(!rdd.isEmpty()) {//category_brand count
 import spark.implicits._
 val df = rdd.map{case (cb, count) => {
 val category = cb.substring(0, cb.indexOf("_"))
 val brand = cb.substring(cb.indexOf("_") + 1)
 (category, brand, count)
 }}.toDF("category", "brand", "sales")
 df.createOrReplaceTempView("tmp_category_brand_sales")
 val sql =
 """
 |select
 | t.category,
 | t.brand,
 | t.sales
 | t.rank
 |from (
 | select
 | category,
 | brand,
 | sales,
| row_number() over(partition by category order by sales desc)
rank
 | from tmp_category_brand_sales
 |) t
 |where t.rank < 4
 """.stripMargin
 spark.sql(sql).show()
 }
 })
 ssc.start()
 ssc.awaitTermination()
 }
 def updateFunc(seq: Seq[Int], option: Option[Int]): Option[Int] = {
 Option(seq.sum + option.getOrElse(0))
 }
}

五、SparkStreaming 优化

5.1 SparkStreaming缓存操作

SparkStreaming的缓存,说白了就是DStream的缓存,DStream的缓存就只有⼀个方面,DStream对应的RDD的缓存,RDD如何缓存?rdd.persist(),所以DStream的缓存其实就是RDD的缓存,使用persist()指定,及其需 要指定持久化策略,大多算子默认情况下,持久化策略为MEMORY_AND_DISK_SER_2。

5.2 SparkStreaming的checkpoint机制

  1. 每⼀个Spark Streaming应⽤,正常来说,都是要7*24⼩时运转的,这就是实时计算程序的特点。因为要持续 不断的对数据进⾏计算。因此,对实时计算应⽤的要求,应该是必须要能够对与应⽤程序逻辑无关的失败,进⾏容错。
  2. 如果要实现这个⽬标,Spark Streaming程序就必须将⾜够的信息checkpoint到容错的存储系统上,从⽽让它 能够从失败中进⾏恢复。有两种数据需要被进⾏checkpoint:
    1)元数据checkpoint——将定义了流式计算逻辑的信息,保存到容错的存储系统上,⽐如HDFS。当运⾏SparkStreaming应⽤程序的Driver进程所在节点失败时,该信息可以⽤于进⾏恢复。元数据信息包括了:
     配置信息——创建Spark Streaming应⽤程序的配置信息,⽐如SparkConf中的信息。
     DStream的操作信息——定义了Spark Stream应⽤程序的计算逻辑的DStream操作信息。
     未处理的batch信息——那些job正在排队,还没处理的batch信息。
    2)、数据checkpoint——将实时计算过程中产⽣的RDD的数据保存到可靠的存储系统中。
    对于⼀些将多个batch的数据进⾏聚合的,有状态的transformation操作,这是⾮常有⽤的。在这种 transformation操作中,⽣成的RDD是依赖于之前的batch的RDD的,这会导致随着时间的推移,RDD的依赖链条 变得越来越⻓。
    要避免由于依赖链条越来越⻓,导致的⼀起变得越来越⻓的失败恢复时间,有状态的transformation操作执⾏过 程中间产⽣的RDD,会定期地被checkpoint到可靠的存储系统上,⽐如HDFS。从⽽削减RDD的依赖链条,进⽽缩 短失败恢复时,RDD的恢复时间。

总结:
元数据checkpoint主要是为了从driver失败中进⾏恢复;⽽RDD checkpoint主要是为了使⽤到有状态的transformation操作时,能够在其⽣产出的数据丢失时,进⾏快速的失败恢复。

5.3 调优建议

5.3.1 设置合理的CPU

很多情况下Streaming程序需要的内存不是很多,但是需要的CPU要很多。在Streaming程序中,CPU资源的使用可以分为两⼤类:

  • ⽤于接收数据;
  • ⽤于处理数据。我们需要设置⾜够的CPU资源,使得有⾜够的CPU资源⽤于接收和处理数据,这样才能及 时⾼效地处理数据。

5.3.2 设置合理的并行度

        如果在计算的任何stage中使⽤的并⾏task的数量没有⾜够多,那么集群资源是⽆法被充分利⽤的。举例来说,对于分布式的reduce操作,⽐如reduceByKey和reduceByKeyAndWindow,默认的并⾏task的数量是由 spark.default.parallelism参数决定的。你可以在reduceByKey等操作中,传⼊第⼆个参数,⼿动指定该操作的并行度,也可以调节全局的spark.default.parallelism参数。

        该参数说的是,对于那些shuffle的⽗RDD的最⼤的分区数据。对于parallelize或者textFile这些输⼊算⼦,因为没有⽗RDD,所以依赖于ClusterManager的配置。如果是local模式,该默认值是local[x]中的x;如果是mesos的细粒度模式,该值为8,其它模式就是Math.max(2, 所有的excutor上的所有的core的总数)。

5.3.3 序列化调优说明

数据序列化造成的系统开销可以由序列化格式的优化来减⼩。在流式计算的场景下,有两种类型的数据需要序列化。

  1. 输⼊数据:默认情况下,接收到的输⼊数据,是存储在Executor的内存中的,使⽤的持久化级别是 StorageLevel.MEMORY_AND_DISK_SER_2。这意味着,数据被序列化为字节从⽽减⼩GC开销,并且会复制以进⾏ executor失败的容错。因此,数据⾸先会存储在内存中,然后在内存不⾜时会溢写到磁盘上,从⽽为流式计算来保 存所有需要的数据。这⾥的序列化有明显的性能开销——Receiver必须反序列化从⽹络接收到的数据,然后再使⽤ Spark的序列化格式序列化数据。
  2. 流式计算操作⽣成的持久化RDD:流式计算操作⽣成的持久化RDD,可能会持久化到内存中。例如,窗⼝操 作默认就会将数据持久化在内存中,因为这些数据后⾯可能会在多个窗⼝中被使⽤,并被处理多次。然⽽,不像 Spark Core的默认持久化级别,StorageLevel.MEMORY_ONLY,流式计算操作⽣成的RDD的默认持久化级别是 StorageLevel.MEMORY_ONLY_SER ,默认就会减⼩GC开销。

上述场景中,使⽤Kryo序列化类库可以减⼩CPU和内存的性能开销。使⽤Kryo时,⼀定要考虑注册⾃定义的 类,并且禁⽤对应引⽤的tracking(spark.kryo.referenceTracking=false 跟踪对同⼀个对象的引⽤情况,这对发现 有循环引⽤或同⼀对象有多个副本的情况是很有⽤的。设置为false可以提⾼性能)。

5.3.4 内存调优

内存调优的另外⼀个⽅⾯是垃圾回收。对于流式应⽤来说,如果要获得低延迟,肯定不想要有因为JVM垃圾回收导 致的⻓时间延迟。有很多参数可以帮助降低内存使⽤和GC开销:

  1. DStream的持久化:正如在“数据序列化调优”⼀节中提到的,输⼊数据和某些操作⽣产的中间RDD,默认持久 化时都会序列化为字节。与⾮序列化的⽅式相⽐,这会降低内存和GC开销。使⽤Kryo序列化机制可以进⼀步减少 内存使⽤和GC开销。进⼀步降低内存使⽤率,可以对数据进⾏压缩,由spark.rdd.compress参数控制(默认 false)。
  2. 清理旧数据:默认情况下,所有输⼊数据和通过DStream transformation操作⽣成的持久化RDD,会⾃动被 清理。Spark Streaming会决定何时清理这些数据,取决于transformation操作类型。例如,你在使⽤窗⼝⻓度为 10分钟内的window操作,Spark会保持10分钟以内的数据,时间过了以后就会清理旧数据。但是在某些特殊场景 下,⽐如Spark SQL和Spark Streaming整合使⽤时,在异步开启的线程中,使⽤Spark SQL针对batch RDD进⾏ 执⾏查询。那么就需要让Spark保存更⻓时间的数据,直到Spark SQL查询结束。可以使⽤streamingContext.remember()⽅法来实现。
  3. CMS垃圾回收器:使用并行的mark-sweep垃圾回收机制,被推荐使用,用来保持GC低开销。虽然并行的GC会降低吞吐量,但是还是建议使用它,来减少batch的处理时间(降低处理过程中的gc开销)。如果要使用,那么要在driver端和executor端都开启。
    在spark-submit中使用--driver-java-options设置;使用spark.executor.extraJavaOptions参数设置。-XX:+UseConcMarkSweepGC。

 5.3.5 背压机制

        设置最大接收速率 - 如果集群资源不够多,streaming 应用程序能够像接收到的那样快速处理数据,则可以通过设置记录 /秒的最大速率限制来对 receiver 进行速率限制。
详细内容请参阅 receiver 的 spark.streaming.receiver.maxRate 和用于 Direct Kafka 方法的spark.streaming.kafka.maxRatePerPartition 的配置参数。
        Spark 1.5中,引入了⼀个称为背压的功能,无需设置此速率限制,因为Spark Streaming会自动计算速率限制,并在处理条件发生变化时动态调整速率限制。可通过将配置参数 spark.streaming.backpressure.enabled 设置为 true 来启用此 backpressure。
        这样就可以解决数据积压和Job等待问题,动态感知数据量的大小,并动态调节Spark每个批次处理的数据量。

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