SparkStreaming是Spark的一个流式计算框架,它支持对许多数据源进行实时监听,例如Kafka, Flume, Kinesis, or TCP sockets,并实现实时计算的能力,但准确来说应该是伪实时,因为它的基本原理就是定时接收数据流,然后将其转化为许多量小的RDD集合,然后对其进行计算汇总,如下图:
SparkStreaming的流式计算其实可以分为很多种类,让我们一步步分开来看
无状态转化操作
第一种是DStream无状态转化操作。它的含义就是:只统计当前阶段的数据,不考虑历史数据。
比如我们下面这个例子,每3秒钟对kafka的数据进行实时消费,然后只对这3秒钟监听到的数据进行处理,不管以前接收的任何数据。
/**
* 建立spark streaming对kafka数据进行实时消费
* 无状态转换,只统计本阶段的数据,无关历史数据
*/
def DStream(): Unit = {
val conf = new SparkConf().setAppName("Kafka").setMaster("local[4]")
val ssc = new StreamingContext(conf, Seconds(3))
ssc.sparkContext.setLogLevel("ERROR")
// 检查点,检查点有容错机制
ssc.checkpoint("resources/data/kafka")
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "slave1:9092,slave2:9092,slave3:9092,spark:9092",
"group.id" -> "test",
// 指定反序列化的类
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean))
val topics = Array("topicA", "topicB")
val stream = KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent,
Subscribe[String, String](topics, kafkaParams)
)
// 消费kafka的数据
val lines = stream.map(record => (record.key, record.value))
val wordCount = lines.flatMap(lines => lines._2.split(" ")).
map(lines => (lines, 1)).reduceByKey(_ + _)
wordCount.print()
ssc.start()
ssc.awaitTermination()
}
有状态转化操作
第二种就是DStream有状态转化操作。与上述的不同,它可以将历史数据也考虑进来,跟当前的数据进行汇总。
它还可以分为两种:
- Window Operations:可以简单地理解为只对最近一段时间内的数据进行汇总统计;
- UpdateStateByKey Operation:可以对历史数据不断进行累计,以达到对历史全量数据的汇总。
Window Operations
如下图,这里设置窗口长度为3 time,窗口滑动间隔为2 time,怎么理解呢?
其实上面已经提过了,这个例子就是每隔2 time对最近3 time的数据进行处理,例如在time 3的时候,我们就是对time 1 + time 2 + time 3
3个窗口的数据进行汇总处理。
还有需要注意的是:这里设置的3 time和2 time必须是我们接收流数据的间隔的整数倍数,例如我们对kafka监听的时候,是每隔1 time就进行一次流式计算;另外一点:为了更好地理解,如果不是Window Operations的话,其实我们就是每隔1 time就进行一次数据处理,但因为这里使用了Window Operations并设置滑动间隔为2 time,那么数据处理的间隔就变为2 time,比如在time 4的时候,我们就不会进行数据处理操作(reducer)。
处理技巧
上图的例子中,我们可以看到,在time 3和time 5的时候,时间窗口都包含了time 3,那么就会对time 3的数据重复计算,
(需要强调一下:这里说的重复计算只是说time 3在上次已经reduce了,这次会重新再reduce一次,但spark不会在对time3在同个窗口中统计两次)
当时间窗口的长度远远大于窗口滑动时间间隔的时候,就会存在相当多的重复计算,这样既浪费时间也浪费了空间,但spark为我们提供一个解决方法:
比如:reduceByWindow(_ + _, Seconds(3), Seconds(2))
我们可以换成reduceByWindow(_ + _, _ - _, Seconds(3), Seconds(2)
这个怎么理解呢?在time 3的时候,第一个窗口win1 = time 1 + time 2 + time 3
,在time 5的时候,如果第一种reduceByWindow,那么就是win2 = time 3 + time 4 + time 5
,那么如果是第二种,就会变成这样的计算流程:win2 = win1 + time 4 + time 5 - time 1 - time 2
,即将保留上次窗口的结果 + 本次窗口新加入的RDDs - 退出本次窗口的RDDs,很容易可以看出在时间窗口的长度远远大于窗口滑动时间间隔的时候可以减少很多不必要的重复计算过程,提升效率。
def windowedDStream(): Unit ={
/**
* 监听socket端口,对Window Operations进行测试
*/
val conf = new SparkConf().setMaster("local[4]").setAppName("SparkSql")
val sc = new SparkContext(conf)
sc.setLogLevel("ERROR")
val ssc = new StreamingContext(sc, Seconds(1))
val lines = ssc.socketTextStream("localhost", 9999, StorageLevel.MEMORY_AND_DISK_SER)
// window length :3,sliding length :2
// 必须是上面Seconds(1)的整数倍
// 每隔2秒钟对最近3秒的数据进行一次reduce操作
val wordCount = lines.map(x => x.toLong).reduceByWindow(_ + _, Seconds(3), Seconds(2))
wordCount.print()
ssc.start()
ssc.awaitTermination()
}
socket端口就是简单的从0开始每秒+1,将整数写入端口
object SocketServer {
private val rd = new java.util.Random()
def rdInt(max: Int): Int ={
rd.nextInt(max)
}
def main(args: Array[String]): Unit = {
val port = 9999 // socket端口号
val interval = 1000 // 读取文件内容的时间间隔:毫秒
var num = 0
val listener = new ServerSocket(port)
while (true){ // 一直监听该socket端口
val socket = listener.accept()
new Thread(){
override def run = {
val out = new PrintWriter(socket.getOutputStream, true)
while (true){
Thread.sleep(interval)
println(num)
out.write(num + "\n")
out.flush()
num += 1
}
socket.close()
}
}.start()
}
}
}
UpdateStateByKey Operation
历史数据累计:即每次都会将上次的结果和本次的结果进行汇总,以达到累计的效果
/**
* 建立spark streaming对kafka数据进行实时消费
* 有状态转换,对历史数据进行累计
*/
def DStreamState(): Unit ={
/**
* 状态更新函数
* 用于updateStateByKey函数,对历史数据进行累计,然后更新到当前状态
* values:当前阶段对应key的value集合
* state:历史状态对应key是否存在
*/
val updateFunc = (values: Seq[Int], state: Option[Int]) => {
val currentValue = values.foldLeft(0)(_ + _) // 当前阶段的value-reduce操作
val previousValue = state.getOrElse(0) // 如果历史存在则返回原value,不存在则赋值0
Some(currentValue + previousValue) // 历史和现阶段累加并返回
}
val conf = new SparkConf().setAppName("Kafka").setMaster("local[4]")
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("ERROR")
// 检查点,检查点有容错机制
ssc.checkpoint("resources/data/kafka")
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "slave1:9092,slave2:9092,slave3:9092,spark:9092",
"group.id" -> "test",
// 指定反序列化的类
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean))
val topics = Array("topicA", "topicB")
val stream = KafkaUtils.createDirectStream[String, String](
ssc,
PreferConsistent,
Subscribe[String, String](topics, kafkaParams)
)
// 消费kafka的数据
val lines = stream.map(record => (record.key, record.value))
val wordCount = lines.flatMap(lines => lines._2.split(" ")).
map(lines => (lines, 1))
val stateCount = wordCount.updateStateByKey[Int](updateFunc)
stateCount.print()
ssc.start()
ssc.awaitTermination()
}
完整的所有代码已上传至GitHub无状态转换操作 && UpdateStateByKey Operation
Window Operations
欢迎关注同名公众号:“我就算饿死也不做程序员”。
交个朋友,一起交流,一起学习,一起进步。