SparkStreaming之DStream的创建方式

SparkStreaming之DStream的创建方式

  • 1. RDD队列(了解)
    • 1.1用法及说明
    • 1.2 案例实操
  • 2. 自定义数据源
    • 2.1 用法及说明
    • 2.2 案例实操
  • 3. Kafka数据源(面试开发重点)
    • 3.1 版本选型
    • 3.2 Kafka 0-8 Receive模式
    • 3.3 Kafka 0-8 Direct模式
    • 3.4 Kafka 0-10 Direct模式
    • 3.5 消费Kafka数据模式总结

1. RDD队列(了解)

1.1用法及说明

  • 测试过程中,可以通过使用ssc.queueStream(queueOfRDDs)来创建DStream,每一个推送到这个队列中的RDD,都会作为一个DStream处理。

1.2 案例实操

1)需求:循环创建几个RDD,将RDD放入队列。通过SparkStream创建DStream,计算WordCount。

2)代码实现

import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}

import scala.collection.mutable

/**
 * @author zjfstart
 * @create 2020-05-21-14:55
 *
 *        通过RDD队列方式创建DStream
 *        需求:循环创建几个RDD,将RDD放入队列。通过SparkStream创建Dstream,计算WordCount
 */
object Test02_DStreamingCreate_RDDQueue {

    def main(args: Array[String]): Unit = {

        // 创建配置文件的对象
        val conf: SparkConf = new SparkConf().setAppName("Test02_DStreamingCreate_RDDQueue").setMaster("local[*]")

        // 创建Spark Streaming上下文环境对象
        // 采集周期为3秒钟
        val ssc = new StreamingContext(conf, Seconds(3))

        // 创建一个队列,里面放的是RDD
        val rddQueue: mutable.Queue[RDD[Int]] = new mutable.Queue[RDD[Int]]

        // 从队列中采集数据,获取DStream
        // oneAtATime默认情况下是true,表示在一个采集周期内采集一个RDD
        val queueDS: InputDStream[Int] = ssc.queueStream(rddQueue,true)

        // 处理采集到的数据
        val resDS: DStream[(Int, Int)] = queueDS.map((_, 1)).reduceByKey(_ + _)

        // 打印输出结果
        resDS.print()

        // 启动采集器
        ssc.start()

        // 循环创建RDD,并将创建的RDD放入到队列中
        for (i <- 1 to 5) {
            rddQueue.enqueue(ssc.sparkContext.makeRDD(6 to 10))   // 把创建好的RDD放入队列中
            Thread.sleep(2000)   // 睡两秒钟
        }

        // 等待采集之后终止程序
        ssc.awaitTermination()
    }
}

3)输出结果

-------------------------------------------
Time: 1590049257000 ms
-------------------------------------------
(6,1)
(7,1)
(8,1)
(9,1)
(10,1)

-------------------------------------------
Time: 1590049260000 ms
-------------------------------------------
(6,1)
(7,1)
(8,1)
(9,1)
(10,1)

-------------------------------------------
Time: 1590049263000 ms
-------------------------------------------
(6,1)
(7,1)
(8,1)
(9,1)
(10,1)

-------------------------------------------
Time: 1590049266000 ms
-------------------------------------------
(6,1)
(7,1)
(8,1)
(9,1)
(10,1)

-------------------------------------------
Time: 1590049269000 ms
-------------------------------------------
(6,1)
(7,1)
(8,1)
(9,1)
(10,1)

-------------------------------------------
Time: 1590049272000 ms
-------------------------------------------

-------------------------------------------

2. 自定义数据源

2.1 用法及说明

  • 自定义一个类,需要继承Receiver,并实现onStart、onStop方法来自定义数据源采集。

2.2 案例实操

1)需求:自定义数据源,实现监控某个端口号,获取该端口号内容。

2)代码实现

import java.io.{BufferedReader, InputStreamReader}
import java.net.{ConnectException, Socket}
import java.nio.charset.StandardCharsets

import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.receiver.Receiver
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * @author zjfstart
 * @create 2020-05-21-14:56
 *
 *        通过自定义数据源方式创建DStream
 *        模拟从指定的网络端口获取数据
 */
object Test03_CustomReceiver {

