SparkStreaming编程

本文详细介绍了SparkStreaming的流式计算概念,对比了Storm和Flink的特点。接着,通过实例展示了SparkStreaming的创建、与HDFS和Kafka的整合,以及常用transformation算子。文章还深入讨论了SparkStreaming的高可用性和优化建议,包括checkpoint机制、DriverHA、调优策略等,帮助读者全面理解SparkStreaming的使用和优化。
摘要由CSDN通过智能技术生成

0. SparkStreaming

  • 流式计算简介
  • SparkStreaming实时处理入门案例
  • SparkStreaming和HDFS整合
  • SparkStreaming与Kafka整合
  • SparkStreaming常见transformation算子
  • SparkStreaming高可用及其优化建议

1. 流式计算简介

1.1. 流式计算

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

  • 数据是无界的(unbounded)

  • 数据是动态的

  • 计算速度是非常快的

  • 计算不止一次

  • 计算不能终止

​ 反过来看看一下离线计算有哪些特点:

  • 数据是有界的(Bounded)

  • 数据静态的

  • 计算速度通常较慢

  • 计算只执行一次

  • 计算终会终止

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的处理流程可以归纳为下图:
在这里插入图片描述

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都包含了一个时间段内的数据。
在这里插入图片描述

​ 对DStream应用的算子,比如map,其实在底层会被翻译为对DStream中每个RDD的操作。比如对一个DStream执行一个map操作,会产生一个新的DStream。但是,在底层,其实其原理为,对输入DStream中每个时间段的RDD,都应用一遍map操作,然后生成的新的RDD,即作为新的DStream中的那个时间段的一个RDD。底层的RDD的transformation操作。
​ 还是由Spark Core的计算引擎来实现的。Spark Streaming对Spark Core进行了一层封装,隐藏了细节,然后对开发人员提供了方便易用的高层次的API。
在这里插入图片描述

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

  • 三者对比

在这里插入图片描述

  • storm和flink简介

    storm: storm.apache.org

在这里插入图片描述

flink: flink.apache.org

在这里插入图片描述

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、排序等昂贵操作,中间结果有必要进行缓存

2. SparkStreaming实时处理入门案例

2.1. 创建项目模块

在这里插入图片描述

指定maven左表

在这里插入图片描述
执行存储位置

在这里插入图片描述
导入maven依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming_2.11</artifactId>
    <version>2.2.2</version>
</dependency>

完整的pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spark-parent-1903</artifactId>
        <groupId>com.desheng.parent</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.desheng.bigdata</groupId>
    <artifactId>spark-streaming</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
        </dependency>
    </dependencies>
</project>

2.2. 编码

2.2.1. 入口类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.2.2. 业务编写

object _01SparkStreamingWordCountOps {
    def main(args: Array[String]): Unit = {
        if(args == null || args.length < 2) {
            println(
                """
                  |Usage: <hostname> <port>
                """.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()
    }
}

2.2.3. 使用netcat进行测试

在这里插入图片描述

在这里插入图片描述

2.3. StreamingContext和Receiver说明

2.3.1. StreamingContext

​ StreamingContext是程序的入口类,用于创建DStream,维护SparkStreaming程序的声明周期。

  • 关于local说明

    当我们将上述程序中的master由local[*],修改为local的时候,程序业务不变,发生只能接收数据,无法处理数据。

在这里插入图片描述

local[*]和local的区别,后者只为当前程序提供一个线程来处理,前者提供可用的所有的cpu的core来处理,当前情况下为2或者4。

所以我们推测,当前程序无法处理数据的原因,只能是拥有cpu-core或者线程个数造成的。

同时还可以推到出来的是,SparkStreaming在当下案例中,优先使用线程资源来接收数据,其次才是对数据的处理,接收数据的对象就是Receiver。

所以,以后注意,如果读取数据的时候有receiver,程序的线程个数至少为2。
  • start

    start方法是用来启动当前sparkStreaming应用的,所以,是不能在ssc.start()之后再添加任何业务逻辑,否则,凉凉!

在这里插入图片描述

- awaitTermination

在这里插入图片描述

2.3.2. Receiver

​ Receiver,顾名思义,就是数据的接收者,这里把资源分成了两部分,一部分用来接收数据,一部分用来处理数据。Receiver接收到的数据,说白了就是一个个的batch数据,是RDD,存储在Executor内存。Receiver就是Executor内存中的一部分。

​ 不是所有的streaming作业都需要有Receiver。

​ 通过下图,来阐述基于Receiver的程序执行的流程

在这里插入图片描述

3. SparkStreaming和HDFS整合

3.1. 说明

​ SparkStreaming监听hdfs的某一个目录,目录下的新增文件,做实时处理。这种方式在特定情况下还是挺多的。需要使用的api为:ssc.fileStream()。

​ 监听的文件,必须要从另一个相匹配的目录移动到其它目录。

  • 监听本地

    无法读取手动拷贝,或者剪切到指定目录下的文件,只能读取通过流写入的文件。

  • 监听hdfs

    有的操作系统和监听本地是一样。

    正常情况下,我们可以读取到通过put上传的文件,还可以读取通过cp拷贝的文件,但是读取不了mv移动的文件。

    读取文件的这种方式,没有额外的Receiver消耗线程资源,所以可以指定master为local

3.2. 编码

object _01SparkStreamingHDFS {
    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("SparkStreamingHDFS")
            .setMaster("local")
        val duration = Seconds(2)
        val ssc = new StreamingContext(conf, duration)
        //读取local中数据 --->需要通过流的方式写入
//        val lines = ssc.textFileStream("file:///E:/data/monitored")

        //hdfs
        val lines = ssc.textFileStream("hdfs://bigdata01:9000/data/spark")

        lines.print()

        ssc.start()
        ssc.awaitTermination()
    }
}

4. SparkStreaming与Kafka整合(*)

4.1. 整合简述

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

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

在这里插入图片描述

​ 在spark-stremaing-kafka-0-8的版本中又分为了两种方式:receiver的方式和direct的方式来读取kafka中的数据,主要区别就是是否依赖zookeeper来管理offset信息,以及是否拥有receiver。

4.2. spark-stremaing-kafka-0-8

api地址:

​ http://spark.apache.org/docs/2.2.2/streaming-kafka-0-8-integration.html

导入依赖

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
    <version>2.2.2</version>
</dependency>

入口类便是KafkaUtils

4.2.1. Receiver的方式

