Spark Streaming【数据流处理原理分析】

Spark Streaming介绍

Spark Streaming它是对Spark核心API的扩展,目的在于对实时数据流进行高吞吐、高容错的处理。Spark Streaming底层是Spark Core。

Spark Streaming流处理框架

Spark Streaming【数据流处理原理分析】_第1张图片

Spark Streaming 原理

  • 首先Spark Streaming前面也说到了是对数据流的处理。数据流是指:数据的流入、数据的处理、数据的流出。数据流处理是一种允许用户在接收到数据后的短时间内快速查询连续数据流和检测条件的技术。数据流处理是针对无界的、小量的(每次处理)、持续实时快速产生的数据。而对应流处理会有一种批处理,批处理是针对有界的、大量的、持久化的静态数据。
    Spark Streaming【数据流处理原理分析】_第2张图片
  • 无论是有界的历史数据还是无界的实时数据流,都采用一套底层API解决。以Spark为例,RDD是Spark Core的核心抽象,Dstream是Spark Streaming的提供的高级抽象,因为DStream是由许多RDD构成。
  • Spark Streaming是核心Spark API的扩展,支持可伸缩、高吞吐量、容错实时数据流处理。流数据的元数据获取也有很多渠道,例如Kafka、Flume、hdfs等。最后,处理后的数据可以推送到文件系统、数据库和活动以表板。实际上,还可以将Spark的MLlib机器学习和GraphX图形处理算法应用于数据流。
    Spark Streaming【数据流处理原理分析】_第3张图片
  • 在内部,它是这样工作的。Spark Streaming接受时时输入数据流,并将数据分批次,然后由Spark engine处理,以批量生成最终的结果流。
    Spark Streaming【数据流处理原理分析】_第4张图片
    Spark流提供了一种高级抽象,成为离散流或DStream,它表示连续的数据流。Dstreams可以从Kafka、Flume和Kinesis等源的输入数据流创建,也可以通过对其他Dstreams应用高级操作创建。在内部,Dstream表示为RDD序列。

StreamingContext

一个 Spark Streaming 程序开发流程

  1. 定义 StreamingContext
  2. 通过 StreamingContext API 创建输入 DStream(Input DStream)
  3. 对 DStream 定义 Transformation(实时计算逻辑)和 Output 操作
  4. 调用 StreamingContext 的 start()方法,启动实时处理数据
  5. 调用 StreamingContext 的 awaitTermination()方法,等待应用程序的终止。或者调用 StreamingContext 的 stop()方法,停止应用程序。

创建SparkConf的两种方式

  1. 通过 SparkConf 创建。
val conf = new SparkConf().setAppName(appName).setMaster(master)
val ssc = new StreamingContext(conf, Seconds(1))
  1. 通过 SparkContext 创建
val conf = new SparkConf().setAppName(appName).setMaster(master)
val sc = new SparkContext(conf)
val ssc = new StreamingContext(sc, Seconds(3))

注释:

  • 一个JVM只能有一个SparkContext启动。意味着应用程序中不应该出现两个SparkContext。
  • 一个JVM同时只能有一个StreamingContext启动,但一个SparkContext可以创建多个StreamingContext,只要上一个StreamingContext先用stop(false)停止,在创建下一个即可。默认调用stop()方法时,会用是停止内部的SparkContext。
  • StreamingContext停止后不能再启动。也就是说条用stop()后不能再start()。
  • StreamingContext启动之后,就不能再往其中添加任何计算逻辑了。也就是说执行start()方法之后,不能再使Dstream执行任何算子。

离散流(DStreams)

离散流或DStream是Spark Streaming提供的基本抽象。它表示连续的数据流,可以是从源接受道德输入数据流,也可以是通过转换输入流生成的经过处理的数据流。在内部,DStream由一系列连续的RDDs表示,RDDs是Spark对不可变得分布式数据集的抽象。DStream中的每个RDD包含来自某个间隔(batch interval)的数据。
Spark Streaming【数据流处理原理分析】_第5张图片
应用于DStream上的任何操作都转换为底层RDDs上的操作。例如:在下面将一个lines流转换为单词的示例(Network WordCount)中,flatMap操作应用于lines DStream中的每个RDD,以生成word DStream的RDDs。
Spark Streaming【数据流处理原理分析】_第6张图片
这些底层的RDD转换是由Spark引擎计算的。DStream操作隐藏了这些细节中的大部分,并为开发人员提供了更高级别的API。详见后文DSteam API。