    def main(args: Array[String]): Unit = {

        // 创建配置文件的对象
        val conf: SparkConf = new SparkConf().setAppName("Test03_CustomReceiver").setMaster("local[*]")

        // 创建Spark Streaming上下文的环境对象
        val ssc = new StreamingContext(conf, Seconds(3))

        // 通过自定义数据源的方式来创建DStream
        val myDS: ReceiverInputDStream[String] = ssc.receiverStream(new MyRecevicer("hadoop102", 9999))

        // 对获取到的数据进行扁平化处理
        val flatMapDS: DStream[String] = myDS.flatMap(_.split(" "))

        // 对数据进行结构上的转换
        val mapDS: DStream[(String, Int)] = flatMapDS.map((_, 1))

        // 对上述的数据进行聚合处理
        val reduceDS: DStream[(String, Int)] = mapDS.reduceByKey(_ + _)

        // 输出结果      注意:调用的是 DS的 print 函数
        reduceDS.print()

        // 启动采集器
        ssc.start()

        // 默认情况下,上下文对象不能关闭
        // scc.stop()

        // 等待采集器结束,终止上下文环境对象
        ssc.awaitTermination()
    }
}

// 自定义数据源   // 模仿SocketReceiver
// Receiver[T]  泛型表示的是读取数据的类型
class MyRecevicer(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY) {

    // 定义一个socket
    private var socket: Socket = _

    // 真正的处理接收数据的逻辑
    // 自定义数据源的入口
    def receive() {
        try {
            // 创建socket连接
            socket = new Socket(host, port)
            // 根据连接对象获取输入流
            val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))
            // 定义一个变量,用于接收读取到的一行数据
            var input: String = null
            while((input = reader.readLine()) != null) {
                store(input)   // 拿到数据给框架进行保存
            }
        } catch {
            case e: ConnectException =>
                restart(s"Error connecting to $host:$port", e)
                return
        } finally {
            onStop()
        }
    }

    // 开启线程(固定的写法)
    override def onStart(): Unit = {

        new Thread("Socket Receiver") {
            setDaemon(true)
            override def run() { receive() }
        }.start()
    }

    // 关闭socket(固定的写法)
    override def onStop(): Unit = {
        synchronized {
            if (socket != null) {
                socket.close()
                socket = null
            }
        }
    }
}

3. Kafka数据源(面试开发重点)

3.1 版本选型

  • ReceiverAPI:需要一个专门的Executor去接收数据,然后发送给其他的Executor做计算。存在的问题,接收数据的Executor和计算的Executor速度会有所不同,特别在接收数据的Executor速度大于计算的Executor速度,会导致计算数据的节点内存溢出。
  • DirectAPI:是由计算的Executor来主动消费Kafka的数据,速度由自身控制。
    SparkStreaming之DStream的创建方式_第1张图片
    注:需要强调是,在spark-streaming-kafka-0-10是不支持ReceiverAPI的;而在Spark2.3.0之后,spark-streaming-kafka-0-8已经过时了。

3.2 Kafka 0-8 Receive模式

  1. 需求:通过SparkStreaming从Kafka读取数据,并将读取过来的数据做简单计算,最终打印到控制台。
  2. 导入依赖
<dependency>
    <groupId>org.apache.sparkgroupId>
    <artifactId>spark-streaming-kafka-0-8_2.11artifactId>
    <version>2.1.1version>
dependency>
  1. 代码编写
  • 0-8Receive模式,offset维护在zk中,程序停止后,继续生产数据,再次启动程序,仍然可以继续消费。可通过get
    /consumers/bigdata/offsets/主题名/分区号 查看
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * @author zjfstart
 * @create 2020-05-21-14:57
 *  *        通过ReceiverAPI连接Kafka数据源,获取数据
 */
object Test04_ReceiverAPI {

