Spark Streaming是一个基于Spark Core之上的实时计算框架,可从Kafka、Flume、Twitter等很多数据源消费数据并对数据进行秒级实时的处理。
Spark Streaming 是基于spark的流式批处理引擎,其基本原理是把输入数据以某一时间间隔批量的处理(微批次),当批处理间隔缩短到秒级时,便可以用于处理实时数据流。其实就是把流式计算当作一系列连续的小规模批处理来对待, 每个输入批次都形成一个RDD!
Spark Streaming中,会有一个接收器组件Receiver,作为一个长期运行的task跑在一个Executor上。
概念很多,没总结完…
#在linux服务器上安装nc工具(nc命令是netcat命令的简称,原本是用来设置路由器.,我们可以利用它向某个端口发送数据)
yum install -y nc
#启动一个服务端并开发9999端口,等一下往这个端口发数据
nc -lk 9999
注意:
1. 源码中要求: spark.master should be set as local[n],n > 1,StreamingContext的cores(核)配置, 一个StreamingContext创建多个input Dstream,会创建多个Receiver,Spark会为每个Receiver 分配一个core用于其运行。其余的core用于真正的计算。
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
//使用Spark监听Soket:node01:9999发送过来的数据, 并做wordcount
object MyWordCount {
def main(args: Array[String]): Unit = {
//spark.master should be set as local[n], n > 1
val conf: SparkConf = new SparkConf()
.setAppName("wc")
.setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
//创建Spark StreamingContext(Spark流式处理上下文对象),
// Seconds(5)表示每5秒对数据进行切分形成一个RDD
val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))
//监听Socket接收数据
//ReceiverInputStream就是接收到的所有的数据组成的RDD,封装成了DStream
val dataDStream: ReceiverInputDStream[String] =
ssc.socketTextStream("node01",9999)
//接下来对DStream进行操作
val word_count: DStream[(String, Int)] = dataDStream.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
word_count.print()
ssc.start()//开启
ssc.awaitTermination()//等待优雅停止
}
}
注意:
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
//将监听到到的数据累加, 并做wordcount
object MyWordCount2 {
def main(args: Array[String]): Unit = {
//spark.master should be set as local[n], n > 1
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))
/*
不设置容错会报错: requirement failed: ....Please set it by StreamingContext.checkpoint().
注意:我们在下面使用到了updateStateByKey对当前数据和历史数据进行累加
那么历史数据存在哪?我们需要给他设置一个checkpoint目录
"./wc"的实际目录是:"D:\idea\Spark_20190804\wc_test\"
*/
ssc.checkpoint("./wc")//开发中写的是HDFS的文件目录
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
val word_count_DS: DStream[(String, Int)] = dataDStream.flatMap(_.split(" ")).map((_,1))
/*=================使用updateStateByKey对当前数据和历史数据进行累加=================*/
val wordAndCount: DStream[(String, Int)] = word_count_DS.updateStateByKey(updateFunc)
wordAndCount.print()
ssc.start()//开启
ssc.awaitTermination()//等待优雅停止
}
// currentValues:当前批次的value值,如:1,1,1 (以测试数据中的hadoop为例)
// historyValue:之前累计的历史值,第一次没有值是0,第二次是3
// 目标是把当前数据+历史数据返回作为新的结果(下次的历史数据)
def updateFunc(currentValues:Seq[Int],historyValue:Option[Int]):Option[Int]={
val result: Int = currentValues.sum+historyValue.getOrElse(0)
Some(result)
}
}
源码解析:
def reduceByKeyAndWindow(//函数名
reduceFunc : scala.Function2[V, V, V], //聚合函数, 此需求也是当前批数据与历史数据相加
windowDuration : org.apache.spark.streaming.Duration, //窗口长度/宽度
slideDuration : org.apache.spark.streaming.Duration)//窗口滑动间隔
:org.apache.spark.streaming.dstream.DStream[scala.Tuple2[K, V]]//返回值还是DStream
= {/* compiled code */ }//函数体
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
//每隔5秒计算最近10秒的数据
object MyWordCount3 {
def main(args: Array[String]): Unit = {
//spark.master should be set as local[n], n > 1
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
val word_count_DS: DStream[(String, Int)] = dataDStream.flatMap(_.split(" ")).map((_,1))
/*=====================使用窗口函数进行WordCount计数=====================*/
val wordAndCount: DStream[(String, Int)] = word_count_DS.reduceByKeyAndWindow((a:Int, b:Int) =>a+b,Seconds(10),Seconds(5))
wordAndCount.print()
ssc.start()//开启
ssc.awaitTermination()//等待优雅停止
}
}
需求: 模拟百度热搜排行榜统计最近10s的热搜词Top3,每隔5秒计算一次
香港机场实施出入管制 香港机场实施出入管制 香港机场实施出入管制 香港机场实施出入管制
太阳下吵架半小时双双中暑 太阳下吵架半小时双双中暑 太阳下吵架半小时双双中暑
首个台风红色预警
告全体香港青年 告全体香港青年 告全体香港青年 告全体香港青年 告全体香港青年
注意:
sortedDStream.print()
是DStream的Output/Action操作, 必须要有, 否则报错: No output operations registered, so nothing to execute
;import org.apache.spark.rdd.RDD
import org.apache.spark.streaming.dstream.{DStream, ReceiverInputDStream}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
//拟百度热搜排行榜统计最近10s的热搜词Top3,每隔5秒计算一次
object BaiduTopN {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))
val dataDStream: ReceiverInputDStream[String] = ssc.socketTextStream("node01",9999)
val word_count_DS: DStream[(String, Int)] = dataDStream.flatMap(_.split(" ")).map((_,1))
/*=====================使用窗口函数进行WordCount计数=====================*/
val wordAndCount: DStream[(String, Int)] = word_count_DS.reduceByKeyAndWindow((a:Int, b:Int) =>a+b,Seconds(10),Seconds(5))
//排序操作(有个foreachRDD方法更好用): DStreame没有排序方法, 所以要把DStreame转成RDD再降序排序
val sortedDStream: DStream[(String, Int)] = wordAndCount.transform(rdd => {
val sortedRDD: RDD[(String, Int)] = rdd.sortBy(_._2, false)
println("↓↓↓↓↓↓↓↓↓↓TOP-3↓↓↓↓↓↓↓↓↓↓")
sortedRDD.take(3).foreach(println)
println("↑↑↑↑↑↑↑↑↑↑TOP-3↑↑↑↑↑↑↑↑↑↑")
sortedRDD
})
sortedDStream.print(3)//No output operations registered, so nothing to execute
ssc.start()//开启
ssc.awaitTermination()//等待优雅停止
}
}
在spark1.3版本后,kafkaUtils里面提供了两种创建DStream的方法:
①Receiver接收方式:不适合在开发中使用
KafkaUtils.createDstream, 会有一个Receiver作为常驻的Task运行在Executor等待数据, 但是一个Receiver效率低,需要开启多个,再手动合并数据,再进行处理,很麻烦;
如果运行Receiver的某台机器宕机,部分数据会丢失, 所以需要开启WAL(预写日志)保证数据安全,那么效率又会降低!Receiver方式是通过zookeeper来连接kafka队列,调用Kafka高阶API,offset存储在zookeeper,由Receiver维护,Spark获取数据存入executor中, spark在消费的时候为了保证数据不丢也会在Checkpoint中存一份offset,可能会出现数据不一致。
所以不管从何种角度来说,Receiver模式都不适合在开发中使用
②Direct直连方式: 开发中使用,要求掌握
Direct方式调用Kafka低阶API直接连接kafka分区来获取数据, 从每个分区直接读取数据提高了并行能力;
offset默认由Spark自己在checkpoint中存储和维护(开发中自己手动维护,把offset存在mysql、redis中),消除了与zk不一致的情况。
另外, 借助Direct模式的特点+手动操作可以保证数据的Exactly once 精准一次。
扩展: 关于消息语义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rBVky6tM-1576328225528)(C:/MarkDown_Photo/图片8.png)]
注意:
启动kafka集群
#三台机运行
cd /export/servers/kafka_2.11-1.0.0
nohup bin/kafka-server-start.sh config/server.properties 2>&1 &
#node01创建topic
/export/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh --create --zookeeper node01:2181 --replication-factor 3 --partitions 3 --topic spark_kafka
#node01启动生产者
/export/servers/kafka_2.11-1.0.0/bin/kafka-console-producer.sh --broker-list node01:9092,node02:9092,node03:9092 --topic spark_kafka
#三台机同时关闭
cd /export/servers/kafka_2.11-1.0.0
bin/kafka-server-stop.sh
测试数据
香港机场实施出入管制 香港机场实施出入管制 香港机场实施出入管制 香港机场实施出入管制
太阳下吵架半小时双双中暑 太阳下吵架半小时双双中暑 太阳下吵架半小时双双中暑
首个台风红色预警
告全体香港青年 告全体香港青年 告全体香港青年 告全体香港青年 告全体香港青年
源码:
def createDirectStream[K, V](
ssc : StreamingContext,
//位置策略,源码强烈推荐使用该策略,会让Spark的Executor和Kafka的Broker均匀对应
locationStrategy : LocationStrategy,
//消费策略,源码强烈推荐使用该策略
consumerStrategy :ConsumerStrategy[K, V]
):InputDStream[ConsumerRecord[K, V] //返回值为InputDStream
= { /* compiled code */ }
Kafka的auto.offset.reset参数说明:
此配置参数表示当此groupId下的消费者,在没有offset值时,consumer应该从哪个offset开始消费.
1. earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
2. latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
3. none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
//这里配置latest自动重置偏移量为最新的偏移量,即如果有偏移量从偏移量位置开始消费,没有偏移量从新来的数据开始消费
代码(多练!)
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.{DStream, InputDStream, ReceiverInputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
//使用Spark-Kafka-0-10版本整合
object MySparkKafka {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc: SparkContext = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc: StreamingContext = new StreamingContext(sc,Seconds(5))
//1. 准备连接kafka的参数
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "node01:9092,node02:9092,node03:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "SparkKafkaDemo",
"auto.offset.reset" -> "latest",
//false表示关闭自动提交.由spark帮你提交到Checkpoint或程序员手动维护
"enable.auto.commit" -> (false: java.lang.Boolean)
)
val topic = Array("spark_kafka")
//2. 使用KafkaUtil连接kafka获取数据
val recordDStream: InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(
ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topic, kafkaParams) //要加上泛型!!
)
//3. 操作数据
//注意: recordDStream里的元素是ConsumerRecord[String, String])类型
val func=(a:ConsumerRecord[String, String]) => a.value()//value方法返回的是字符串
val lineDStream: DStream[String] = recordDStream.map(func)//得到的lineDStream里封装的是一行数据
val wordAndOneDStream = lineDStream.flatMap(_.split(" ")).map((_,1))
val wordAndCount: DStream[(String, Int)] = wordAndOneDStream.reduceByKeyAndWindow((a:Int, b:Int)=>a+b,Seconds(10),Seconds(5))
val sortedDStream: DStream[(String, Int)] = wordAndCount.transform(rdd => {
rdd.sortBy(_._2, false)
})
sortedDStream.print()
ssc.start()
ssc.awaitTermination()
}
}
①在MySQL创建下表
#在MySQL创建下表
#使用了联合主键, 新数据重新计算的结果会覆盖历史数据
USE sparkdb;
CREATE TABLE `t_offset` (
`topic` varchar(255) NOT NULL,
`partition` int(11) NOT NULL,
`groupid` varchar(255) NOT NULL,
`offset` bigint(20) DEFAULT NULL,
PRIMARY KEY (`topic`,`partition`,`groupid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
启动kafka集群
#三台机运行
cd /export/servers/kafka_2.11-1.0.0
nohup bin/kafka-server-start.sh config/server.properties 2>&1 &
#node01创建topic
/export/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh --create --zookeeper node01:2181 --replication-factor 3 --partitions 3 --topic spark_kafka
#node01启动生产者
/export/servers/kafka_2.11-1.0.0/bin/kafka-console-producer.sh --broker-list node01:9092,node02:9092,node03:9092 --topic spark_kafka
#三台机同时关闭
cd /export/servers/kafka_2.11-1.0.0
bin/kafka-server-stop.sh
测试数据
香港机场实施出入管制 香港机场实施出入管制 香港机场实施出入管制 香港机场实施出入管制
太阳下吵架半小时双双中暑 太阳下吵架半小时双双中暑 太阳下吵架半小时双双中暑
首个台风红色预警
告全体香港青年 告全体香港青年 告全体香港青年 告全体香港青年 告全体香港青年
②入口类代码演示
注意事项:
import java.sql.{DriverManager, ResultSet}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.streaming.dstream.InputDStream
import org.apache.spark.streaming.kafka010.{OffsetRange, _}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable
//Desc 使用Spark-Kafka-0-10版本整合,并手动提交偏移量,维护到MySQL中(还可以维护到redis中, 本次未演示)
object KafkaSparkMySQL {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("wc").setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")
val ssc = new StreamingContext(sc,Seconds(5))//5表示5秒中对数据进行切分形成一个RDD
//1.准备连接Kafka的参数
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "node01:9092,node02:9092,node03:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "SparkKafkaDemo",
"auto.offset.reset" -> "latest",
"enable.auto.commit" -> (false: java.lang.Boolean)
)
val topics = Array("spark_kafka")
/* 2.使用KafkaUtil连接Kafak获取数据
注意:
如果MySQL中没有记录offset,则直接连接,从latest开始消费
如果MySQL中有记录offset,则应该从该offset处开始消费*/
val offsetMap: mutable.Map[TopicPartition, Long] = OffsetUtil.getOffsetMap("SparkKafkaDemo","spark_kafka")
val recordDStream: InputDStream[ConsumerRecord[String, String]] =
if(offsetMap.size > 0){//有记录offset
println("MySQL中记录了offset,则从该offset处开始消费\r\n")
KafkaUtils.createDirectStream[String, String](ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams,offsetMap))
}else{//没有记录offset
println("没有记录offset,则直接连接,从latest开始消费")
KafkaUtils.createDirectStream[String, String](ssc,
LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe[String, String](topics, kafkaParams))
}
/*3.操作数据
注意:我们的目标是要自己手动维护偏移量,也就意味着,消费了一小批数据就应该提交一次offset
而这一小批数据在DStream的表现形式就是RDD,所以我们需要对DStream中的RDD进行操作获取offset
对DStream中的RDD进行操作的API有transform(转换)和foreachRDD(动作) */
recordDStream.foreachRDD(rdd=>{
if(rdd.count() > 0){//当前这一时间批次有数据
rdd.foreach(record => println("接收到的Kafk发送过来的数据为:" + record))
/*接收到的Kafk发送过来的数据为:
ConsumerRecord(topic = spark_kafka, partition = 1, offset = 6, CreateTime = 1565400670211, checksum = 1551891492,
serialized key size = -1, serialized value size = 43, key = null, value = hadoop spark ...)
☆通过打印接收到的消息可以看到,里面有我们需要维护的offset,和要处理的数据
☆所以接下来把offset相关从rdd中取出来, 通过OffsetUtil维护(保存)到mySQL中
☆如何取呢???
为了方便我们对offset的维护/管理,spark提供了一个HasOffsetRanges类,帮我们封装offset的数据*/
val offsetRanges: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
println("\r\n↓↓↓↓↓↓下面打印的是HasOffsetRanges封装的和offset相关的信息↓↓↓↓↓↓")
for (o <- offsetRanges){
println(s"topic=${o.topic},partition=${o.partition},fromOffset=${o.fromOffset},untilOffset=${o.untilOffset}")
}
//我们写了一个工具类, 让工具类ffsetUtil来完成维护(保存)到mySQL中
OffsetUtil.saveOffsetRanges("SparkKafkaDemo",offsetRanges)
//我们还可以用下面的方式手动提交offset,默认维护到Checkpoint中
//recordDStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges)
}
})
ssc.start()//开启
ssc.awaitTermination()//等待优雅停止
}
//手动维护offset的工具类
object OffsetUtil {
//从数据库读取偏移量
def getOffsetMap(groupid: String, topic: String) = {
val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/sparkdb?characterEncoding=UTF-8", "root", "123456")
val pstmt = connection.prepareStatement("select * from t_offset where groupid=? and topic=?")
pstmt.setString(1, groupid)
pstmt.setString(2, topic)
val rs: ResultSet = pstmt.executeQuery()
val offsetMap = mutable.Map[TopicPartition, Long]()
while (rs.next()) {
offsetMap += new TopicPartition(rs.getString("topic"), rs.getInt("partition")) -> rs.getLong("offset")
}
rs.close()
pstmt.close()
connection.close()
offsetMap
}
//将偏移量保存到数据库
def saveOffsetRanges(groupid: String, offsetRange: Array[OffsetRange]) = {
val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/sparkdb?characterEncoding=UTF-8", "root", "123456")
//replace into表示之前有就替换,没有就插入
val pstmt = connection.prepareStatement("replace into t_offset (`topic`, `partition`, `groupid`, `offset`) values(?,?,?,?)")
for (o <- offsetRange) {
pstmt.setString(1, o.topic)
pstmt.setInt(2, o.partition)
pstmt.setString(3, groupid)
pstmt.setLong(4, o.untilOffset)
pstmt.executeUpdate()
}
pstmt.close()
connection.close()
}
}
}