SparkStreaming-DStream

Spark Streaming接收数据并将其分隔成一批批的数据,然后被Spark engine处理形成一批批的结果。需指出,Spark Streaming可以被应用与机器学习和图计算。
SparkStreaming-DStream_第1张图片

Spark Streaming提供了一个高级抽象称为DStream,代表连续的数据流。DStream可从kafka、flume、kinesis等数据源创建,DStream内部是一个RDDs序列。

快速入门

StreamingContext是所有流处理的入口,下例创建一个2线程的本地StreamingContext,1秒一批

import org.apache.spark._
import org.apache.spark.streaming._

val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
// 创建一个输入DStream代表TCP数据源,监听localhost 9999端口
val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))
wordCounts.print()

// 开始运行,接收并处理数据
ssc.start()
// 阻塞至运算终止(手动停止或出现错误),也可使用streamingContext.stop()停止处理过程
ssc.awaitTermination()

StreamingContext 可以由一个已知的SparkContext 对象创建
val ssc = new StreamingContext(sc, Seconds(1)) //sc 已知的SparkContext 对象

一旦context 启动,将不能增加新的流运算
一旦context终止,将不能被重启
一个JVM只允许一个Streamingcontext存在
stop()除了停止StreamingContext同时也停止SparkContext,如果仅停止Streamingcontext,则调用stop(false)
一个SparkContext可被多个StreamingContext重用,只要在新的被创建之前停止旧的

DStream

DStream内部是一个连续的RDDs序列,就像下图:
SparkStreaming-DStream_第2张图片
任何在DStream上的操作都相应转换到下一级RDDs,比如:
SparkStreaming-DStream_第3张图片
这些转换操作在底层隐藏了众多细节,由Spark engine执行。

输入DStream与Receiver

每一个输入DStream代表从数据源接收数据,都跟一个Receiver对象(接收数据在被处理之前,将数据存储在内存)相关联。Spark Streaming提供两种支持的数据源:
基础数据源:从StreamingContext API直接使用,比如文件系统,socket连接
高级数据源:比如kafka、flume、kinesis等,通过额外的工具包引入

在本地使用Spark Streaming时,不要用“local” 或 “local[1]” 设置master url,因为基于receiver的DStream除了用一个线程跑receiver以外,还需要另外的线程来处理数据。所以通常使用“local[n]”。提交到集群上,分配给Spark Streaming应用的核数要多于receiver,否则没有多余的核来处理数据。

文件流

文件流用于从外部文件创建输入Dtream,创建方式如StreamingContext.fileStream[KeyClass, ValueClass, InputFormatClass],它读取任何支持HDFS API的文件系统(HDFS、S3、NFS等)文件。

// 针对其他支持HDFS API的文件系统文件
streamingContext.fileStream[KeyClass, ValueClass, InputFormatClass](dataDirectory)
// 针对文本文件,最简单的方式
streamingContext.textFileStream(dataDirectory)

文件必须格式相同
文件进入dataDirectory的能被处理的正确方式:移动或者重命名
一旦文件被处理完成,即便修改了,新数据也不会再读取

RDD流

RDD流一般用于测试,使用streamingContext.queueStream(queueOfRDDs)来创建输入DStream。

val queueOfRDDs = new mutable.Queue[RDD[Int]]()

// 创建DStream
val inputDStream = ssc.queueStream(queueOfRDDs,oneAtATime = false)
自定义数据源

首先需要实现一个自定义Receiver,通过实现Receiver接口并实现onStart()、onStop()方法

class CustomReceiver(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_AND_DISK_2) with Logging {

  def onStart() {
    // 开启一个线程用于接收数据
    new Thread("Socket Receiver") {
      override def run() { receive() }
    }.start()
  }

  def onStop() {
    // 这里没什么要做的,因为receive()内while循环条件isStopped返回false时自行停止
  }

  // 创建一个socket连接接收数据,知道receiver停止
  private def receive() {
    var socket: Socket = null
    var userInput: String = null
    try {
      socket = new Socket(host, port)

      // 一直运行,知道停止或连接中断
      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("Trying to connect again")
    } catch {
      case e: java.net.ConnectException =>
        restart("Error connecting to " + host + ":" + port, e)
      case t: Throwable =>
        restart("Error receiving data", t)
    }
  }
}

然后在Spark Streaming程序中使用自定义receiver

val customReceiverStream = ssc.receiverStream(new CustomReceiver(host, port))
val words = customReceiverStream.flatMap(_.split(" "))
...

DStreams转换操作

下面常用转换功能跟RDD类似

转换 意义
map(func) 映射
flatMap(func) 一个元素拆分成一个序列
filter(func) 过滤
repartition(numPartitions) 重分区
union(otherStream) 两个DStream的并集
count() 统计DStream中每个RDD的元素个数,并返回一个DStream
reduce(func) 规约DStream中每个RDD的值为一个单值
countByValue() 对数据类型为K的DStream调用此方法,返回一个新(K, Long) 对的DStream,其中long值为每个key在RDD中出现的个数
reduceByKey(func, [numTasks]) 按key规约
join(otherStream, [numTasks]) 两个DStream做连接,返回一个 (K, (V, W)) 对的新DStream
cogroup(otherStream, [numTasks]) 两个DStream联合分组,返回一个 (K, Seq[V], Seq[W]) 对的新DStream
* UpdateStateByKey 转换

updateStateByKey转换使我们可以用新数据连续的更新被保存的任意状态。通过下面两步来使用这个功能:

  1. 定义状态。状态是一个任意的数据类型。
  2. 定义状态更新函数。状态更新函数定义了如何使用老状态和新输入进行更新。
    使用updateStateByKey需要对检查点目录进行配置,会使用检查点来保存状态。