Input DStream 与Receivers(接收器)

Input DStream 是表示从流媒体源接受的输入数据流的DStream,通常是第一个DStream。
Spark Streaming提供了两类内置流媒体源。

  • 基础源 StreamingContext API中直接可用的资源。示例:文件系统和Socket连接。
  • 高级源 Kafka、Flume 、hdfs等(这些需要额外的依赖)
    可以在流处理程序中并行的接受多个数据流,即创建多个Input DStreams。这将创建同时接受多个数据流的多个receivers(接受器)。但需要注意,一个Spark 的 worker/executor是一个长期运行的任务(task),因此他将占用分配给Spark Streaming的应用程序的所有核中的一个核(core)。因此,要记住,一个Spark Streaming应用需要分配足够的核(core)或线程(threads)。

注意:

  • 在本地运行Spark Streaming程序时,不要使用local或者local[1]作为主URL。这两种方法都意味着只有一个线程将用于本地运行任务。如果使用基于接收器的输入 DStream(socket 、Kafka等),name将使用单个线程来运行接收器。因此,本地运行时,使用使用local[*]作为主URL,其中要运行n个接受方。
  • 在集群上运行时,分配给Spark Streaming应用程序的内核数量必须大于接收器的数量。否则,系统将接受数据,但是无法处理它

基础数据源

  • 在StreamingContext API中直接可以使用的数据源。
    Socket (TCP Socket)
    上面Network WordCount示例便是Socket数据源
  • 文件流(File Streams)

注意:textFileStream()参数必须是文件目录,但可以支持通配符如"hdfs://namenode:8020/logs/2020/*"

Spark将监视该目录任务新建的文件,一旦有新文件才会处理。所有文件要求有相同的数据格式,并且监视文件的修改时间二不是创建时间,注意更新文件内容不会被监视,一旦开始处理,这些文件必须不能在更改,因此如果文件被连续地追加,新的数据也不会被读取。文件流不需要运行接收器,因此,不需要分配内核

RDDs队列

通过用于测试中。为了使用测试数据测试SparkSteaming应用程序,可以使用streamingContext.queueStream(queueOfRDDs)创建一个基于RDDs队列的DStream,每个进入队列的RDD都将被视为DStream中的一个批次数据,并且就像一个流进行处理。

自定义数据源(了解,扩展)

自定义数据源即自定义Receiver。自定义接受器必须通过实现两个方法来扩展Receiver抽象类。

  • onStart():开始接受数据要做的事情
  • onStop(): 停止接受数据的操作。
    onStart()和onStop()不能无线阻塞。通常,onStart()将启动负责接收数据的线程,而onStop()将确保这些接受数据的线程被停止。接收线程也可以使用isStopped方法来检查他们是否应该停止接收数据。
    一旦接收导数据,就可以通过调用store(data)将数据存储在Spark中,这是Receiver类提供的方法。store()有多种形式,可以一次存储接收到的数据记录,也可以作为对象/序列化字节的整个集合。注意:用于实现接收器的store()的峰哥会影响其可靠性和容错语义。
    接受线程中的任何异常都应被捕获并正确处理,以避免接收方的无声故障。restart()将通过异调用 onStop()和延迟后调用 onStart()来重新启动接收器。stop()将调用 onStop()并终止接收方。此外,reportError()在不停止/重新启动接收器的情况下向驱动程序报告错误消息(在日志和 UI 中可见)。
    自定义 Socket 接收器示例:

DStream API(重点)

转换操作(Transformation)

与RDDs类似,转换允许修改输入DStream中的数据。DStream支持许多在普通Spark RDD上可用的转换。一些常见的转换操作定义如下。

map(func)

通过函数func传递源DStream的每个元素来返回一个新的DStream。

flatMap(func)

与map类似,但是每个输入项可以映射到0或多个输出项。

filter(func)

通过只选择func返回true的源DStream的记录来返回一个新的DStream。

repartition(num Partitions)

通过创建更多或更傻的分区来改变DStream中的并行度。

