Spark Streaming

1. 课程目标

1.1. 掌握Spark Streaming的原理

1.2. 熟练使用Spark Streaming完成流式计算任务

2. Spark Streaming介绍

2.1. Spark Streaming概述

2.1.1. 什么是Spark Streaming

 

Spark Streaming类似于Apache Storm,用于流式数据的处理。根据其官方文档介绍,Spark Streaming有高吞吐量和容错能力强等特点。Spark Streaming支持的数据输入源很多,例如:KafkaFlumeTwitterZeroMQ和简单的TCP套接字等等。数据输入后可以用Spark的高度抽象原语如:mapreducejoinwindow等进行运算。而结果也能保存在很多地方,如HDFS,数据库等。另外Spark Streaming也能和MLlib(机器学习)以及Graphx完美融合。

 

2.1.2. 为什么要学习Spark Streaming

1.易用

2.容错

3.易整合到Spark体系

 

2.1.3. SparkStorm的对比

Spark

Storm

 

 

开发语言:Scala

开发语言:Clojure

编程模型:DStream

编程模型:Spout/Bolt

 

 

 

3. DStream

3.1. 什么是DStream

Discretized StreamSpark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据,如下图:

 

对数据的操作也是按照RDD为单位来进行的

 

计算过程由Spark engine来完成

 

3.2. DStream相关操作

DStream上的原语与RDD的类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()transform()以及各种Window相关的原语。

 

3.2.1. Transformations on DStreams



Transformation算子 用途
map(func)返回会一个新的DStream,并将源DStream中每个元素通过func映射为新的元素
flatMap(func)和map类似,不过每个输入元素不再是映射为一个输出,而是映射为0到多个输出
filter(func)返回一个新的DStream,并包含源DStream中被func选中(func返回true)的元素
repartition(numPartitions)更改DStream的并行度(增加或减少分区数)
union(otherStream)返回新的DStream,包含源DStream和otherDStream元素的并集
count()返回一个包含单元素RDDs的DStream,其中每个元素是源DStream中各个RDD中的元素个数
reduce(func)返回一个包含单元素RDDs的DStream,其中每个元素是通过源RDD中各个RDD的元素经func(func输入两个参数并返回一个同类型结果数据)聚合得到的结果。func必须满足结合律,以便支持并行计算。
countByValue()如果源DStream包含的元素类型为K,那么该算子返回新的DStream包含元素为(K, Long)键值对,其中K为源DStream各个元素,而Long为该元素出现的次数。
reduceByKey(func, [numTasks])如果源DStream 包含的元素为 (K, V) 键值对,则该算子返回一个新的也包含(K, V)键值对的DStream,其中V是由func聚合得到的。注意:默认情况下,该算子使用Spark的默认并发任务数(本地模式为2,集群模式下由spark.default.parallelism 决定)。你可以通过可选参数numTasks来指定并发任务个数。
join(otherStream, [numTasks])如果源DStream包含元素为(K, V),同时otherDStream包含元素为(K, W)键值对,则该算子返回一个新的DStream,其中源DStream和otherDStream中每个K都对应一个 (K, (V, W))键值对元素。
cogroup(otherStream, [numTasks])如果源DStream包含元素为(K, V),同时otherDStream包含元素为(K, W)键值对,则该算子返回一个新的DStream,其中每个元素类型为包含(K, Seq[V], Seq[W])的tuple。
transform(func)返回一个新的DStream,其包含的RDD为源RDD经过func操作后得到的结果。利用该算子可以对DStream施加任意的操作。
updateStateByKey(func)返回一个包含新”状态”的DStream。源DStream中每个key及其对应的values会作为func的输入,而func可以用于对每个key的“状态”数据作任意的更新操作。
 

 

特殊的Transformations

 

1.UpdateStateByKey Operation

UpdateStateByKey原语用于记录历史记录,上文中Word Count示例中就用到了该特性。若不用UpdateStateByKey来更新状态,那么每次数据进来后分析完成后,结果输出后将不在保存

 

2.Transform Operation

Transform原语允许DStream上执行任意的RDD-to-RDD函数。通过该函数可以方便的扩展Spark API。此外,MLlib(机器学习)以及Graphx也是通过本函数来进行结合的。

 

3.Window Operations