// 定义更新状态方法,参数values为当前批次单词频度,state为以往批次单词频度
val updateFunc = (values: Seq[Int], state: Option[Int]) => {
  val currentCount = values.foldLeft(0)(_ + _)
  val previousCount = state.getOrElse(0)
  Some(currentCount + previousCount)
}
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
val pairs = words.map(word => (word, 1))

// 使用updateStateByKey来更新状态,统计从运行开始以来单词总的次数
val stateDstream = pairs.updateStateByKey[Int](updateFunc)
stateDstream.print()
* Transform 转换

Transform允许在DStream上执行任意的RDD-to-RDD函数。如果在DStream的API中没有相应的RDD操作,通过该函数扩展Spark RDD API。每一批次调度一次。

// 包含spam的RDD
val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) 

val cleanedDStream = wordCounts.transform { rdd =>
  // 将spam与数据Rdd做连接进行数据清晰 
  rdd.join(spamInfoRDD).filter(...) 
  ...
}
* 窗口转换

Spark Streaming提供一种窗口计算,允许你再一个滑动的窗口内做跨越多个批次的转换操作。
SparkStreaming-DStream_第4张图片
每一次滑动窗口,落入窗口内的RDDs被合并、处理产生出一个新的RDDs。就像上例,每个窗口整合三个单位的数据,并且滑动两次。这说明窗口转换需要两个参数:

  • 窗口长度。比如图中为3
  • 滑动间隔。比如图中为2
    这两个参数必须是DStream批次间隔(图中为1)的整数倍。
// 每10s规约一次最近30s内的数据
val windowedWordCounts = pairs.reduceByKeyAndWindow((a:Int,b:Int) => (a + b), Seconds(30), Seconds(10))

其他窗口转换:

窗口转换 意义
window(windowLength, slideInterval) 创建基于窗口批次的新DStream
countByWindow(windowLength, slideInterval) 窗口内元素的个数
reduceByWindow(func, windowLength, slideInterval) 按func规约窗口内的值
reduceByKeyAndWindow(func, windowLength, slideInterval, [numTasks]) 针对键值对数据,按func规约窗口内的值
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks]) 上一个的高效版本,利用上一个窗口的结果,从而达到增量计算的目的。就是多了一个反相计算的函数
countByValueAndWindow(windowLength, slideInterval, [numTasks]) 返回窗口内不同类型值的个数

连接操作

  • Stream-stream 连接
val stream1: DStream[String, String] = ...
val stream2: DStream[String, String] = ...
val joinedStream = stream1.join(stream2)
val windowedStream1 = stream1.window(Seconds(20))
val windowedStream2 = stream2.window(Minutes(1))
val joinedStream = windowedStream1.join(windowedStream2)
  • Stream-dataset 连接
val dataset: RDD[String, String] = ...
val windowedStream = stream.window(Seconds(20))...
val joinedStream = windowedStream.transform { rdd => rdd.join(dataset) }

DStream API
PairDStream API

输出操作

print()

在driver上打印DStream中每一批数据的最开始10个元素。

saveAsTextFiles(prefix, [suffix])

以文本文件形式存储DStream的数据。每一批的存储文件名基于参数中的prefix和suffix,如”prefix-Time_IN_MS[.suffix]”。

saveAsObjectFiles(prefix, [suffix])

以Java对象序列化的方式将DStream中的数据保存为 SequenceFiles。每一批的存储文件名基于参数,比如为"prefix-TIME_IN_MS[.suffix]"。

saveAsHadoopFiles(prefix, [suffix])

保存为Hadoop files。每一批的文件名基于参数,比如为"prefix-TIME_IN_MS[.suffix]"。

foreachRDD(func)

将函数 func 用于DStream的每一个RDD。其中传入函数func应实现将每一个RDD中数据存储到外部系统,如存入文件或者写入数据库。

// 常见错误用法一
dstream.foreachRDD { rdd =>
  // 在 driver上执行
  val connection = createNewConnection()  
  rdd.foreach { record =>
	// 在 worker上执行。这时会把connection对象序列化发送到work节点上,
	// 但connection不能序列化,会报初始化错误或不能序列化
    connection.send(record) 
  }
}
// 常见错误用法二
dstream.foreachRDD { rdd =>
  rdd.foreach { record =>
  	// 此时,在worker上创建连接。但每一条记录都创建一次连接,资源消耗大,不必要
    val connection = createNewConnection()
    connection.send(record)
    connection.close()
  }
}
// 不完美用法
dstream.foreachRDD { rdd =>
  // 为每一个分区创建一个连接。一个连接处理一个分区的数据。
  rdd.foreachPartition { partitionOfRecords =>
    val connection = createNewConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    connection.close()
  }
}
// 最佳实践
dstream.foreachRDD { rdd =>
  // 为每一个分区创建一个连接。一个连接处理一个分区的数据。
  rdd.foreachPartition { partitionOfRecords =>
    // 将连接池化。ConnectionPool 是一个静态、懒加载的连接池
    val connection = ConnectionPool.getConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    // 使用完连接以后退还给连接池,下次复用
    ConnectionPool.returnConnection(connection)  
  }
}
// 优化以后的最佳实践
dstream.foreachRDD { rdd =>
  // 为每一个分区创建一个连接。一个连接处理一个分区的数据。
  rdd.foreachPartition { partitionOfRecords =>
    // 将连接池化。ConnectionPool 是一个静态、懒加载的连接池
    val connection = ConnectionPool.getConnection()
    var recordList = partitionOfRecords.toList
    // 批量发送
    connection.sendBatch(recordList)
    // 使用完连接以后退还给连接池,下次复用
    ConnectionPool.returnConnection(connection)
  }
}

你可能感兴趣的:(Spark)