spark笔记-实时计算

1. Spark Streaming

  1. SPark Streaming是Spark中一个组件,基于Spark Core进行构建,用于对流式进行处理,类似于Storm。
  2. Spark Streaming能够和Spark Core、Spark SQL来进行混合编程。
  3. Spark Streaming我们主要关注:
    • Spark Streaming 能接受什么数据? kafka、flume、HDFS、Twitter等。
    • Spark Streaming 能怎么处理数据? 无状态的转换(前面处理的数据和后面处理的数据没啥关系)、有转换转换(前面处理的数据和后面处理的数据是有关系的,比如叠加关系)

1.1 spark和storm各自特点

Spark Streaming吞吐量高,和spark的扩展性:和Spark Core、Spark SQL无缝整合,实时处理出来的中间数据
Storm–实时处理,Spark Streaming—微批处理(RDD转换)。

Storm支持事务机制、健壮性、容错性、动态调整并行度等特性。

1.2 使用场景

对于Storm来说:

  1. 纯实时–实时金融系统,金融交易和分析
  2. 事务机制和可靠性机制
  3. 如果还需要针对高峰低峰时间段,动态调整实时计算程序的并行度,以最大限度利用集群资源(通常是在小型公司,集群资源紧张的情况),也可以考虑用Storm
  4. 不需要在中间执行SQL交互式查询、复杂的transformation算子等,那么用Storm是比较好的选择

对于Spark Streaming来说:

  1. 还包括了离线批处理、交互式查询等业务功能
  2. 涉及到高延迟批处理、交互式查询等功能

1.3 Spark Streaming的实现

  1. Spark Streaming 采用“微批次”架构。
  2. 对于整个流式计算来说,数据流你可以想象成水流,微批次架构的意思就是将水流按照用户设定的时间间隔分割为多个水流段。一个段的水会在Spark中转换成为一个RDD,所以对水流的操作也就是对这些分割后的RDD进行单独的操作。每一个RDD的操作都可以认为是一个小的批处理(也就是离线处理)。

1.4 Spark Streaming DStream

  1. DStream是类似于RDD和DataFrame的针对流式计算的抽象类。在源码中DStream是通过HashMap来保存他所管理的数据流的。K是RDD中数据流的时间,V是包含数据流的RDD。
  2. 对于DStream的操作也就是对于DStream他所包含的所有以时间序列排序的RDD的操作。

1.5 Spark Streaming 使用方法

通过StreamingContext来进入Spark Streaming。也可以通过已经创建好的SparkContext来创建SparkStreaming。

wordcount:

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

val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))

// Create a DStream that will connect to hostname:port, like localhost:9999
val lines = ssc.receiverStream(new CustomReceiver("master01", 9999))

// Split each line into words
val words = lines.flatMap(_.split(" "))

//import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
// Count each word in each batch
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)

// Print the first ten elements of each RDD generated in this DStream to the console
wordCounts.print()

ssc.start()             // Start the computation
ssc.awaitTermination()  // Wait for the computation to terminate
//ssc.stop()

2. Spark Streaming输入

2.1 文件数据源

1. Spark Streaming通过streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory) 这个方法提供了对目录下文件数据源的支持。
2. 如果你的文件是比较简单的文本文件,你可以使用 streamingContext.textFileStream(dataDirectory)  来代替。
3. 文件数据源目前不支持嵌套目录:
    1. 文件需要有相同的数据格式
    2. 文件进入dataDirectory的方式需要通过移动或者重命名来实现
    3. 一旦文件移动进目录,则不能再修改,即便修改了也不会读取新数据。
org.apache.spark.streaming._

val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.textFileStream("hdfs://master01:9000/data/")
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
wordCounts.print()
ssc.start()

2.2 自定义Receiver

1. 你需要新建一个Class去继承Receiver,并给Receiver传入一个类型参数,该类型参数是你需要接收的数据的类型。extends Receiver[String] (StorageLevel.MEMORY_AND_DISK)