Window Operations有点类似于Storm中的State,可以设置窗口的大小和滑动窗口的间隔来动态的获取当前Steaming的允许状态

这里列出一些基于窗口计算的算子


Transformation窗口算子 用途
window(windowLengthslideInterval)将源DStream窗口化,并返回转化后的DStream
countByWindow(windowLength,slideInterval)返回数据流在一个滑动窗口内的元素个数
reduceByWindow(funcwindowLength,slideInterval)基于数据流在一个滑动窗口内的元素,用func做聚合,返回一个单元素数据流。func必须满足结合律,以便支持并行计算。
reduceByKeyAndWindow(func,windowLengthslideInterval, [numTasks])基于(K, V)键值对DStream,将一个滑动窗口内的数据进行聚合,返回一个新的包含(K,V)键值对的DStream,其中每个value都是各个key经过func聚合后的结果。
注意:如果不指定numTasks,其值将使用Spark的默认并行任务数(本地模式下为2,集群模式下由 spark.default.parallelism决定)。当然,你也可以通过numTasks来指定任务个数。
reduceByKeyAndWindow(funcinvFunc,windowLength,slideInterval, [numTasks])和前面的reduceByKeyAndWindow() 类似,只是这个版本会用之前滑动窗口计算结果,递增地计算每个窗口的归约结果。当新的数据进入窗口时,这些values会被输入func做归约计算,而这些数据离开窗口时,对应的这些values又会被输入 invFunc 做”反归约”计算。举个简单的例子,就是把新进入窗口数据中各个单词个数“增加”到各个单词统计结果上,同时把离开窗口数据中各个单词的统计个数从相应的统计结果中“减掉”。不过,你的自己定义好”反归约”函数,即:该算子不仅有归约函数(见参数func),还得有一个对应的”反归约”函数(见参数中的 invFunc)。和前面的reduceByKeyAndWindow() 类似,该算子也有一个可选参数numTasks来指定并行任务数。注意,这个算子需要配置好检查点(checkpointing)才能用。
countByValueAndWindow(windowLength,slideInterval, [numTasks])基于包含(K, V)键值对的DStream,返回新的包含(K, Long)键值对的DStream。其中的Long value都是滑动窗口内key出现次数的计数。
和前面的reduceByKeyAndWindow() 类似,该算子也有一个可选参数numTasks来指定并行任务数。

 

3.2.2. Output Operations on DStreams

Output Operations可以将DStream的数据输出到外部的数据库或文件系统,当某个Output Operations原语被调用时(与RDDAction相同),streaming程序才会开始真正的计

输出算子 用途
print()在驱动器(driver)节点上打印DStream每个批次中的头十个元素。
Python API 对应的Python API为 pprint()
saveAsTextFiles(prefix, [suffix])将DStream的内容保存到文本文件。
每个批次一个文件,各文件命名规则为 “prefix-TIME_IN_MS[.suffix]”
saveAsObjectFiles(prefix, [suffix])将DStream内容以序列化Java对象的形式保存到顺序文件中。
每个批次一个文件,各文件命名规则为 “prefix-TIME_IN_MS[.suffix]”Python API 暂不支持Python
saveAsHadoopFiles(prefix, [suffix])将DStream内容保存到Hadoop文件中。
每个批次一个文件,各文件命名规则为 “prefix-TIME_IN_MS[.suffix]”Python API 暂不支持Python
foreachRDD(func)这是最通用的输出算子了,该算子接收一个函数func,func将作用于DStream的每个RDD上。
func应该实现将每个RDD的数据推到外部系统中,比如:保存到文件或者写到数据库中。
注意,func函数是在streaming应用的驱动器进程中执行的,所以如果其中包含RDD的action算子,就会触发对DStream中RDDs的实际计算过程。
算过程。


 

4. 实战

4.1. Spark Streaming实现实时WordCount

架构图:

 

1.安装并启动生成者

首先在一台Linuxip192.168.10.101())上用YUM安装nc工具

yum install -y nc

 

启动一个服务端并监听9999端口

nc -lk 9999

 

2.编写Spark Streaming程序