    def main(args: Array[String]): Unit = {

        // 创建配置文件的对象
        val conf: SparkConf = new SparkConf().setAppName("Test04_ReceiverAPI").setMaster("local[*]")

        // 创建SparkStreaming上下文的环境对象
        val ssc = new StreamingContext(conf, Seconds(3))

        // 连接Kafka,创建DStream
        val kafkaDStream: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(
            ssc,
            // 这里面需要注意的是:要和之前配置Kafka的server-properties的配置文件中配置的zookeeper连接地址一致
            "hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka", // zookeeper集群地址
            "test0105", // 消费者组
            Map("bigdata-0105" -> 2) // 主题 -> 分区数
        )

        // 获取Kafka中的消息,我们只需要v的部分:消息体
        val lineDS: DStream[String] = kafkaDStream.map(_._2)

        // 扁平化
        val flatMapDS: DStream[String] = lineDS.flatMap(_.split(" "))

        // 结构转换  进行计数
        val mapDS: DStream[(String, Int)] = flatMapDS.map((_, 1))

        // 聚合
        val reduceDS: DStream[(String, Int)] = mapDS.reduceByKey(_ + _)

        // 打印输出
        reduceDS.print()

        // 开启任务
        ssc.start()

        // 等待
        ssc.awaitTermination()
    }
}
  1. 集群上操作步骤
  • 查看主题
[atguigu@hadoop102 kafka-2.4.1]$ bin/kafka-topics.sh --list --bootstrap-server hadoop102:9092
  • 创建bigdata-0105主题,分区数为2,副本数为2
[atguigu@hadoop102 kafka-2.4.1]$ bin/kafka-topics.sh --create --bootstrap-server hadoop102:9092 --topic bigdata-0105 --partitions 2 --replication-factor 2 
  • 生产者生产消息
[atguigu@hadoop102 kafka-2.4.1]$ bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic bigdata-0105
  • 在生产者发送消息,然后运行编写好的程序。看是否能成功消费到数据。

3.3 Kafka 0-8 Direct模式

1)需求:通过SparkStreaming从Kafka读取数据,并将读取过来的数据做简单计算,最终打印到控制台。

2)导入依赖

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

3)代码编写(自动维护offset1

  • offset维护在checkpoint中,但是获取StreamingContext的方式需要改变,目前这种方式会丢失消息
import kafka.serializer.StringDecoder
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * @author zjfstart
 * @create 2020-05-21-15:32
 *
 *  通过DirectAPI连接Kafka数据源,获取数据
 *  自动的维护偏移量,偏移量维护在checkpiont中
 *  目前我们这个版本,只是指定的检查点,只会将offset放到检查点中,但是并没有从检查点中取,会存在消息丢失
 */
object Test05_DirectAPI_Auto01 {

    def main(args: Array[String]): Unit = {
        // 创建配置文件的对象
        val conf: SparkConf = new SparkConf().setAppName("Test05_DirectAPI_Auto01").setMaster("local[*]")

        // 创建Spark Streaming的上下文环境对象
        val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))

        // 设置检查点目录
        ssc.checkpoint("D:\\MyWork\\IdeaProjects\\spark0105_exer\\src\\main\\checkPoint")

        // 准备Kafka参数
        val kafkaParams: Map[String, String] = Map[String, String](
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092",
            ConsumerConfig.GROUP_ID_CONFIG -> "bigdata"
        )

        // 创建DStream
        val kafkaDStream: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](
            ssc,
            kafkaParams,
            Set("bigdata-0105")
        )

        // 获取Kafka中的消息,我们只需要v的部分:消息体
        val lineDS: DStream[String] = kafkaDStream.map(_._2)

        // 扁平化
        val flatMapDS: DStream[String] = lineDS.flatMap(_.split(" "))

        // 结构转换  进行计数
        val mapDS: DStream[(String, Int)] = flatMapDS.map((_, 1))

        // 聚合
        val reduceDS: DStream[(String, Int)] = mapDS.reduceByKey(_ + _)

        // 打印输出
        reduceDS.print()

        // 开启任务
        ssc.start()

        // 等待
        ssc.awaitTermination()

    }
}