2. 需要去复写Receiver的方法: onStart方法(在Receiver启动的时候调用的方法)、onStop方法(在Receiver正常停止的情况下调用的方法)
3. 可以在程序中通过streamingContext.receiverStream( new CustomeReceiver)来调用你定制化的Receiver。
4. 涉及多线程,sock(获取ip,端口,交给输入流调用),输入流调用时: val lines=  ssc.receiverStream(new socketReceiver("hadoop100",9999))
class CustomReceiver(host: String, port: Int)
  extends Receiver[String](StorageLevel.MEMORY_AND_DISK_2) with Logging {

  def onStart() {
    // Start the thread that receives data over a connection
        new Thread("Socket Receiver") {
      override def run() { receive() }
    }.start()
  }
  
  def onStop() {
    // There is nothing much to do as the thread     calling receive()
    // is designed to stop by itself if isStopped() returns false
  }

  /** Create a socket connection and receive data until receiver is stopped */
  private def receive() {
    var socket: Socket = null
    var userInput: String = null
    try {
      // Connect to host:port
      socket = new Socket(host, port)

      // Until stopped or connection broken continue reading
      val reader = new BufferedReader(
        new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
      userInput = reader.readLine()
      while(!isStopped && userInput != null) {
        store(userInput)
        userInput = reader.readLine()
      }
      reader.close()
      socket.close()

      // Restart in an attempt to connect again when server is active again
      restart("Trying to connect again")
    } catch {
      case e: java.net.ConnectException =>
        // restart if could not connect to server
        restart("Error connecting to " + host + ":" + port, e)
      case t: Throwable =>
        // restart if there is any other error
        restart("Error receiving data", t)
    }
  }
}

通过streamingContext.receiverStream()
来使用自定义的数据采集源。

// Assuming ssc is the StreamingContext
val customReceiverStream = ssc.receiverStream(new CustomReceiver(host, port))
val words = lines.flatMap(_.split(" "))
...

模拟Spark内置的Socket链接:

package com.atguigu.streaming

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

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

/**
  * Created by wuyufei on 06/09/2017.
  */

class CustomReceiver (host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_AND_DISK_2) {
  override def onStart(): Unit = {
  new Thread("Socket Receiver") {
      override def run() { receive() }
    }.start()
  }

  override def onStop(): Unit = {
    // There is nothing much to do as the thread calling receive()
    // is designed to stop by itself if isStopped() returns false
  }

  /** Create a socket connection and receive data until receiver is stopped */
  private def receive() {
    var socket: Socket = null
    var userInput: String = null
    try {
      // Connect to host:port
      socket = new Socket(host, port)

      // Until stopped or connection broken continue reading
      val reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))
            userInput = reader.readLine()
      while(!isStopped && userInput != null) {

        // 传送出来
        store(userInput)

        userInput = reader.readLine()
      }
      reader.close()
      socket.close()

      // Restart in an attempt to connect again when server is active again
      restart("Trying to connect again")
    } catch {
      case e: java.net.ConnectException =>
        // restart if could not connect to server
        restart("Error connecting to " + host + ":" + port, e)
      case t: Throwable =>
        // restart if there is any other error
        restart("Error receiving data", t)
            }
  }
}

object CustomReceiver {
  def main(args: Array[String]) {

    val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
    val ssc = new StreamingContext(conf, Seconds(1))

    // Create a DStream that will connect to hostname:port, like localhost:9999
    val lines = ssc.receiverStream(new CustomReceiver("master01", 9999))

    // Split each line into words
    val words = lines.flatMap(_.split(" "))

    //import org.apache.spark.streaming.StreamingContext._ // not necessary since Spark 1.3
    // Count each word in each batch
    val pairs = words.map(word => (word, 1))
    val wordCounts = pairs.reduceByKey(_ + _)
        // Print the first ten elements of each RDD generated in this DStream to the console
    wordCounts.print()

    ssc.start()             // Start the computation
    ssc.awaitTermination()  // Wait for the computation to terminate
    //ssc.stop()
  }
}