package org.spark.streaming
import cn.itcast.spark.util.LoggerLevel
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object NetworkWordCount {
  def main(args: Array[String]) {
    //设置日志级别
    
LoggerLevel.setStreamingLogLevels()
    //创建SparkConf并设置为本地模式运行
    //注意local[2]代表开两个线程
    
valconf =new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
    //设置DStream批次时间间隔为2秒
    
valssc =new StreamingContext(conf,Seconds(2))
    //通过网络读取数据
    
vallines = ssc.socketTextStream("192.168.10.101",9999)
    //将读到的数据用空格切成单词
    
valwords = lines.flatMap(_.split(" "))
    //将单词和1组成一个pair
    
valpairs = words.map(word => (word,1))
    //按单词进行分组求相同单词出现的次数
    
valwordCounts = pairs.reduceByKey(_ + _)
    //打印结果到控制台
    
wordCounts.print()
    //开始计算
    
ssc.start()
    //等待停止
    
ssc.awaitTermination()
  }
}

 

3.启动Spark Streaming程序:由于使用的是本地模式"local[2]"所以可以直接在本地运行该程序

注意:要指定并行度,如在本地运行设置setMaster("local[2]"),相当于启动两个线程,一个给receiver,一个给computer。如果是在集群中运行,必须要求集群中可用core数大于1

 

 

4.Linux端命令行中输入单词

 #nc -lk 9999

hello tom hello jerry  hello tom

5.IDEA控制台中查看结果

 

问题:结果每次在Linux段输入的单词次数都被正确的统计出来,但是结果不能累加!如果需要累加需要使用updateStateByKey(func)来更新状态,下面给出一个例子:

package cn.itcast.spark.streaming
import cn.itcast.spark.util.LoggerLevel
import org.apache.spark.{HashPartitioner, SparkConf}
import org.apache.spark.streaming.{StreamingContext, Seconds}
object NetworkUpdateStateWordCount {
  /**
    * String : 单词 hello
    * Seq[Int] :单词在当前批次出现的次数
    * Option[Int] : 历史结果
    */
  
valupdateFunc= (iter:Iterator[(String,Seq[Int], Option[Int])]) => {
    //iter.flatMap(it=>Some(it._2.sum + it._3.getOrElse(0)).map(x=>(it._1,x)))
    
iter.flatMap{case(x,y,z)=>Some(y.sum + z.getOrElse(0)).map(m=>(x, m))}
  }

  def main(args: Array[String]) {
    LoggerLevel.setStreamingLogLevels()
    val conf = newSparkConf().setMaster("local[2]").setAppName("NetworkUpdateStateWordCount")
    val ssc = newStreamingContext(conf,Seconds(5))
    //做checkpoint 写入共享存储中
    
ssc.checkpoint("c://aaa")
    val lines = ssc.socketTextStream("192.168.10.100",9999)
    //reduceByKey 结果不累加
    //val result = lines.flatMap(_.split(" ")).map((_, 1)).reduceByKey(_+_)
    //updateStateByKey结果可以累加但是需要传入一个自定义的累加函数:updateFunc
    
valresults = lines.flatMap(_.split(" ")).map((_,1)).updateStateByKey(updateFunc,newHashPartitioner(ssc.sparkContext.defaultParallelism),true)
    results.print()
    ssc.start()
    ssc.awaitTermination()
  }
}

 

4.2. Spark Streaming整合Kafka完成网站点击流实时统计

   Apache Kafka是一个分布式的消息发布-订阅系统。可以说,在任何实时大数据处理工具缺少与Kafka整合都是不完善的。使用Spark Streaming从Kafka中接受数据,这里会介绍两种方法:(1)使用Receivers和Kafka高层次的API;(2)【spark2.0版本以后最常用】使用Direct API,这是使用低层次的Kafka API,并没有使用到Receivers

1.安装并配置zk

2.安装并配置Kafka

3.启动zk

4.启动Kafka

5.创建topic

bin/kafka-topics.sh --create --zookeeper node1.itcast.cn:2181,node2.itcast.cn:2181 \

--replication-factor 3 --partitions 3 --topic urlcount

6.编写基于Receivers的Spark Streaming应用程序

package org.spark.streaming
import org.apache.spark.{HashPartitioner, SparkConf}
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}