union(otherStream)

返回一个新的DSream,它包含源DStream和otherDStream中元素的并集。

count()

通过计算源DStream的DStream的每个RDD中的元素数量,返回一个新的单元素RDDsStreams。

reduce(func)

通过使用函数func(接受两个参数并返回一个参数)聚合源DStream的每个RDD中的元素,返回一个新的单元素RDDs DStream。这个函数应该是结合律和交换律,这样才能并行计算。

countByValue()

当对类型为K的元素的DStream调用时,返回一个新的DStream(K,Long)对,其中每个键的值是他在源DStream的每个RDD中的频率。

reduceByKey(func,[numTasks])

当在(K,V)对的DStream上调用时,返回一个新的(K,V)对的Dsteam,其中每个键的值使用给定的reduce函数进行聚合。注意:在默认情况下,这将使用Spark的默认并行任务书(本地模式为2,而在集群模式下,改数量由配置属性spark.default.parallelism决定)来进行分组。可以传递一个可选的numTasks参数来设置不同数量的任务。

join(otherStream,[numTasks])

当调用两个DStream(K,V)和(K,W)对的DStream时,返回一个新的(K,Seq[V],Seq[W])元祖DStream。

cogroup(otherStream,[numTasks])

当调用(K,V)和(K,W)对的DStream时,返回一个新的(K,Seq[V],Seq[W])元祖DStream。

transform(func)

通过对源DStream的每个RDD应用一个RDD-to-RDD函数来返回一个新的DStream。这可以用来DStream上执行任意的RDD操作。

updateStateByKey(func)

返回一个新的"状态"DStream,其中通过对键的前一个状态和键的新值应用给定的函数来更新每个键的状态。这可以用来维护每个键的任意状态数据。
相比RDD转换,DStream有两个特殊操作:UpdateStateByKey操作和Window操作。

  1. UpdateStateByKey
    updateStateByKey操作允许维护任意状态,同时不断地用新信息更新它。要使用它,必须执行两个步骤。
    • 定义状态,状态可以是任意的数据类型。
    • 定义状态更新函,使用一个函数指定如何使用输入流中的前一个状态和新值来更新状态。
      在每个批量处理中,Spark将对所有现有keys应用状态更新功能,而不管他们在批量处理中是否有新数据。如果更新函数返回None,则键值对被删除。
  2. Window
    Spark Straem还提供了窗口计算,他允许在数据的滑动窗口上应用转换。
    Spark Streaming【数据流处理原理分析】_第7张图片

输出操作(Output)

输出操作允许将DStream的数据推送到外部系统,如数据库或文件系统。由于输出操作实际上允许外部系统使用转换后的数据,因此他们会触发所有DStream转换的实际执行(类似于RDDs的action)。

print()

在运行流应用程序的驱动节点上打印DStream中每批数据的前十个元素。这对开发和调试非常有用。

saveAsTextFiles(prefix,[suffix])

将DStream的内容保存为文本文件。每个批处理间隔的文件名是根据前缀和后缀“prefix-TIME_IN_MS[.suffix]”生成的。

saveAsHadoopFiles(prefix,[suffix])

将DStream的内容保存为Hadoop文件。

saveAsObjectFiles(prefix, [suffix])

将这个DStream的内容保存为序列化的java对象的序列文件。

foreachRDD(func)

将函数func应用于从流生成的每个RDD,这是最通用的输出操作。该函数应该将给每个RDD中的数据推送到外部系统,例如将RDD保存到文件中,或者通过网络将其写入数据库。请注意,func函数是在运行流应用程序的驱动程序进程中执行的,并且通常会有RDDactions,这将强制进行流RDDs的计算。