2.3 RDD数据源

 1. 你可以通过StreamingContext.queueStream(rddQueue)这个方法来监控一个RDD的队列,所有加入到这个RDD队列中的新的RDD,都会被Streaming去处理。
import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.{Seconds, StreamingContext}

import scala.collection.mutable

object QueueRdd {

  def main(args: Array[String]) {

    val conf = new SparkConf().setMaster("local[2]").setAppName("QueueRdd")
    val ssc = new StreamingContext(conf, Seconds(1))

    // Create the queue through which RDDs can be pushed to
    // a QueueInputDStream
    //创建RDD队列
    val rddQueue = new mutable.SynchronizedQueue[RDD[Int]]()

    // Create the QueueInputDStream and use it do some processing
        // 创建QueueInputDStream
    val inputStream = ssc.queueStream(rddQueue)

    //处理队列中的RDD数据
    val mappedStream = inputStream.map(x => (x % 10, 1))
    val reducedStream = mappedStream.reduceByKey(_ + _)

    //打印结果
    reducedStream.print()

    //启动计算
    ssc.start()

    // Create and push some RDDs into
    for (i <- 1 to 30) {
      rddQueue += ssc.sparkContext.makeRDD(1 to 300, 10)
      Thread.sleep(2000)

      //通过程序停止StreamingContext的运行
      //ssc.stop()
    }
  }
}

2.4 Spark Streaming Kafka输入

在工程中需要引入 Maven 工件 spark- streaming-kafka_2.10 来使用它。创建出一个流数据,需 要使用 StreamingContext 实例、一个由逗号隔开的 ZooKeeper 主机列表字符串、消费者组的名字(唯一名字),以及一个从主题到针对这个主题的接收器线程数的映射表来调用 createStream() 方法。

import org.apache.spark.streaming.kafka._
...
// 创建一个从主题到接收器线程数的映射表

val topics = List(("pandas", 1), ("logs", 1)).toMap

val topicLines = KafkaUtils.createStream(ssc, zkQuorum, group, topics) 
topicLines.map(_._2)

演示一个实例,SparkStreaming如何从Kafka读取消息,如果通过连接池方法把消息处理完成后再写会Kafka。

  • kafka Connection Pool程序
import java.util.Properties
import org.apache.commons.pool2.impl.DefaultPooledObject
import org.apache.commons.pool2.{BasePooledObjectFactory, PooledObject}
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}

case class KafkaProducerProxy(brokerList: String,
                            producerConfig: Properties = new Properties,
                            defaultTopic: Option[String] = None,
                            producer: Option[KafkaProducer[String, String]] = None) {

  type Key = String
  type Val = String

  require(brokerList == null || !brokerList.isEmpty, "Must set broker list")

  private val p = producer getOrElse {
  var props:Properties= new Properties();
    props.put("bootstrap.servers", brokerList);
    props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
    props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

    new KafkaProducer[String,String](props)
  }


  private def toMessage(value: Val, key: Option[Key] = None, topic: Option[String] = None): ProducerRecord[Key, Val] = {
    val t = topic.getOrElse(defaultTopic.getOrElse(throw new IllegalArgumentException("Must provide topic or default topic")))
    require(!t.isEmpty, "Topic must not be empty")
    key match {
      case Some(k) => new ProducerRecord(t, k, value)
      case _ => new ProducerRecord(t, value)
    }
  }

  def send(key: Key, value: Val, topic: Option[String] = None) {
  
    p.send(toMessage(value, Option(key), topic))
  }

  def send(value: Val, topic: Option[String]) {
    send(null, value, topic)
  }

  def send(value: Val, topic: String) {
    send(null, value, Option(topic))
  }

  def send(value: Val) {
    send(null, value, None)
  }

  def shutdown(): Unit = p.close()

}


abstract class KafkaProducerFactory(brokerList: String, config: Properties, topic: Option[String] = None) extends Serializable {

