spark-4 Spark Streaming

2 篇文章 0 订阅

四、Spark Streaming

Spark Streaming是Spark Core扩展而来的一个高吞吐、高容错的实时处理引擎,支持可伸缩、高吞吐量、容错的实时数据流处理。数据可以从许多来源获取,如 Kafka、Flume、Kinesis 或 TCP sockets,可以使用复杂的算法处理数据,这些算法用高级函数表示,如 map、reduce、join 和 window。最后,处理后的数据可以推送到文件系统、数据库和活动仪表板。实际上,还可以将 Spark 的 MLlib 机器学习和 GraphX 图形处理算法应用于数据流。典型的处理流程如图4.1所示。

在这里插入图片描述
图4.1 Spark Streaming处理的流程

4.1 Spark Streaming与Storm

Spark Streaming 与Storm都可以用于进行实时流计算。但是他们两者的区别是非常大的。其中区别之一就是,Spark Streaming 和Storm的计算模型完全不一样,Spark Streaming是基于RDD的,因此需要将一小段时间内的,比如1秒内的数据,收集起来,作为一个RDD,然后再针对这个batch的数据进行处理。而Storm却可以做到每来一条数据,都可以立即进行处理和计算。因此,Spark Streaming实际上严格意义上来说,只能称作准实时的流计算框架;而Storm是真正意义上的实时计算框架。

此外,Storm支持的一项高级特性,是Spark Streaming暂时不具备的,即Storm支持分布式流式计算程序(Topolopy)。在运行过程中,可以动态地调整并行度,从而动态提高并发处理能力。而Spark Streaming是无法动态调整并行度的。

但是Spark Streaming也有其优点,首先Spark Streaming由于是基于batch进行处理的,因此相较于Storm基于单条数据进行处理,具有数倍甚至数十倍的吞吐量。此外,Spark Streaming由于也身处于Spark生态圈内,因此Spark Streaming可以与Spark Core、Spark SQL,甚至是Spark Mllib、Spark GraphX进行无缝整合。流式处理完的数据,可以立即进行各种map、reduce转换操作,可以立即使用sql进行查询,甚至可以立即使用machine learning或者图计算算法进行处理。这种一站式的大数据处理功能和优势,是Storm无法匹敌的。

因此,通常在对实时性要求特别高,而且实时数据量不稳定,比如在白天有高峰期的情况下,可以选择使用Storm。但是如果是对实时性要求一般,允许1秒的准实时处理,而且不要求动态调整并行度的话,选择Spark Streaming是更好的选择。

4.2 Streaming 原理

Spark Streaming提供了称为离散流或DStream的高级抽象,它表示连续的数据流,在内部DStream表示为RDD序列,每个RDD包含一定间隔的数据。所有对于DStream的操作都会相应地转换成对RDD的操作。

编写Spark Streaming程序的基本步骤是:
(1)通过创建输入DStream来定义输入源;
(2)通过对DStream应用转换操作和输出操作来定义流计算;
(3)用streamingContext.start()来开始接收数据和处理流程;
(4)通过streamingContext.awaitTermination()方法来等待处理结束(手动结束或因为错误而结束);
(5)可以通过streamingContext.stop()来手动结束流计算进程。

4.3 创建StreamingContext

DStream的创建依赖于StreamingContext对象,有两种创建StreamingContext的方式:通过SparkContext创建和通过SparkConf创建;
(1)Spark conf创建:

val conf = new SparkConf().setAppName(appName).setMaster(master); 
val ssc = new StreamingContext(conf, Seconds(1));

appName是用来在Spark UI上显示的应用名称。master是Spark、Mesos或Yarn集群的URL,或者是local[*]。batch interval可以根据你的应用程序的延迟要求以及可用的集群资源情况来设置。
(2)通过SparkContext创建:

val sc = new SparkContext(conf) 
val ssc = new StreamingContext(sc, Seconds(1))

4.4 输入DStream和Receiver

从源得到的输入DStream对应一个接收器对象,可以从源接收消息并存储到Spark内存中进行处理。Spark Streaming提供两种streaming源:
(1)基础源:直接可以使用streaming上下文API的源,比如files和socket;
(2)高级源:通过引用额外实体类得到的Kafka、Flume源。可以在应用中创建使用多个输入DStreams来实现同时读取多种数据流。

worker/executor 是持久运行的任务,因此它将占用一个分给该应用的core,因此Spark Streaming需要分配足够的core去运行接收器和处理接收的数据。在本地运行Spark Streaming程序时,不要使用“local”或“local[1]”作为主URL。这两者中的任何一个都意味着在本地运行任务只使用一个线程。如果使用基于receiver的输入DStream(如Kafka、Flume等),这表明将使用单个线程运行receiver,而不留下用于处理所接收数据的线程。因此在本地运行时,始终使用“local[n]”作为主URL,其中n必须大于运行的receiver数量,否则系统将接收数据,但不能处理它。

Kafka和Flume这类源需要外部依赖包,其中一些库具有复杂的依赖关系,Spark shell中没有这些高级源代码,因此无法在spark-shell中测试基于这些高级源代码的应用程序,但可以手动将包引入。