objectKafkaWordCount {
  defmain(args:Array[String]) {
    if(args.length < 4) {
      System.err.println("Usage: KafkaWordCount <zkQuorum> <group> <topics> <numThreads>")
      System.exit(1)
    }
    StreamingExamples.setStreamingLogLevels() //需要编写设置打印日志级别

    valArray(zkQuorum, group, topics, numThreads) =args
    valsparkConf=newSparkConf().setAppName("KafkaWordCount")
    valssc= newStreamingContext(sparkConf, Seconds(2))
    ssc.checkpoint("checkpoint")

    valtopicMap=topics.split(",").map((_,numThreads.toInt)).toMap
    vallines=KafkaUtils.createStream(ssc, zkQuorum, group, topicMap).map(_._2)
    valwords=lines.flatMap(_.split(" "))
    valwordCounts=words.map(x=> (x, 1L))
      .reduceByKeyAndWindow(_+_,_-_, Minutes(10), Seconds(2),2)
    wordCounts.print()
 
    ssc.start()
    ssc.awaitTermination()
  }
}


7.编写基于Direct API的Spark Streaming应用程序


import org.apache.spark.SparkConf
import org.apache.spark.streaming._
import org.apache.spark.streaming.kafka010._

object DirectKafkaWordCount{
  def main(args: Array[String]): Unit = {
    if (args.length < 2) {
      System.err.println(s"""
                       |Usage: DirectKafkaWordCount <brokers> <topics>
                       |  <brokers> is a list of one or more Kafka brokers
                       |  <topics> is a list of one or more kafka topics to consume from
                       |
        """.stripMargin)
      System.exit(1)
    }
    StreamingExamples.setStreamingLogLevels()

    val Array(brokers, topics) = args

    // Create context with 2 second batch interval
    val sparkConf = new SparkConf().setAppName("DirectKafkaWordCount")
    val ssc = new StreamingContext(sparkConf, Seconds(2))

    // Create direct kafka stream with brokers and topics
    val topicsSet = topics.split(",").toSet
    val kafkaParams = Map[String, String]("metadata.broker.list" -> brokers)
    val messages = KafkaUtils.createDirectStream[String, String](
      ssc,
      LocationStrategies.PreferConsistent,
      ConsumerStrategies.Subscribe[String, String](topicsSet, kafkaParams))

    // Get the lines, split them into words, count the words and print
    val lines = messages.map(_.value)
    val words = lines.flatMap(_.split(" "))
    val wordCounts = words.map(x => (x, 1L)).reduceByKey(_ + _)
    wordCounts.print()

    // Start the computation
    ssc.start()
    ssc.awaitTermination()
  }
}

8.Direct API方式与Receivers相比,有以下优点:

(1)、简化并行我们不需要创建多个Kafka 输入流,然后union他们。而使用directStream,Spark Streaming将会创建和Kafka分区一样的RDD分区个数,而且会从Kafka并行地读取数据,也就是说Spark分区将会和Kafka分区有一一对应的关系,这对我们来说很容易理解和使用;

  (2)、高效第一种实现零数据丢失是通过将数据预先保存在WAL中,这将会复制一遍数据,这种方式实际上很不高效,因为这导致了数据被拷贝两次:一次是被Kafka复制;另一次是写到WAL中。但是本文介绍的方法因为没有Receiver,从而消除了这个问题,所以不需要WAL日志;

  (3)、恰好一次语义(Exactly-once semantics)。Receivers方式通过使用Kafka高层次的API把偏移量写入Zookeeper中,这是读取Kafka中数据的传统方法。虽然这种方法可以保证零数据丢失,但是还是存在一些情况导致数据会丢失,因为在失败情况下通过Spark Streaming读取偏移量和Zookeeper中存储的偏移量可能不一致。而本文提到的方法是通过Kafka低层次的API,并没有使用到Zookeeper,偏移量仅仅被Spark Streaming保存在Checkpoint中。这就消除了Spark Streaming和Zookeeper中偏移量的不一致,而且可以保证每个记录仅仅被Spark Streaming读取一次,即使是出现故障。

有关sparkStreaming的详细代码详见(github)https://github.com/xiaoxiaobaixiong/sparkStreamingLearning
官方sparkStreaming 2.2.0文档http://spark.apache.org/docs/latest/streaming-programming-guide.html
Spark Streaming编程指南http://ifeve.com/spark-streaming-2/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值