  def newInstance(): KafkaProducerProxy
}

class BaseKafkaProducerFactory(brokerList: String,
                                  config: Properties = new Properties,
                                  defaultTopic: Option[String] = None)
  extends KafkaProducerFactory(brokerList, config, defaultTopic) {

  override def newInstance() = new KafkaProducerProxy(brokerList, config, defaultTopic)

}


class PooledKafkaProducerAppFactory(val factory: KafkaProducerFactory)
  extends BasePooledObjectFactory[KafkaProducerProxy] with Serializable {

  override def create(): KafkaProducerProxy = factory.newInstance()

  override def wrap(obj: KafkaProducerProxy): PooledObject[KafkaProducerProxy] = new 
  DefaultPooledObject(obj)

  override def destroyObject(p: PooledObject[KafkaProducerProxy]): Unit = {
    p.getObject.shutdown()
    super.destroyObject(p)
  }
}
  • KafkaStreaming main:
import org.apache.commons.pool2.impl.{GenericObjectPool, GenericObjectPoolConfig}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.api.java.function.VoidFunction
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}

object createKafkaProducerPool{

  def apply(brokerList: String, topic: String):  GenericObjectPool[KafkaProducerProxy] = {
    val producerFactory = new BaseKafkaProducerFactory(brokerList, defaultTopic = Option(topic))
    val pooledProducerFactory = new PooledKafkaProducerAppFactory(producerFactory)
    val poolConfig = {
      val c = new GenericObjectPoolConfig
      val maxNumProducers = 10
      c.setMaxTotal(maxNumProducers)
      c.setMaxIdle(maxNumProducers)
      c
    }
    new GenericObjectPool[KafkaProducerProxy](pooledProducerFactory, poolConfig)
  }
}

object KafkaStreaming{

  def main(args: Array[String]) {
  val conf = new SparkConf().setMaster("local[4]").setAppName("NetworkWordCount")
    val ssc = new StreamingContext(conf, Seconds(1))

    //创建topic
    val brobrokers = "172.16.148.150:9092,172.16.148.151:9092,172.16.148.152:9092"
    val sourcetopic="source";
    val targettopic="target";

    //创建消费者组
    var group="con-consumer-group"
    //消费者配置
    val kafkaParam = Map(
      "bootstrap.servers" -> brobrokers,//用于初始化链接到集群的地址
      "key.deserializer" -> classOf[StringDeserializer],
      "value.deserializer" -> classOf[StringDeserializer],
      //用于标识这个消费者属于哪个消费团体
      "group.id" -> group,
      //如果没有初始化偏移量或者当前的偏移量不存在任何服务器上,可以使用这个配置属性
      //可以使用这个配置,latest自动重置偏移量为最新的偏移量
      "auto.offset.reset" -> "latest",
      //如果是true,则这个消费者的偏移量会在后台自动提交
         "enable.auto.commit" -> (false: java.lang.Boolean)
    );



    //ssc.sparkContext.broadcast(pool)

    //创建DStream,返回接收到的输入数据
    var stream=KafkaUtils.createDirectStream[String,String](ssc, LocationStrategies.PreferConsistent,ConsumerStrategies.Subscribe[String,String](Array(sourcetopic),kafkaParam))


    //每一个stream都是一个ConsumerRecord
    stream.map(s =>("id:" + s.key(),">>>>:"+s.value())).foreachRDD(rdd => {
      rdd.foreachPartition(partitionOfRecords => {
        // Get a producer from the shared pool
        val pool = createKafkaProducerPool(brobrokers, targettopic)
        val p = pool.borrowObject()

        partitionOfRecords.foreach {message => 
    System.out.println(message._2);p.send(message._2,Option(targettopic))}

        // Returning the producer to the pool also shuts it down
        pool.returnObject(p)

      })
    })

    ssc.start()
    ssc.awaitTermination()

  }
}

2.2.1 程序部署

  1. 启动zookeeper和kafka。
