一、介绍
Spark Streaming是核心SparkAPI的扩展,可实现实时数据流的可扩展,高吞吐量,容错流处理;可以从许多来源(如Kafka,Flume,Kinesis或TCP端口)中获取数据,并且可以使用以高级函数(如map,reduce,join和window)表示的复杂算法进行处理;最后,处理后的数据可以推送到文件系统,数据库和实时仪表板。。
二、数据源
sparkstreaming既支持基本输入数据源如file、socket、rdd队列,也支持高级输入数据源如kafka,kinesis和flume。
1 基本数据源
1.1 文件系统(local hdfs等)
streamingContext.fileStream[KeyClass,valueClass,InputFormatClass](dataDirectory)
// 输入源为文件的DStream
val ds = ssc.fileStream("/opt/modules/hadoop-3.0.3/logs")
ds.print()
1.2 tcp socket
// 创建一个连接到localhost:port的DStream
val lines = ssc.socketTextStream("localhost", 9999)
val words = lines.flatMap(_.split(" "))
// 每个批次word计数
val pairs = words.map(word => (word, 1))
val wordCounts = pairs.reduceByKey(_ + _)
wordCounts.print()
1.3 queue RDD
var rddQueue = new mutable.SynchronizedQueue[RDD[Int]]
val queueStream = ssc.queueStream(rddQueue)
val result = queueStream.map(x=>(x%5,1)).reduceByKey(_+_)
result.print()
ssc.start()
while(true){
rddQueue += ssc.sparkContext.makeRDD(1 to 100,2)
}
ssc.awaitTermination()
2. 高级数据源
常用sparkstreaming消费kafka方式有两种:
2.1 基于receiver方式
算子:KafkaUtils.createStream
方法:PUSH,从topic中去推送数据,将数据推送过来
API:调用的Kafka高级API
效果:此种方式企业不常用,因为接收到的数据存储在Executor的内存,会出现数据漏处理或者多处理状况
解释:这种方法使用Receiver来接收数据。Receiver是使用Kafka高级消费者API实现的。与所有的接收者一样,通过Receiver从Kafka接收的数据存储在Spark执行程序exector中,然后由Spark Streaming启动的作业处理数据。但是,在默认配置下,这种方法可能会在失败时丢失数据。为了确保零数据丢失,您必须在Spark Streaming(在Spark 1.2中引入)中额外启用写入日志,同时保存所有接收到的Kafka数据写入分布式文件系统(例如HDFS)的预先写入日志,以便所有数据都可以在失败时恢复。
缺点:
- Kafka中的主题分区与Spark Streaming中生成的RDD的分区不相关。因此,增加主题特定分区KafkaUtils.createStream()的数量只会增加在单个接收器中使用哪些主题消耗的线程的数量。在处理数据时不会增加Spark的并行性
- 多个kafka输入到DStream会创建多个group和topic,用于使用多个接收器并行接收数据
- 如果已经使用HDFS等复制文件系统启用了写入日志,则接收到的数据已经在日志中复制。因此,输入流的存储级别为存储级别StorageLevel.MEMORY_AND_DISK_SER
2.2 基于direct方式
算子:KafkaUtils.createDirectStream
方式:PULL,到topic中去拉取数据。
API:kafka低级API
效果:每次到Topic的每个分区依据偏移量进行获取数据,拉取数据以后进行处理,可以实现高可用
解释:在Spark 1.3中引入了这种新的无接收器“直接”方法,以确保更强大的端到端保证。这种方法不是使用接收器来接收数据,而是定期查询Kafka在每个topic+分partition中的最新偏移量,并相应地定义要在每个批次中处理的偏移量范围。当处理数据的作业启动时,Kafka简单的客户API用于读取Kafka中定义的偏移范围(类似于从文件系统读取文件)。请注意,此功能在Spark 1.3中为Scala和Java API引入,在Spark 1.4中针对Python API引入。
优势:
- 简化的并行性:不需要创建多个输入Kafka流并将其合并。与此同时directStream,Spark Streaming将创建与使用Kafka分区一样多的RDD分区,这些分区将全部从Kafka并行读取数据。所以在Kafka和RDD分区之间有一对一的映射关系,这更容易理解和调整。
- 效率:在第一种方法中实现零数据丢失需要将数据存储在预写日志中,这会进一步复制数据。这实际上是效率低下的,因为数据被有效地复制了两次,一次是由Kafka,另一次是由预先写入日志(Write Ahead Log)复制。此方法消除了这个问题,因为没有接收器,因此不需要预先写入日志。只要你有足够的kafka保留,消息可以从kafka恢复。
- 精确语义:第一种方法是使用Kafka的高级API在Zookeeper中存储消耗的偏移量。传统上这是从Kafka消费数据的方式。虽然这种方法(合并日志)可以确保零数据丢失,但在某些失败情况下,很小的几率两次数据都同时丢失,发生这种情况是因为Spark Streaming可靠接收到的数据与Zookeeper跟踪的偏移之间的不一致。因此,在第二种方法中,我们使用不使用Zookeeper的简单Kafka API。在其检查点内,Spark Streaming跟踪偏移量。这消除了Spark Streaming和Zookeeper / Kafka之间的不一致性,因此Spark Streaming每次记录都会在发生故障时有效地接收一次。
请注意,这种方法的一个缺点是它不会更新Zookeeper中的偏移量,因此基于Zookeeper的Kafka监控工具将不会显示进度。但是,您可以在每个批次中访问由此方法处理的偏移量,并自己更Zookeeper
//设置检查点
ssc.checkpoint("/opt/development/checkpoint")
val topic = Set("user_pro")
val kafkaParams = Map[String,String](
"metadata.broker.list" -> "lee:9092",
"key.serializer" -> "value.serializer",
"refresh.leader.backoff.ms" -> "5000",
"group.id" -> "baymax_SparkStraming"
)
val ds1 = KafkaUtils.createDirectStream[String, String,StringDecoder,StringDecoder](ssc, kafkaParams, topic).map(_._2)
三、trainsormations操作
类似RDD和DataFrame,DStream有以下转换操作:
val ds2 = ds1.map(f=>(JSON.parseObject(f)
.getInteger("star_sign").toInt,1))
val ds3 = ds1.filter(f=>JSON.parseObject(f).getInteger("age")<=20)
val ds4 = ds3.repartition(4)
val ds5 = ds3.union(ds1)
// reduce聚合,筛选batch年龄最小的
val ds6 = ds1.reduce((x,y)=>{
if(JSON.parseObject(x).getInteger("age")>JSON
.parseObject(y).getIntValue("age"))
y
else x
})
// 每隔10秒计算前15秒年龄最小的
val ds7 = ds1.reduceByWindow((x,y)=>{
if(JSON.parseObject(x).getInteger("age")>JSON
.parseObject(y).getIntValue("age"))
y
else x
},Seconds(15),Seconds(10))
// DStream[K]=>DStream[K,Long]
implicit val cmo = new Ordering[String]{
override def compare(x:String,y:String):Int =
-JSON.parseObject(x).getInteger("age").compareTo(JSON.parseObject(y).getInteger("age"))
}
val ds8 = ds1.countByValue(2)(cmo)
val ds9 = ds2.reduceByKey(_+_)
// 每隔10秒计算各星座出现次数
val ds10 = ds2.reduceByKeyAndWindow(_+_, Seconds(10))
val ds11 = ds2.join(ds10)
val ds12 = ds2.cogroup(ds10)
特别注意:
updateStateByKey和mapWithState对比:
- online计算:batch=>window=>全局所有时间间隔内产生的数据
- updateStateByKey:统计全局key的状态,就算没有数据输入,也会在每个批次返回key状态
- mapWithState:统计全局key状态,但如果没有数据输入,便不会返回之前的key的状态
- 区别:updateStateByKey数据量太大,需要checkpoint,会占用较大内存
- 对于没有输入,不会返回那些没有变化的key数据,checkpoint
- 不会像updateStateByKey,占用太多内存
val ds13 = ds2.updateStateByKey(
(value:Seq[Int], state:Option[Int])=>{
val values = value.sum
Some(values + state.getOrElse(0))
})
val initialRDD = ssc.sparkContext.parallelize(List[(Int,Int)]())
val mappingFunction=
(key:Int,value:Option[Int], state:State[Int]) => {
val newState = state.getOption().getOrElse(0) + value.getOrElse(0)
state.update(newState)
(key, newState)
}
val ds14 = ds2.mapWithState(StateSpec.function(mappingFunction).initialState(initialRDD))
四、窗口transformation操作
val ds15 = ds2.window(Seconds(10))
val ds16 = ds2.countByWindow(Seconds(15),Seconds(10))
val ds17 = ds2.countByValueAndWindow(Seconds(15), Seconds(10))
val ds18 = ds2.join(ds15)
五、输出操作
1.输出文件系统
saveAsTextFiles传参为prefix和suffix,路径保存在prefix, 若hdfs,则设置为hdfs://lee:8020/
ds1.saveAsTextFiles("hdfs://lee:8020/logs/user_pro_logs", "log")
ds1.saveAsObjectFiles("/opt/data/logs/user_pro_logs", "object")
2.数据库
以HBase为例,HBase常用操作见https://blog.youkuaiyun.com/baymax_007/article/details/81213762。
val tableName = "user_pro"
val cf = "info"
val columns = Array("uid","ceil","qq","age",
"birthday","height")
ds1.foreachRDD(rdd=>{
rdd.foreachPartition(records=>{
HBaseUtils1x.init()
val puts = new ArrayBuffer[Put]()
records.foreach(f=>{
val uid = JSON.parseObject(f).getString("uid")
val ceil = JSON.parseObject(f).getString("ceil")
val qq = JSON.parseObject(f).getString("qq")
val age = JSON.parseObject(f).getString("age")
val birthday = JSON.parseObject(f).getString("birthday")
/**
* 服务端建表
* create 'user_pro',
* {NAME=>'info',BLOCKSIZE=>'16384',BLOCKCACHE=>'false',TTL=>86400},
* {NUMREGIONS=>10,SPLITALGO=>'HexStringSplit'}
* rowkey设计:
* 哈希散列化,取前8位+本身
*/
val rowKey = MD5Hash.getMD5AsHex(Bytes.toBytes
(uid)).substring(0,8) + "_" + uid
val cols = Array(uid,ceil,qq,age,birthday,height,is_married,duration_of_menstruation,menstrual_cycle,star_sign,weight,province,city,recipient,recip_ceil)
try{
puts.append(HBaseUtils1x.getPutAction(rowKey,
cf, columns, cols))
}catch{
case e:Throwable => println(f)
}
})
import collection.JavaConverters._
HBaseUtils1x.addDataBatchEx(tableName, puts.asJava)
HBaseUtils1x.closeConnection()
})
})
参看文献
http://spark.apache.org/docs/latest/streaming-programming-guide.html
https://blog.youkuaiyun.com/weixin_39478115/article/details/78884876
https://www.cnblogs.com/soyo/p/7678489.html