import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkSQLSparkStreamingDemo extends App {
    //TODO 创建一个spark StreamingContext对象
    val conf: SparkConf = new SparkConf().setMaster("local[2]").setAppName("DEMO01")
    val ssc = new StreamingContext(conf,Seconds(5))
    //TODO 创建SparkSession对象
    val spark=SparkSession.builder().config(conf).getOrCreate()
    import spark.implicits._
    //TODO 使用spark streaming来进行wordcount
    val inputDstream: ReceiverInputDStream[String] = ssc.socketTextStream("hadoop101",5678)
    //TODO 对输入的流进行操作
    val wordDstream: DStream[String] = inputDstream.flatMap(_.split(" "))

    wordDstream.foreachRDD(
        rdd=>{
            if (rdd.count()!=0){
                val df1=rdd.map(x=>Word(x)).toDF()
                df1.createOrReplaceTempView("words")
                spark.sql(
                    """
                      |select word,count(*)
                      |from words
                      |group by word
                    """.stripMargin).show()
            }
        }
    )
    //TODO 通过start() 启动消息采集和处理
    ssc.start()
    //TODO 等待程序终止
    ssc.awaitTermination()
}

case class Word(word:String)

Saprk Streaming 继承Flume

Spark Stream通过Push和Pull两种方式对接Flume数据源。以Saprk Streaming的角度来看,Push方式属于推送(由Flume向Spark推送)而Pull属于拉取(Spark 拉取Flume的输出)。
不论以何种方式,发开过程类似,都是由Spark Streaming对接Flume数据流,Flume作为Spark Streaming的数据源。Push和Pull两者的差别主要体现在Flume Sink的不同,而Flume Source与Channel不会受影响。在演示示例时,FlumeSource以nectcat为例,Channel为memory,重点关注SInk的变化。

Push方式

当采用Push方式时,Flume架构为:netcat->memory->arvo。
这里的avro sink需要指定avro服务地址,容易混响的是avro服务有flume还是Spark启动问题。要搞清楚这个问题,首先Push方式在Spark端如何实现。

import org.apache.spark.SparkConf
import org.apache.spark.streaming.flume.FlumeUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkFlumePushDemo extends App {
    val conf = new SparkConf().setAppName("flumeDemo01").setMaster("local[2]")
    val ssc = new StreamingContext(conf, Seconds(5))

    //TODO push方式
    val flumeStream = FlumeUtils.createStream(ssc, "192.168.222.115", 55555)

    flumeStream.map(x=>new String(x.event.getBody.array()).trim)
            .flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).print()

    ssc.start()
    ssc.awaitTermination()

}

flume的配置文件

agent.sources = s1
agent.channels = c1
agent.sinks = sk1

# 设置Source的类型为netcat,使用的channel为c1
agent.sources.s1.type = netcat
agent.sources.s1.bind = bigdata
agent.sources.s1.port = 11111
agent.sources.s1.channels = c1


agent.channels.c1.type = memory
agent.channels.c1.capacity = 1000


# AvroSink向Spark(55555)推送数据
# 使用push  createStream
agent.sinks.sk1.type=avro
agent.sinks.sk1.hostname= bigdata
agent.sinks.sk1.port= 33333
agent.sinks.sk1.channel = c1

启动顺序:先启动jar文件 -> flume配置文件 ->启动telnet11111端口。

Pull方式

当采用Pull方式时,Flume架构为:netcat -> memory ->SparkSink
此Sink全类名为org.apache.spark.streaming.flume.sink.SparkSink,需要在Flume的".conf"文件中使用,该类包含在spark-stream-flume-sink组件中。所以还需要将次组件放在$FLUME_HOME/lib下。具体远吗如下图所示:
Spark Streaming【数据流处理原理分析】_第8张图片
SparkSink由Flume启动avro服务,所以本上上任然是avroSink,如下图所示。然后由Spark连接后轮循数据,一位这Flume Agent应该先启动。

案例演示

saprk代码

import org.apache.spark.SparkConf
import org.apache.spark.streaming.flume.FlumeUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}

object SparkFlumePollDemo extends App {
    val conf = new SparkConf().setAppName("flumeDemo02").setMaster("local[2]")
    val ssc = new StreamingContext(conf, Seconds(5))

    //TODO poll方式
    val flumePollStream= FlumeUtils
            .createPollingStream(ssc,"hadoop101",55555)


    flumePollStream.map(x=>new String(x.event.getBody.array()).trim)
            .flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).print()

    ssc.start()
    ssc.awaitTermination()
}

flume代码

agent.sources = s1
agent.channels = c1
agent.sinks = sk1

#设置Source的内省为netcat,使用的channel为c1
agent.sources.s1.type = netcat
agent.sources.s1.bind = bigdata
agent.sources.s1.port = 44444
agent.sources.s1.channels = c1