bin/kafka-server-start.sh -daemon ./config/server.properties
  1. 创建两个topic,一个为source,一个为target.
bin/kafka-topics.sh --create --zookeeper 192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181,192.168.1.104:2181 --replication-factor 2 --partitions 2 --topic source
bin/kafka-topics.sh --create --zookeeper 192.168.1.101:2181,192.168.1.102:2181,192.168.1.103:2181,192.168.1.104:2181 --replication-factor 2 --partitions 2 --topic targe
  1. 启动kafka console producer 写入source topic
bin/kafka-console-producer.sh --broker-list 192.168.1.101:9092,192.168.1.102:9092, 192.168.1.103:9092,192.168.1.104:9092 --topic source
  1. 启动kafka console consumer 监听target topic
bin/kafka-console-consumer.sh --bootstrap-server 192.168.1.101:9092,192.168.1.102:9092, 192.168.1.103:9092,192.168.1.104:9092 --topic source
  1. 启动kafkaStreaming程序
[bigdata@master01 ~]$ ./hadoop/spark-2.1.1-bin-hadoop2.7/bin/spark-submit --class com.atguigu.streaming.KafkaStreaming ./kafkastreaming-jar-with-dependencies.jar

2.5 Spark Streaming flume输入

Spark提供两个不同的接收器来使用Apache Flume。

  • 推式接收器:该接收器以 Avro 数据池的方式工作,由 Flume 向其中推数据。
  • 拉式接收器:该接收器可以从自定义的中间数据池中拉数据,而其他进程可以使用 Flume把数据推进该中间数据池。

3. DStream转换

包括无状态转化和有状态转化
无状态转化:map(),flatMap(),filit(),repartition()
有状态转化(根据之前的RDD或中间数据生成当前RDD):updateStateBykey() window系列的
updateStateBykey():结果贯穿整个应用程序,需要做检查点的目录
window系列,如reduceByKeyAndWindow 是一个时间范围的计算,处理一段时间内发送的业务,如:性能,比例,包括:
		1. 窗口大小:类似于1步
		2. 滑动步长:类似于1步的大小
			 
输出:
print、saveAsTextFiles、saveAsHadoopFiles

worldscount(updateStateBykey)例子:

object Worldcount{
    def main (args:Array[String]){
    //创建SparkConf,streamingContext
        val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount)
        val ssc = new streamingContext(conf, Seconds(3))
        //设置一个检查点的目录
        ssc.cheakpoint(".")
        //通过streamingContext来获取master机器上的7777端口传过来的语句
        val lines = ssc.socketTextStream("master",7777)
        //将单词按空格分隔开
        val words = lines.flatMap(_,split(" "))
        //需要将每一个单词都映射称为一个元组(word,1)
        val pairs = words.map(word, 1)
        
        //定义更新状态方法,参数values为当前批次单词数量,state为之前批处理单词数量
        val updateFunc = (values: Seq[Int], state: Option[Int]) =>{
            //计算当前批次相同key的单词总数
            val currentCount = values.foldLeft(0)(_ + _)
            //获取上一次保存的单词总数
            val previousCount = state.getOrElse(0)
            //返回新的单词总数
            Some(currentCount + previousCount)
        
        val stateDstream = pairs.updateStateByKey[Int](updateFunc)
        stateDstream.print()
        }
    }
}

4. Structured Streaming

一种基于 Spark SQL 引擎的可扩展且容错的流处理引擎,它最关键的思想是将实时数据流视为一个不断增加的表,从而就可以像操作批静态数据一样来操作流数据。相较于spark streaming,Structured Streaming为事件驱动型, 就是数据真正发生的时间,比如用户浏览了一个页面可能会产生一条用户的该时间点的浏览日志。

Structured Streaming将实时流抽象成一张无边界的表,输入的每一条数据当成输入表的一个新行,同时将流式计算的结果映射为另外一张表,完全以结构化的方式去操作流式数据。
【图1】
会对输入的查询生成“结果表”,每个触发间隔(例如,每 1 秒)新行将附加到输入表,最终更新结果表,每当结果表更新时,能够将更改后的结果写入外部接收器去。

