Transformation:
和RDD的类似,有几个需要单独学习: transform、UpdateStateByKey、Window Operations
Output/Action:
和RDD的类似,有几个需要单独学习:print、foreachRDD(func)
UpdateStateByKey
package cn.itcast.spark.stream
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
/**
* Author itcast
* Date 2019/9/24 10:39
* Desc 演示使用SparkStreaming接收来自Socket的数据(ip:node01,port:9999),并对所有数据进行聚合,使用updateStateByKey
*/
object WordCount2 {
def main(args: Array[String]): Unit = {
//1.创建StreamingContext
//spark.master should be set as local[n], n > 1 in local mode
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc, Seconds(5))//Seconds(5)表示每隔5秒对数据进行批次的划分
//The checkpoint directory has not been set. Please set it by StreamingContext.checkpoint().
//注意:根据错误提示需要设置一个checkpoint目录,因为我们要对历史值进行聚合,历史值存在哪里?需要给它设置一个目录
ssc.checkpoint("./ssc") //实际开发写HDFS
//2.使用ssc从Socket接收数据
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//3.对数据进行处理
//3.1切分
val wordDStream: DStream[String] = dataDStream.flatMap(_.split(" "))//_表示发送过来的每一行数据
//3.2每个单词记为1
val wordAndOneDStream: DStream[(String, Int)] = wordDStream.map((_, 1))//_表示每个单词
//3.3聚合--对所有数据进行聚合,使用updateStateByKey
//val result: DStream[(String, Int)] = wordAndOneDStream.reduceByKey(_+_)
//updateFunc: (Seq[V], Option[S]) => Option[S]
val result: DStream[(String, Int)] = wordAndOneDStream.updateStateByKey(updateFunc)
//3.4输出结果
result.print()
ssc.start()//开启
ssc.awaitTermination()//等待终止
}
/**
* 定义一个updateFunc方法用来聚合所有的数据(将历史值+当前这一批次的值)
* @param currentValues 当前这一批次的值[1,1,1,.....]
* @param historyValue 历史值,第一次没有为None,我们应该把它设置为0,所以应该用getOrElse(0)
* @return
*/
def updateFunc(currentValues:Seq[Int], historyValue:Option[Int]):Option[Int]={
val result: Int = historyValue.getOrElse(0) + currentValues.sum
Some(result)
}
}
reduceByKeyAndWindows
package cn.itcast.spark.stream
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
/**
* Author itcast
* Date 2019/9/24 10:39
* Desc 演示使用SparkStreaming接收来自Socket的数据(ip:node01,port:9999),
* 使用窗口操作,统计最近10s的数据,每隔5s统计一次
*/
object WordCount3 {
def main(args: Array[String]): Unit = {
//1.创建StreamingContext
//spark.master should be set as local[n], n > 1 in local mode
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc, Seconds(5))//Seconds(5)表示每隔5秒对数据进行批次的划分
//The checkpoint directory has not been set. Please set it by StreamingContext.checkpoint().
//注意:根据错误提示需要设置一个checkpoint目录,因为我们要对历史值进行聚合,历史值存在哪里?需要给它设置一个目录
ssc.checkpoint("./ssc") //实际开发写HDFS
//2.使用ssc从Socket接收数据
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//3.对数据进行处理
//3.1切分
val wordDStream: DStream[String] = dataDStream.flatMap(_.split(" "))//_表示发送过来的每一行数据
//3.2每个单词记为1
val wordAndOneDStream: DStream[(String, Int)] = wordDStream.map((_, 1))//_表示每个单词
//3.3聚合--对所有数据进行聚合,使用updateStateByKey
//val result: DStream[(String, Int)] = wordAndOneDStream.reduceByKey(_+_)
//updateFunc: (Seq[V], Option[S]) => Option[S]
//val result: DStream[(String, Int)] = wordAndOneDStream.updateStateByKey(updateFunc)
//=====================================================================
//使用窗口操作,统计最近10s的数据,每隔5s统计一次
// @param windowDuration 窗口的长度 10s
// width of the window; must be a multiple of this DStream's batching interval
// @param slideDuration 滑动间隔 5s
// sliding interval of the window (i.e., the interval after which the new DStream will generate RDDs); must be a multiple of this DStream's batching interval
//windowDuration>slideDuration 部分数据会被重复(重新)计算,实际中用的最多
val result: DStream[(String, Int)] = wordAndOneDStream.reduceByKeyAndWindow((a:Int,b:Int)=>a+b,Seconds(10),Seconds(5))
//windowDuration=slideDuration 数据不会被重新计算也不会丢失,实际中偶尔会用
//windowDuration(10)<slideDuration(15) 部分数据会丢失不参与计算,实际中基本不会使用
//这里需要注意:以后开发中需要学会设置windowDuration和slideDuration这两个参数:
//如:广告流点击预测,统计最近1小时的广告点击数据,每隔1分钟更新一次
//那么windowDuration = Seconds(3600) , slideDuration=Seconds(60)
//或者windowDuration = Minutes(60) , slideDuration=Minutes(1)
//3.4输出结果
result.print()
ssc.start()//开启
ssc.awaitTermination()//等待终止
}
}
模拟百度热搜求topN
package cn.itcast.spark.stream
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
/**
* Author itcast
* Date 2019/9/24 10:39
* Desc 演示使用SparkStreaming接收来自Socket的数据(ip:node01,port:9999),
* 使用窗口操作,模拟百度热搜词汇排行榜TopN
* 统计最近10s的数据,每隔5s统计一次
*/
object WordCount4 {
def main(args: Array[String]): Unit = {
//1.创建StreamingContext
//spark.master should be set as local[n], n > 1 in local mode
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc, Seconds(5))//Seconds(5)表示每隔5秒对数据进行批次的划分
//The checkpoint directory has not been set. Please set it by StreamingContext.checkpoint().
//注意:根据错误提示需要设置一个checkpoint目录,因为我们要对历史值进行聚合,历史值存在哪里?需要给它设置一个目录
ssc.checkpoint("./ssc") //实际开发写HDFS
//2.使用ssc从Socket接收数据
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
//3.对数据进行处理
//3.1切分
val wordDStream: DStream[String] = dataDStream.flatMap(_.split(" "))//_表示发送过来的每一行数据
//3.2每个单词记为1
val wordAndOneDStream: DStream[(String, Int)] = wordDStream.map((_, 1))//_表示每个单词
//4.统计最近10s的数据,每隔5s统计一次
// windowDuration:10s
// slideDuration: 5s
val wordAndCountDStream: DStream[(String, Int)] = wordAndOneDStream.reduceByKeyAndWindow((a:Int,b:Int)=>a+b,Seconds(10),Seconds(5))
//5.求TopN
val result: DStream[(String, Int)] = wordAndCountDStream.transform(rdd => {
//5.1排序
//_表示(单词, 数量) , _._2表示按照数量排序 ,false表示逆序
val sortRDD: Array[(String, Int)] = rdd.sortBy(_._2, false).take(3)
println("==========top3========")
sortRDD.foreach(println)
println("==========top3========")
rdd
})
//输出结果
//注意:上面的代码没有涉及到DStream的Action操作,会报错:No output operations registered, so nothing to execute
//所以下面的代码不能省略
result.print()
ssc.start()//开启
ssc.awaitTermination()//等待终止
}
}
使用SpartStreaming整合kafka
1.★面试题:Receiver & Direct
开发中我们经常会利用SparkStreaming实时地读取kafka中的数据然后进行处理,在spark1.3版本后,kafkaUtils里面提供了两种创建DStream的方法:
1.Receiver接收方式:
KafkaUtils.createDstream(开发中不用,了解即可,但是面试可能会问)
Receiver作为常驻的Task运行在Executor等待数据,但是一个Receiver效率低,需要开启多个,再手动合并数据(union),再进行处理,很麻烦
Receiver那台机器挂了,可能会丢失数据,所以需要开启WAL(预写日志)保证数据安全,那么效率又会降低!
Receiver方式是通过zookeeper来连接kafka队列,调用Kafka高阶API,offset存储在zookeeper,由Receiver维护,
spark在消费的时候为了保证数据不丢也会在Checkpoint中存一份offset,可能会出现数据不一致
所以不管从何种角度来说,Receiver模式都不适合在开发中使用了,已经淘汰了
2.Direct直连方式:
KafkaUtils.createDirectStream(开发中使用,要求掌握)
Direct方式是直接连接kafka分区来获取数据,从每个分区直接读取数据大大提高了并行能力
Direct方式调用Kafka低阶API(底层API),offset自己存储和维护,默认由Spark维护在checkpoint中,消除了与zk不一致的情况
当然也可以自己手动维护,把offset存在mysql、redis中
所以基于Direct模式可以在开发中使用,且借助Direct模式的特点+手动操作可以保证数据的Exactly once 精准一次
package cn.itcast.spark.stream
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.streaming.{Seconds, StreamingContext}
/**
* Author itcast
* Date 2019/9/24 16:11
* Desc 使用spark-streaming-kafka-0-10_2.11版本的Direct模式整合Kafka
*/
object SparkStreamingKafkaDemo {
def main(args: Array[String]): Unit = {
//1.准备环境
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc, Seconds(5))//Seconds(5)表示每隔5秒对数据进行批次的划分
//2.准备参数
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "node01:9092,node02:9092,node03:9092,",//Kafka集群地址
"key.deserializer" -> classOf[StringDeserializer], //key的反序列化类型
"value.deserializer" -> classOf[StringDeserializer],//value的反序列化类型
"group.id" -> "sparkstreaming", //消费者组名称
//earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
//latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
//none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
//这里配置latest自动重置偏移量为最新的偏移量,即如果有偏移量从偏移量位置开始消费,没有偏移量从新来的数据开始消费
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean)//false表示不开启自动提交zk/__consumer_offset中,默认由Spark手动提交到Checkpoint中或程序员手动提交
)
//3.主题
val topics = Array("spark_kafka")
//4.连接Kafka
//ssc: StreamingContext,
//locationStrategy: LocationStrategy,
//consumerStrategy: ConsumerStrategy[K, V],
val recordDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream[String, String](ssc,
LocationStrategies.PreferConsistent,//位置策略,直接使用源码中强烈推荐的PreferConsistent
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams)) //消费策略:直接使用源码中强烈推荐的Subscribe
//5.处理数据--获取value
val lineDStream: DStream[String] = recordDStream.map(_.value())//_表示从Kakfa消费到的每一条完整的消息(里面包含key/value/offset/topic)
//6.WordCount
val result: DStream[(String, Int)] = lineDStream.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
result.print()
ssc.start()
ssc.awaitTermination()
}
}
SparkStreaming整合kafka的面试题
1. 如何保证消息不丢失?
ü 生产者:消息确认机制ACK
0:不需要确认,消费可能丢失
1:Leader确认即可,消息一般来说不会丢失
-1:Leader和Follower都确认,消息绝对安全,但是效率低
ü 消费者:维护偏移量offset
先提交再消费:可能会出现offset提交了,但没有消费消息,程序这时挂了,则消息丢失
先消费再提交:可能会出现消息消费了,但没有提交,程序挂了,重启后会出现重复消费
2. 如何保证消息不被重复消费?
出现概率较低且一般业务如果对重复数据不敏感,则不需要处理(也可以在后期进行数据清洗时再处理)
但是涉及到钱相关的,或者其他对于数据要求精确性高的,则需要避免消息重复
可以在真正消费处理这个消息的时候,把消息的key存在外部容器中(如Redis),下次消费的时候,容器中有,则表示消费过了,就不再消费
3. 如何保证消息消费有序?
一个分区内部的消息本身就是有序的,但是不能保证全部分区的消息读取有序,即全局无序,区内有序
如果要保证消息消费有序,则只能有一个分区,但是就违背了kakfa通过分区提高并发的初衷
所以这个命题没有意义,仅仅是面试官难为你
4. Kafka为什么能够吞吐量那么高?
本身从软件架构设计上就是为高并发高吞吐所设计的:多broker/多分区/消息消费拉模式…
底层:写磁盘文件的时候顺序读写(Sequence IO)、页缓存(PageCache)、零拷贝机制(Sendfile)…
总之不管从软件架构上,还是从物理硬件原理上,Kafka都是一个天生为高并发、高吞吐所设计的!
**5. 读取消息时为什么只从Leader上去读?不从Follower上去读? **
因为kafka并发读已经由分区来实现了,如果还要保证一条消息写入到Leader后,能从Follower进行并发读,那么则需要保证Leader和Follower数据的实时一致性,这样写消息就慢了!