4)代码编写(自动维护offset2

  • offset维护在checkpoint中,获取StreamingContext为getActiveOrCreate()方法
  • 这种方式缺点:
    (1)checkpoint小文件过多;
    (2)checkpoint记录最后一次时间戳,再次启动的时候会把间隔时间的周期再执行一次。
import kafka.serializer.StringDecoder
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * @author zjfstart
 * @create 2020-05-21-16:07
 *
 *        通过DirectAPI连接Kafka数据源,获取数据
 *        自动的维护偏移量,偏移量维护在checkpiont中
 *        优点:修改StreamingContext对象的获取方式,先从检查点获取,如果检查点没有,通过函数创建。会保证数据不丢失
 *        缺点:
 *        1.小文件过多
 *        2.在checkpoint中,只记录最后offset的时间戳,再次启动程序的时候,会从这个时间到当前时间,把所有周期都执行一次
 */
object Test05_DirectAPI_Auto02 {

    def main(args: Array[String]): Unit = {

        // 创建Streaming上下文的对象
        val ssc: StreamingContext = StreamingContext.getActiveOrCreate("D:\\MyWork\\IdeaProjects\\spark0105_exer\\src\\main\\checkPoint", () => getStreamingContext)

        ssc.start()
        ssc.awaitTermination()
    }

    def getStreamingContext(): StreamingContext = {
        // 创建配置文件的对象
        val conf: SparkConf = new SparkConf().setAppName("Test05_DirectAPI_Auto02").setMaster("local[*]")

        // 创建Spark Streaming上下文环境对象
        val ssc = new StreamingContext(conf, Seconds(3))

        // 设置检查点目录
        ssc.checkpoint("D:\\MyWork\\IdeaProjects\\spark0105_exer\\src\\main\\checkPoint")

        // 准备Kafka参数
        val kafkaParams: Map[String, String] = Map[String, String](
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092",
            ConsumerConfig.GROUP_ID_CONFIG -> "bigdata"
        )

        // 创建DStream
        val kafkaDStream: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](
            ssc,
            kafkaParams,
            Set("bigdata-0105")
        )

        // 获取Kafka中的消息,我们只需要v的部分
        val lineDS: DStream[String] = kafkaDStream.map(_._2)

        //扁平化
        val flatMapDS: DStream[String] = lineDS.flatMap(_.split(" "))

        //结构转换  进行计数
        val mapDS: DStream[(String, Int)] = flatMapDS.map((_,1))

        //聚合
        val reduceDS: DStream[(String, Int)] = mapDS.reduceByKey(_+_)

        //打印输出
        reduceDS.print

        ssc
    }

}

5)代码编写(手动维护offset

import kafka.common.TopicAndPartition
import kafka.message.MessageAndMetadata
import kafka.serializer.StringDecoder
import org.apache.kafka.clients.consumer.ConsumerConfig
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka.{HasOffsetRanges, KafkaUtils, OffsetRange}
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * @author zjfstart
 * @create 2020-05-21-16:27
 *
 *        通过DirectAPI连接Kafka数据源,获取数据
 *        手动维护offset
 */
object Test07_DirectAPI_Handler {

    def main(args: Array[String]): Unit = {

        // 创建配置文件的对象
        val conf: SparkConf = new SparkConf().setAppName("Test07_DirectAPI_Handler").setMaster("local[*]")

        // 创建SparkStreaming上下文环境对象
        val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))

        // 准备Kafka的参数
        val kafkaParams: Map[String, String] = Map[String, String](
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092",
            ConsumerConfig.GROUP_ID_CONFIG -> "bigdata"
        )

        // 获取上一次消费的位置(偏移量)
        // 实际项目中,为了保证数据精准一致性,我们对数据进行消费处理之后,将偏移量保存在有事务的存储中,如MySQL
        var fromOffsets:Map[TopicAndPartition,Long] = Map[TopicAndPartition,Long](
            TopicAndPartition("bigdata-0105",0)->10L,
            TopicAndPartition("bigdata-0105",1)->10L
        )

        // 从指定的offset读取数据进行消费
        val kafkaDStream: InputDStream[String] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder, String](
            ssc,
            kafkaParams,
            fromOffsets,
            (m: MessageAndMetadata[String, String]) => m.message()
        )

        //消费完毕之后,对偏移量offset进行更新
        var offsetRanges: Array[OffsetRange] = Array.empty[OffsetRange]  // 定义一个集合,用来存放偏移量的范围
        kafkaDStream.transform{
            rdd=>{
                offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
                rdd
            }
        }.foreachRDD{
            rdd=>{
                for (o <- offsetRanges) {
                    println(s"${o.topic} ${o.partition} ${o.fromOffset} ${o.untilOffset}")
                }
            }
        }

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