基于可靠性的考虑,可以将数据源分为两类:可靠的接收器的数据被Receiver 接收后发送确认到源头(如Kafka、Flume),并将数据存储在spark,不可靠的接收器不会向源发送确认。

4.5 DStream的操作

4.5.1 DStream转换

与RDD类似,转换操作允许修改来自输入DStream的数据,转换操作包括无状态转换操作和有状态转换操作。
无状态转换操作实例:在“套接字流”词频统计采用无状态转换,每次统计都只统计当前批次到达的单词的词频,和之前批次无关,不会进行累计。
有状态转换操作实例:滑动窗口转换操作和updateStateByKey操作。
一些常见的转换(类似于RDD的transformation操作)如下:
(1)窗口操作
每次窗口在源DStream上滑动,窗口内的源RDD被组合/操作生成了窗口RDD。任何窗口操作都需要指定两个参数:窗口长度:窗口的持续时间(代码中值是30);滑动间隔:执行窗口操作的间隔(代码中值是10)。这两个参数必须是源DStream的批处理间隔的倍数。
举例说明窗口操作:希望通过每隔10秒在最近30秒数据中生成字数来扩展前面的示例。为此,我们必须在最近的30秒数据上对(word,1)的DStream键值对应用reduceByKey操作。这是使用reduceByKeyAndWindow操作完成的。

val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))

所有的滑动窗口操作都需要使用参数:windowLength(窗口长度)和slideInterval(滑动间隔),常见窗口操作总结如下:
Window:基于源DStream产生的窗口化的批数据计算得到新的Dstream。
countByWindow: 返回DStream中元素的滑动窗口计数。
reduceByWindow:返回一个单元素流。利用函数func聚集滑动时间间隔的流的元素创建这个单元素流。函数func必须满足结合律从而支持并行计算。
reduceByKeyAndWindow(三参数):应用到一个(K,V)键值对组成的DStream上时,会返回一个由(K,V)键值对组成的新的DStream。每一个key的值均由给定的reduce函数(func函数)进行聚合计算。注意:在默认情况下,这个算子利用了Spark默认的并发任务数去分组。可以通过numTasks参数的设置来指定不同的任务数。
reduceByKeyAndWindow(四参数):比上述reduceByKeyAndWindow(三参数)更高效的reduceByKeyAndWindow,每个窗口的reduce值是基于先前窗口的reduce值进行增量计算得到的;它会对进入滑动窗口的新数据进行reduce操作,并对离开窗口的老数据进行“逆向reduce”操作。但是,只能用于“可逆reduce函数”,即那些reduce函数都有一个对应的“逆向reduce函数”(以InvFunc参数传入)。
countByValueAndWindow:当应用到一个(K,V)键值对组成的DStream上,返回一个由(K,V)键值对组成的新的DStream。每个key的值都是它们在滑动窗口中出现的频率。
updateStateByKey:需要在跨批次之间维护状态时,必须使用updateStateByKey操作。

(2)多流关联
窗口计算上join操作非常有用,在Spark Streaming中可以轻松实现不同类型的join,包括leftouterjoin、rightouterjoin和fullouterjoin。每个批处理间隔中stream1生成的RDD与stream2生成的RDD关联如下:

val stream1: DStream[String, String] = ... 
val stream2: DStream[String, String] = ... 
val joinedStream = stream1.join(stream2)

4.5.2 Dstream的输出

输出操作允许将DStream的数据推送到外部系统,如数据库或files,由于输出操作触发所有DStream转换的实际执行(类似于RDD的action操作),并允许外部系统使用转换后的数据,输出操作如图4.2所示有以下几种:
在这里插入图片描述
图4.2 Dstream的输出操作

4.6 DataFrame和SQL操作

可以轻松地对流数据使用DataFrame和SQL操作,但必须使用StreamingContext将正在使用的SparkContext创建为SparkSession。下面例子使用DataFrame和SQL生成单词计数。每个RDD都转换为DataFrame,注册为临时表后使用SQL进行查询:

val words: DStream[String] = words.foreachRDD { rdd => 
val spark = SparkSession.builder.config(rdd.sparkContext.getConf).getOrCreate() 
import spark.implicits._ 
val wordsDataFrame = rdd.toDF("word") wordsDataFrame.createOrReplaceTempView("words") 
val wordCountsDataFrame = spark.sql("select word, count(*) as total from words group by word") wordCountsDataFrame.show() }

4.7 流处理

4.7.1 Spark-shell流处理

进入spark-shell后就默认获得了的SparkConext,即sc,从SparkConf对象创建StreamingContext对象,spark-shell中创建StreamingContext对象如下:

import org.apache.spark.streaming._
val ssc = new StreamingContext(sc, Seconds(1))

如果是编写一个独立的Spark Streaming程序,而不是在spark-shell中运行,则需要通过如下方式创建StreamingContext对象:

import org.apache.spark._ 
import org.apache.spark.streaming._ 
val conf = new SparkConf().setAppName("TestDStream").setMaster("local[2]") 
val ssc = new StreamingContext(conf, Seconds(1))

4.7.2 文件流

文件流可以读取本机文件,也可以读取读取HDFS上文件,如果部署在yarn模式的Spark,则启动spark-shell默认读取HDFS上对应的: hdfs:xxxx/user/xx/ 下的文件;

import org.apache.spark.streaming._ scala> 
val ssc = new StreamingContext(sc, Seconds(5))
val lines = ssc.textFileStream("hdfs://xxx/yzg_test.txt")
val Counts = lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_ + _)
 Counts.saveAsTextFiles("hdfs://xxx/bendi"))
ssc.start()
ssc.awaitTermination()
ssc.stop()

以上代码在spark-shell中运行后,每隔5秒读取hdfs上的文件并进行词频统计后写入到hdfs中的“bendi-时间戳”文件夹下,直到ssc.stop();Counts.saveAsTextFiles(“file://xxx/bendi”))和Counts.print分别写本地和std输出。

4.7.3 Socket套接字流

Spark Streaming可以通过Socket端口实时监听并接收数据计算,步骤如下:
(1)driver端创建StreamingContext对象,启动上下文时依次创建JobScheduler和ReceiverTracker,并调用他们的start方法;
(2)ReceiverTracker在start方法中发送启动接收器消息给远程Executor,消息内部含有ServerSocket的地址信息;
(3)在executor一侧,由Receiver TrackerEndpoint终端接受消息,抽取消息内容,利用sparkContext结合消息内容创建ReceiverRDD对象,最后提交rdd给spark集群。
在代码实现上,使用nc –lk 9999 开启地址172.22.241.184主机的9999监听端口,并持续往里面写数据;使用spark-shell实现监听端口代码如下,输入源为socket源,进行简单的词频统计后,统计结果输出到HDFS文件系统。

import org.apache.spark._ scala> 
import org.apache.spark.streaming._ scala> 
import org.apache.spark.storage.StorageLevel
val ssc = new StreamingContext(sc, Seconds(5))
val lines = ssc.socketTextStream("172.22.241.184", 9999, StorageLevel.MEMORY_AND_DISK_SER)
val wordCounts = lines.flatMap(_.split(" ")).map((_,1)).reduceByKey(_ + _)
wordCounts.saveAsTextFiles("hdfs://xxx/bendi-socket"))
ssc.start()
ssc.awaitTermination()
ssc.stop()

4.7.4 Kafka流(窗口)

Kafka和Flume等高级输入源需要依赖独立的库(jar文件),如果使用spark-shell读取kafka等高级输入源,需要将对应的依赖jar包放在spark的依赖文件夹lib下。根据当前使用的kafka版本,适配所需要的spark-streaming-kafka依赖的版本,在maven仓库下载。将对应的依赖jar包放在CDH的spark的依赖文件夹lib下,通过引入包内依赖验证是否成功:

import org.apache.spark._
import org.apache.spark.streaming._
import org.apache.spark.streaming.kafka._
val ssc = new StreamingContext(sc, Seconds(5))
ssc.checkpoint("hdfs://usr/spark/kafka/checkpoint")
val zkQuorum = "172.22.241.186:2181"
val group = "test-consumer-group"
val topics = "yzg_spark"
val numThreads = 1
val topicMap =topics.split(",").map((_,numThreads.toInt)).toMap
val lineMap = KafkaUtils.createStream(ssc,zkQuorum,group,topicMap)
val pair = lineMap.map(_._2).flatMap(_.split(" ")).map((_,1))
val wordCounts = pair.reduceByKeyAndWindow(_ + _,_ -_,Minutes(2),Seconds(10),2)
wordCounts.print
ssc.start

4.8 updateStateByKey操作

当Spark Streaming需要跨批次间维护状态时,就必须使用updateStateByKey操作。以词频统计为例,对于有状态转换操作而言,当前批次的词频统计是在之前批次的词频统计结果的基础上进行不断累加,所以最终统计得到的词频是所有批次的单词总的词频统计结果。

val updateFunc = (values: Seq[Int], state: Option[Int]) => {
val currentCount = values.foldLeft(0)(_ + _) 
val previousCount = state.getOrElse(0) 
Some(currentCount + previousCount) }

实现:

import org.apache.spark._ 
import org.apache.spark.streaming._ 
import org.apache.spark.storage.StorageLevel 
val ssc = new StreamingContext(sc, Seconds(5)) ssc.checkpoint("hdfs:172.22.241.184:8020//usr/spark/checkpoint") 
val lines = ssc.socketTextStream("172.22.241.184", 9999, StorageLevel.MEMORY_AND_DISK_SER) 
val wordCounts = lines.flatMap(_.split(" ")).map((_,1)).updateStateByKey[Int](updateFunc) 
wordCounts.saveAsTextFiles("hdfs:172.22.241.184:8020//user/spark/bendi-socket") 
ssc.start() 
ssc.awaitTermination() 
ssc.stop()
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值