一共有三种Output模式:

  • Append模式:只有自上次触发后在Result Table表中附加的新行将被写入外部存储器。重点模式,一般使用它。

  • Complete模式: 将整个更新表写入到外部存储。每次batch触发计算,整张Result Table的结果都会被写出到外部存储介质。

  • Update模式:只有自上次触发后在Result Table表中更新的行将被写入外部存储器。注意,这与完全模式不同,因为此模式不输出未更改的行。

2
事件时间在此模型中非常自然地表示,来自设备的每个事件都是表中的一行,事件时间是该行中的一个列值。这允许基于窗口的聚合(例如每分钟的事件数)仅仅是时间列上的特殊类型的分组和聚合 - 每个时间窗口是一个组,并且每一行可以属于多个窗口/组。因此,可以在静态数据集(例如,来自收集的设备事件日志)以及数据流上一致地定义这种基于事件时间窗的聚合查询。
wo

4.1 延迟数据处理Watermark

Structured Streaming基于Event time能自然地处理延迟到达的数据,保留或者丢弃。

由于Spark正在更新Result Table,因此当存在延迟数据时,它可以完全控制更新旧聚合,以及清除旧聚合以限制中间状态数据的大小。

使用Watermark,允许用户指定数据的延期阈值,并允许引擎相应地清除旧的状态。
watermark

4.2 容错性

  1. 流式数据处理系统的可靠性通常是通过系统可以处理每个记录的次数来定义的。系统可以在所有可能的操作情形下提供三种类型的保证(无论出现何种故障):
    • At most once:每个记录将被处理一次或不处理。
    • At least once: 每个记录将被处理一次或多次。 这比“最多一次”更强,因为它确保不会丢失任何数据。但可能有重复处理。
    • Exactly once:每个记录将被精确处理一次 - 不会丢失数据,并且不会多次处理数据。 这显然是三者中最强的保证。

如果假设正好在process的过程中,系统挂掉了,那么数据就会丢了,但因为 Structured Streaming 如果是complete模式,因为是全量数据,所以其实做好覆盖就行,也就说是幂等的。

如果是append 模式,则可能只能保证at-least once ,而对于其内部,也就是result table 是可以保证exactly-once 的。

  1. 容错机制
    在故障或故意关闭的情况下,用户可以恢复先前进度和状态,并继续在其停止的地方,简称断点续传。这是通过使用检查点checkpoint和预写日志write ahead logs来完成的。

    用户可以指定checkpoint的位置,Spark将保存所有进度信息(如每个触发器中处理的offset偏移范围)和正在运行的聚合到checkpoint中。任务重启后,使用这些信息继续执行。

StructuredStreaming+Kafka使用教程:https://blog.csdn.net/lovechendongxing/article/details/81748553

4.3 Join操作

Structured Streaming支持流式Dataset/DataFrame和静态Dataset/DataFrame的Join,也支持和另一个流式Dataset/DataFrame的Join。Join的结果是增量生成的,类似于流的聚合。需要注意的是:

  1. 不管是使用哪种类型的join,对于拥有相同数据的流式Dataset/DataFrame和静态Dataset/DataFrame,其结果是相同的。
  2. 在import package时,分清java与scala类名,两者的名字很像。

4.3.1 Stream-static Joins(流-静态 Joins)

自Spark 2.0版本起,Structured Streaming就已经支持了流式Dataset/DataFrame与静态Dataset/DataFrame之间的join(inner join,一些类型的outer join)。

staticDf = spark.read. ...
streamingDf = spark.readStream. ...
streamingDf.join(staticDf,"type") // 默认inner join
streamingDf.join(staticDf,"type","right_join")  //左外连接

stream-static joins没有状态,所以不需要状态存储。然后,某些类型的stream-static outer joins现在还没有被支持。

4.3.2 Stream-Stream Joins(流-流 Joins)