#SparkSink,要求flume lib目录存在spark-streaming-flume-sink_2.11-x.x.x.jar
agent.sinks.sk1.type= org.apache.spark.streaming.flume.sink.SparkSink
agent.sinks.sk1.hostname= bigdata
agent.sinks.sk1.port=55555
agent.sinks.sk1.channel = c1
#设置channel信息
#内存模式
agent.channels.c1.type = memory
agent.channels.c1.capacity = 1000

执行顺序:先执行flume配置文件 ->spark的jar文件 ->开启端口

Saprk Streaming 集成Kafka

因为Kafka项目在0.8和0.10版本之间引入了新的消费之API,因此有两个单独的相应Spark Streaming包:

  • spark-streaming-kafka-0.8
  • spark-streaming-kafka-0.10
    注意选择正确的包,spark-streaming-kafka-0-8 与 Kafka Brokers 0.9 和 0.10 +兼容,但spark-streamingkafka-0-10与Kafka Brokers 0.10之前版本不兼容。总之,向后兼容。

Receiver 方式

Receiver是最早的方式。Receiver方式通过Receiver来获取数据,是使用Kafka的High Level Consumer API来实现的。Receiver将Kafka数据源中获取的数据存储在Spark Executor的内存中,然后Spark Streaming 启动的job会去处理那些数据。但是,在默认的配置下,这种方式可能会因为顶层的故障而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制。该机制会同步地将接收到的kafka数据写入分布式文件系统上的雨鞋日志中。所以,及时底层节点出现了失败,也可以使用雨鞋日志中的数据进行恢复。

Direct 方式(无 Receiver)

这种新的不基于 Receiver 的直接方式,是在 Spark 1.3 中引入的,从而能够确保更加健壮的机制。替代掉使用 Receiver 来接收数据后,这种方式会周期性地查询 Kafka,来获得每个topic+partition 的最新的 offset,从而定义每个 batch 的offset 的范围。当处理数据的 job 启动时,就会使用 Kafka 的简单 consumer api来获取 Kafka 指定 offset 范围的数据。
Direct 方式采用 Kafka 简单的 consumer api 方式来读取数据,此种方式不再需要专门 Receiver 来持续不断读取数据。当 batch 任务触发时,由 Executor 读取数据,并参与到其他 Executor 的数据计算过程中去。driver 来决定读取多少 offsets,并将 offsets 交由 checkpoints 来维护。将触发下次 batch 任务,再由 Executor 读 取 Kafka 数据并计算。从此过程我们可以发现 Direct 方式无需 Receiver 读取数据,而是需要计算时再读取数据,所以 Direct 方式的数据消费对内存的要求不高,只需要考虑批量计算所需要的内存即可。

Direct方式的优点如下:

  1. 简化并行读取: 如果要读取partition,不需要创建多个输入DStream然后对他们进行union操作。Spark 会创建Kafka partition一样多的RDD partition,并且会并行从kafka中读取数据。所以Kafka partition 和RDD partition之间,有一个一对一的映射关系。
  2. 高性能: 如果要保证零数据丢失,在基于receiver的方式中,需要开启WAL机制。这种方式其实效率低下,因为数据实际上被复制了两份,kafka自己本身就有高考考的机制,会对数据复制一份,而这里又会复制一份到WAL中。而基于direct的方式,不依赖Receiver,不需要开启WAL机制,只要kafka中做了数据的复制,那么就可以通过kafka的副本进行恢复。
  3. 一次且仅一次的事务机制
    • 基于 receiver 的方式,是使用 Kafka 的高阶 API 来在 ZooKeeper 中保存消费过的 offset 的。这是消费 Kafka 数据的传统方式。这种方式配合着 WAL机制可以保证数据零丢失的高可靠性,但是却无法保证数据被处理一次且仅一次,可能会处理两次。因为 Spark 和 ZooKeeper 之间可能是不同步的。
    • 基于 direct 的方式,使用 kafka 的简单 api,Spark Streaming 自己就负责追踪消费offset,并保存在 checkpoint 中。Spark 自己一定是同步的,因此可以保证数据是消费一次且仅消费一次。

你可能感兴趣的:(spark,大数据,分布式,apache,spark)