3.4 Kafka 0-10 Direct模式

  1. 需求:通过SparkStreaming从Kafka读取数据,并将读取过来的数据做简单计算,最终打印到控制台。
  2. 导入依赖
<dependency>
    <groupId>org.apache.sparkgroupId>
    <artifactId>spark-core_2.11artifactId>
    <version>2.1.1version>
dependency>
<dependency>
    <groupId>org.apache.sparkgroupId>
    <artifactId>spark-streaming_2.11artifactId>
    <version>2.1.1version>
dependency>
<dependency>
    <groupId>org.apache.sparkgroupId>
    <artifactId>spark-streaming-kafka-0-10_2.11artifactId>
    <version>2.1.1version>
dependency>
  1. 代码实现
import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
 * @author zjfstart
 * @create 2020-05-22-9:26
 *  *        通过DirectAPI 0-10 消费Kafka数据
 *        消费的offset保存在__consumer_offsets主题中
 */
object SparkStreaming01_DirectAPI_0_10 {

    def main(args: Array[String]): Unit = {
        // 创建配置文件的对象
        val conf: SparkConf = new SparkConf().setAppName("SparkStreaming01_DirectAPI_0_10").setMaster("local[*]")

        // 创建SparkStreaming上下文的环境对象
        val ssc: StreamingContext = new StreamingContext(conf, Seconds(3))

        // 声明Kafka相关的连接参数
        val kafkaParams: Map[String, Object] = Map[String, Object](
            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092, hadoop103:9092, hadoop104:9092",
            ConsumerConfig.GROUP_ID_CONFIG -> "BigData",
            // 反序列化方式:两种写法
            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> "org.apache.kafka.common.serialization.StringDeserializer",
            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer]  // 获取类的全限定名
        )

        // 通过读取Kafka数据,创建DStream
        val kafkaDStream0_10: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](
            ssc,
            // 位置策略,指定计算的Executor
            LocationStrategies.PreferConsistent,
            // 消费策略   参数:消费的主题,Kafka连接参数
            ConsumerStrategies.Subscribe[String, String](Set("bigdata-0105"), kafkaParams)
        )

        // WordCount计算
        kafkaDStream0_10.map(_.value())
          .flatMap(_.split(" "))
          .map((_, 1))
          .reduceByKey(_+_)
          .print()

        ssc.start()
        ssc.awaitTermination()
    }
}
  1. 集群操作步骤
  • 生产者生产消息
[atguigu@hadoop102 kafka-2.4.1]$ bin/kafka-console-producer.sh --broker-list hadoop102:9092 --topic bigdata-0105
>aa aa bb bb cc cc
>zhang zhang zhang
  • 启动编写好的程序,会消费到Kafka中生产出的消息;

  • 其中,停止掉程序,然后继续在Kafka上生产消息,再次启动程序,仍然能消费到历史消息,这是因为0-10版本的offset(偏移量)是存放在Kafka内置的一个主题中,主题的名字叫做__consumer_offset。

3.5 消费Kafka数据模式总结

  • 0-8 ReceiverAPI
    1)专门的Executor读取数据,速度不统一;
    2)跨机器传输数据,WAL;
    3)Executor读取数据通过多个线程的方式,想要增加并行度,则需要多个流union;
    4)offset存储在Zookeeper中。

  • 0-8 DirectAPI
    1)Executor读取数据并计算;
    2)增加Executor个数来增加消费的并行度
    3)offset存储:
    (1)CheckPoint(getActiveOrCreate方式创建StreamingContext);
    (2)手动维护(有事务的存储系统);
    (3)获取offset必须在第一个调用的算子中:offsetRanges = rdd.asInstanceOf[HashOffsetRanges].offsetRanges。

  • 0-10 DirectAPI
    1)Executor读取数据并计算
    2)增加Executor个数来增加消费的并行度
    3) offset存储
    (1)__consumer_offsets系统主题中
    (2)手动维护(有事务的存储系统)

你可能感兴趣的:(Spark)