自Spark 2.3版本起,加入了对Stream-Stream Joins的支持。对于两个数据流做join的难点在于:在任何时候,两个数据集的视图都是不完整的,这使得在输入之间查找匹配变得更加困难。从一个输入流接收到的任何行都可以与来自另一个输入流的将来的任何尚未接收到的行匹配。因此,对两端输入流的过往数据进行缓存,这样就可以用它来匹配以后的输入数据,从而产生join的结果。此外,和流聚合一样,我们使用watermark机制自动的处理迟到数据以及限制state的存储。

  1. Inner Joins(内连接)—— watermark可选
    任何类型的列以及join条件都是支持的。然而,随着流的运行,其state会无限的增大。为了避免这种无限增大的state,必须定义额外的join条件,使得过于旧的输入数据无法与将来的输入数据进行匹配,因此可以在state中清除它。换句话说,必须在join中做如下附加步骤:
    (1)在两端的输入数据上定义watermark,以便引擎知道其延迟的程度;
    (2)在两端输入数据上定义event-time(是数据被生产出来的时间,可以是比如传感器发出信号的时间等,此时数据还没有被传输给kafka)约束,这样引擎就可以计算出一个输入的旧行何时不需要与另一个输入进行匹配。
from pyspark.sql.functions import expr

impressions = spark.resdStream. ....
clicks = spark.resdStream. ...

impressionWithWatermark.join(
    clicksWithWatermark,
    expr"""
    clickId = impressionsId
    clickTime >= impressionTime AND
    clickTime <= impressionTime + interval 1 hours
    """)
  1. Outer Joins(外连接)—— watermark必须有
    对于内连接来说,watermark+event-time约束是可选的,对于左外和右外连接来说,它们是必须指定的。这是因为对于outer join中产生的NULL记录,引擎必须知道输入行在将来的什么时候不再匹配。一个outer join查询看起来与前面的示例非常相似,只是会有一个额外的参数将其指定为outer join。
impressionsWithWatermark.join(
    clicksWithWatermark,
    expr"""
    clickId = impressionsId
    clickTime >= impressionTime AND
    clickTime <= impressionTime + interval 1 hours
    """),"leftOut"       // 可选:"inner","leftOuter","rightOuter"

需要注意的是

  1. outer join的NULL记录会被延迟产生,延迟的时间取决于设定的watermark延时以及时间范围条件。这是因为引擎必须等待以确保现在和将来都不会再有匹配。
  2. 如果join的两个输入流中的任意一个在一段时间内没有接收到数据,output join的输出将会被延迟。

4.3.3 structured streaming支持的join类型

structured streaming join

5.背压

默认情况下,SparkStremaing根据Receiver以生产者生产数据的速度来接收数据,但是在工作状态下, 实际计算一个批次数据的时间一般要大于Streaming应用设置的批处理间隔。这就意味着Spark Streaming处理数据的速度要小于数据接收的速度, 数据处理能力低,导致数据全部堆积在内存中,进一步导致Receiver所在的Executor会发生内存溢出的问题。

sparkConf.set("spark. streaming.backpressure.enabled","true")

背压:配置项spark. streaming.kafka.maxRatePerPartition,对防止流式应用在下边两种情况下出现流量过载时尤其重要:

  1. Kafka Topic中有大量未处理的消息,并且我们设置是Kafka auto. offset .reset参数值为smallest,他可以防止第一个批次出现数据流量过载情况。
  2. 当Kafka生产者突然飙升流量的时候,他可以防止批次处理出现数据流量过载情况。

同时,Spark Streaming加入了一个RateController,利用的算法是PID,需要的反馈数据是任务处理的结束时间、调度时间、处理时间、消息条数。这些数据是通过Sparklistener体系获得,然后通过P的compute计算得到一个速率,进而可以计算得到一个offset,然后跟限速设置最大消费条数比较得到一个最终要消费的消息最大offset。

猜你喜欢

转载自blog.csdn.net/weixin_42526352/article/details/104881972