1 Spark Streaming是什么
Spark Streaming 是 Spark 核心 API 的扩展, 用于构建弹性, 高吞吐量, 容错的在线数据流的流式处理程序. 总之一句话: Spark Streaming 用于流式数据的处理
在 Spark Streaming 中,处理数据的单位是一批而不是单条,而数据采集却是逐条进行的,因此 Spark Streaming 系统需要设置间隔使得数据汇总到一定的量后再一并操作,这个间隔就是批处理间隔。批处理间隔是 Spark Streaming 的核心概念和关键参数,它决定了 Spark Streaming 提交作业的频率和数据处理的延迟,同时也影响着数据处理的吞吐量和性能。
2 DStream
Discretized Stream(DStream) 是 Spark Streaming 提供的基本抽象, 表示持续性的数据流, 可以来自输入数据, 也可以是其他的 DStream 转换得到. 在内部, 一个 DSteam 用连续的一系列的 RDD 来表示. 在 DStream 中的每个 RDD 包含一个确定时间段的数据
2.1 wordcount案例
使用 netcat 工具向 9999 端口不断的发送数据,通过 Spark Streaming 读取端口数据并统计不同单词出现的次数
2.2 添加依赖
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_2.11</artifactId>
<version>2.1.1</version>
</dependency>
2.3 代码如下
import org.apache.spark.SparkConf
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object StreamingWordCount {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("StreamingWordCount").setMaster("local[2]")
// 1. 创建SparkStreaming的入口对象: StreamingContext 参数2: 表示事件间隔 内部会创建 SparkContext
val ssc = new StreamingContext(conf, Seconds(5))
// 2. 创建一个DStream
val lines: ReceiverInputDStream[String] = ssc.socketTextStream("192.168.199.101", 9999)
// 3.切分单词
val words: DStream[String] = lines.flatMap(_.split(" "))
// 4.单词组成元组
val wordAndOne: DStream[(String, Int)] = words.map(x => (x, 1))
// 5.统计单词个数
val count: DStream[(String, Int)] = wordAndOne.reduceByKey(_ + _)
// 6.显示
count.print()
// 7.开始接收数据
ssc.start()
//8. 等待计算结束(要么手动退出,要么出现异常)才退出主程序
ssc.awaitTermination()
}
}
2.4 在linux服务器上启动netcat
nc -lk 9999
2.5 运行streamning
可以看到会每隔5秒钟去采集数据并展示出来
2.6执行流程
3 DStream 创建
Spark Streaming 原生支持一些不同的数据源。
3.1 RDD队列
测试过程中,可以通过使用ssc.queueStream(queueOfRDDs)来创建DStream,每一个推送到这个队列中的RDD,都会作为一个DStream处理。
import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable
object RDDQueueDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("RDDQueueDemo").setMaster("local[2]")
// 1. 创建SparkStreaming的入口对象: StreamingContext 参数2: 表示事件间隔 内部会创建 SparkContext
val ssc: StreamingContext = new StreamingContext(conf, Seconds(5))
val sc: SparkContext = ssc.sparkContext
// 2. 创建一个可变队列
val queue: mutable.Queue[RDD[Int]] = mutable.Queue[RDD[Int]]()
val rddDs: InputDStream[Int] = ssc.queueStream(queue, true)
rddDs.reduce(_ + _).print()
// 3.开始接收数据
ssc.start()
// 4.循环的方式向队列中添加 RDD
for (elem <- 1 to 5) {
queue += sc.parallelize(1 to 100 - elem)
Thread.sleep(2000)
}
// 5.等待计算结束(要么手动退出,要么出现异常)才退出主程序
ssc.awaitTermination()
}
}
3.2 自定义数据源
其实就是自定义接收器
需要继承Receiver,并实现onStart、onStop方法来自定义数据源采集。
自定义数据源,实现监控某个端口号,获取该端口号内容。
import java.io.{BufferedReader, InputStreamReader}
import java.net.Socket
import java.nio.charset.StandardCharsets
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.receiver.Receiver
object MySource {
def apply(host: String, port: Int): MySource = new MySource(host, port)
}
class MySource(host: String, port: Int) extends Receiver[String](StorageLevel.MEMORY_ONLY) {
//开始接收数据
override def onStart(): Unit = {
// 启动一个新的线程来接收数据
new Thread() {
override def run(): Unit = {
receive()
}
}.start()
}
// 此方法用来接收数据
def receive(): Unit = {
val socket: Socket = new Socket(host, port)
val reader: BufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))
var line: String = null
while (!isStopped && (line = reader.readLine()) != null) {
//将数据发送给spark
store(line)
}
//关流
reader.close()
socket.close()
// 重启任务
restart("Trying to connect again")
}
override def onStop(): Unit = {
}
}
代码实现
import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object MySourceDemo {
def main(args: Array[String]): Unit = {
System.setProperty("hadoop.home.dir", "F:\\hadoop\\hadoop-2.7.2")
val conf = new SparkConf().setAppName("MySourceDemo").setMaster("local[2]")
// 1. 创建SparkStreaming的入口对象: StreamingContext 参数2: 表示事件间隔 内部会创建 SparkContext
val ssc: StreamingContext = new StreamingContext(conf, Seconds(5))
// 2. 监听端口
val lines: ReceiverInputDStream[String] = ssc.receiverStream(MySource("192.168.199.101", 9999))
//3.处理数据
val words: DStream[String] = lines.flatMap(_.split(" "))
val wordAndOne: DStream[(String, Int)] = words.map(x => (x, 1))
val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_ + _)
//4.显示结果
result.print()
//5.开始接收数据
ssc.start()
//6.等待计算结束(要么手动退出,要么出现异常)才退出主程序
ssc.awaitTermination()
ssc.stop(false)
}
}
3.3 Kafka 数据源
在工程中需要引入 Maven 依赖 spark-streaming-kafka_2.11来使用它。
包内提供的 KafkaUtils 对象可以在 StreamingContext和JavaStreamingContext中以你的 Kafka 消息创建出 DStream。
两个核心类:KafkaUtils、KafkaCluster
导入依赖:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-8_2.11</artifactId>
<version>2.1.1</version>
</dependency>
3.3.1高级API
import kafka.serializer.StringDecoder
import org.apache.kafka.clients.consumer
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}
object kafkaDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("kafkaDemo").setMaster("local[2]")
val ssc: StreamingContext = new StreamingContext(conf, Seconds(5))
//kafka配置参数
//kafka参数声明
val brokers = "master-1:9092,master-2:9092,slave-1:9092"
val topic = "first"
val group = "bigdata"
val deserialization = "org.apache.kafka.common.serialization.StringDeserializer"
val kafkaParams = Map(
ConsumerConfig.GROUP_ID_CONFIG -> group,
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> brokers,
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> deserialization,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> deserialization
)
// val dStream = KafkaUtils.createDirectStream(ssc,kafkaParams,Set(topic))
val dStream: InputDStream[(String, String)] = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](
ssc, kafkaParams, Set(topic))
dStream.print()
//数据处理
val wordAndOne: DStream[(String, Int)] = dStream.flatMap({
x => x._2.split(" ")
}).map(x => (x, 1))
val wordAndCount: DStream[(String, Int)] = wordAndOne.reduceByKey(_ + _)
wordAndCount.print()
// dStream.print()
//开始接收数据
ssc.start()
// 等待计算结束(要么手动退出,要么出现异常)才退出主程序
ssc.awaitTermination()
}
}
执行结果:
4 DStream转换
DStream 上的原语与 RDD 的类似,分为Transformations(转换)和Output Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()、transform()以及各种Window相关的原语。
4.1 无状态转换操作
无状态转化操作就是把简单的RDD转化操作应用到每个批次上,也就是转化DStream中的每一个RDD。部分无状态转化操作列在了下表中。
4.2 有状态转换操作
4.2.1 updateStateByKey
updateStateByKey操作允许在使用新信息不断更新状态的同时能够保留他的状态.
需要做两件事情:
- 定义状态. 状态可以是任意数据类型
- 定义状态更新函数. 指定一个函数, 这个函数负责使用以前的状态和新值来更新状态.
import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
object StreamingWordCount02 {
def main(args: Array[String]): Unit = {
System.setProperty("hadoop.home.dir", "F:\\hadoop\\hadoop-2.7.2")
System.setProperty("HADOOP_USER_NAME", "root")
val conf = new SparkConf().setAppName("StreamingWordCount02").setMaster("local[2]")
val ssc: StreamingContext = new StreamingContext(conf, Seconds(5))
val dStream: ReceiverInputDStream[String] = ssc.socketTextStream("master-1", 9999, StorageLevel.MEMORY_ONLY)
val wordAndOne: DStream[(String, Int)] = dStream.flatMap(_.split("\\W+")).map((_, 1))
// 设置检查点: 使用updateStateByKey必须设置检查点
ssc.sparkContext.setCheckpointDir("hdfs://master-1:9000/checkpoint2")
// val result: DStream[(String, Int)] = wordAndOne.reduceByKey(_ + _)
val result: DStream[(String, Int)] = wordAndOne.updateStateByKey[Int](updateFunction _)
result.print()
ssc.start()
ssc.awaitTermination()
}
def updateFunction(newValue: Seq[Int], runningCount: Option[Int]): Option[Int] = {
// 新的总数和状态进行求和操作
// a /: b表示a元素逐次与b数组中所有数执行后面()中的内容
val newCount: Int = (0 /: newValue) (_ + _) + runningCount.getOrElse(0)
Some(newCount)
}
}
执行结果:
4.2.2 window 操作
Spark Streaming 也提供了窗口计算, 允许执行转换操作作用在一个窗口内的数据.
默认情况下, 计算只对一个时间段内的RDD进行, 有了窗口之后, 可以把计算应用到一个指定的窗口内的所有 RDD 上.
一个窗口可以包含多个时间段. 基于窗口的操作会在一个比StreamingContext的批次间隔更长的时间范围内,通过整合多个批次的结果,计算出整个窗口的结果。
观察上图, 窗口在 DStream 上每滑动一次, 落在窗口内的那些 RDD会结合在一起, 然后在上面操作产生新的 RDD, 组成了 window DStream.
在上面图的情况下, 操作会至少应用在 3 个数据单元上, 每次滑动 2 个时间单位. 所以, 窗口操作需要 2 个参数:
- 窗口长度 – 窗口的持久时间(执行一次持续多少个时间单位)(图中是 3)
- 滑动步长 – 窗口操作被执行的间隔(每多少个时间单位执行一次).(图中是 2 )
注意: 这两个参数必须是源 DStream 的 interval 的倍数.
4.2.2.1 reduceByKeyAndWindow
reduceByKeyAndWindow(reduceFunc: (V, V) => V, windowDuration: Duration)
val wordAndOne: DStream[(String, Int)] = words.map((_, 1))
/*
参数1: reduce 计算规则
参数2: 窗口长度
参数3: 窗口滑动步长. 每隔这么长时间计算一次.
*/
val count: DStream[(String, Int)] =
wordAndOne.reduceByKeyAndWindow((x: Int, y: Int) => x + y,Seconds(15), Seconds(10))
4.2.2.2 reduceByKeyAndWindow
reduceByKeyAndWindow(reduceFunc: (V, V) => V, invReduceFunc: (V, V) => V, windowDuration: Duration, slideDuration: Duration)
比没有invReduceFunc高效. 会利用旧值来进行计算.
invReduceFunc: (V, V) => V 窗口移动了, 上一个窗口和新的窗口会有重叠部分, 重叠部分的值可以不用重复计算了. 第一个参数就是新的值, 第二个参数是旧的值.
ssc.sparkContext.setCheckpointDir("hdfs://hadoop201:9000/checkpoint")
val count: DStream[(String, Int)] =
wordAndOne.reduceByKeyAndWindow((x: Int, y: Int) => x + y,(x: Int, y: Int) => x - y,Seconds(15), Seconds(10))
4.2.2.3 window
window(windowLength, slideInterval)
基于对源 DStream 窗化的批次进行计算返回一个新的 Dstream
4.2.2.4 countByWindow
countByWindow(windowLength, slideInterval)
返回一个滑动窗口计数流中的元素的个数。
4.2.2.5 countByValueAndWindow
countByValueAndWindow(windowLength, slideInterval, [numTasks])
对(K,V)对的DStream调用,返回(K,Long)对的新DStream,其中每个key的的对象的v是其在滑动窗口中频率。如上,可配置reduce任务数量。