  • 编程

    /**
      * 使用kafka的receiver-api读取数据
      */
    object _02SparkStreamingWithKafkaReceiverOps {
        def main(args: Array[String]): Unit = {
            val conf = new SparkConf()
                .setAppName("SparkStreamingWithKafkaReceiver")
                .setMaster("local[*]")
            val duration = Seconds(2)
            val ssc = new StreamingContext(conf, duration)
            val kafkaParams = Map[String, String](
                "zookeeper.connect" -> "bigdata01:2181,bigdata02:2181,bigdata03:2181/kafka",
                "group.id" -> "g_1903_1",
                "zookeeper.connection.timeout.ms" -> "10000"
            )
            val topics = Map[String, Int](
                "spark" -> 3
            )
            val messages:ReceiverInputDStream[(String, String)] = KafkaUtils
                .createStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topics,StorageLevel.MEMORY_AND_DISK_SER_2)
    
            messages.print()
    
            ssc.start()
            ssc.awaitTermination()
        }
    }
    
  • 说明

     这种方式使用Receiver来获取数据。Receiver是使用Kafka的高层次Consumer API来实现的。receiver从Kafka中获取的数据都是存储在Spark Executor的内存中的,然后Spark Streaming启动的job会去处理那些数据。
    
      然而,在默认的配置下,这种方式可能会因为底层的失败而丢失数据。如果要启用高可靠机制,让数据零丢失,就必须启用Spark Streaming的预写日志机制(Write Ahead Log,WAL)。该机制会同步地将接收到的Kafka数据写入分布式文件系统(比如HDFS)上的预写日志中。所以,即使底层节点出现了失败,也可以使用预写日志中的数据进行恢复。
    
  • 需要注意的地方

    1. Kafka的topic分区和Spark Streaming中生成的RDD分区没有关系。 在KafkaUtils.createStream中增加分区数量只会增加单个receiver的线程数,不会增加Spark的并行度

    2. 可以创建多个的Kafka的输入DStream, 使用不同的group和topic, 使用多个receiver并行接收数据。

    3. 如果启用了HDFS等有容错的存储系统,并且启用了写入日志,则接收到的数据已经被复制到日志中。因此,输入流的存储级别设置StorageLevel.MEMORY_AND_DISK_SER(即使用KafkaUtils.createStream(…,StorageLevel.MEMORY_AND_DISK_SER))的存储级别。

  • 数据会丢失原因

在这里插入图片描述

4.2.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:${untilOffset}")
                    }
                    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每次记录都会在发生故障的情况下有效地收到一次。为了实现输出结果的一次语义,将数据保存到外部数据存储区的输出操作必须是幂等的,或者是保存结果和偏移量的原子事务。

      幂等说明:多次操作结果都一样,把这种操作称之为幂等操作,比如数据库的delete操作,或者:

      INSERT INTO USER (id, NAME, age) 
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值