1.流批对比
Spark Streaming类似于Apache Storm,用于流式数据的处理。根据其官方文档介绍,Spark Streaming有高吞吐量和容错能力强等特点.
2.输入位置和输出位置
和Spark基于RDD的概念很相似,Spark Streaming使用离散化流(discretized stream)作为抽象表示,叫作DStream。DStream 是随时间推移而收到的数据的序列。在内部,每个时间区间收到的数据都作为 RDD 存在,而 DStream 是由这些 RDD 所组成的序列(因此 得名“离散化”)。
Spark Streaming的编程抽象是离散化流,也就是DStream。它是一个 RDD 序列,每个RDD代表数据流中一个时间片内的数据。
3.基于receiver的streaming作业执行流程
4.什么是DStreams
Discretized Stream是Spark Streaming的基础抽象,代表持续性的数据流和经过各种Spark原语操作后的结果数据流。在内部实现上,DStream是一系列连续的RDD来表示。每个RDD含有一段时间间隔内的数据,
Spark Streaming把一系列连续的批次称为Dstream。
5.Dstream的特点:
就是将流式计算分解成为一系列确定并且较小的批处理作业
可以将失败或者执行较慢的任务在其他节点上并行执行
有较强的的容错能力,基于lineage
Dstream内含high-level operations进行处理
Dstream内部实现为一个RDD序列
6.DStreams输入
Spark Streaming原生支持一些不同的数据源。一些“核心”数据源已经被打包到Spark Streaming 的 Maven 工件中,而其他的一些则可以通过 spark-streaming-kafka 等附加工件获取。
Steaming的数据源和接收器
数据源:基本数据源/高级数据源
基本数据源:socket、file,akka actoer。Steaming中自带了该数据源的读取API
高级数据源:kafka,flume,kinesis,Twitter等其他的数据。必须单独导入集成的JAR包spark-streaming-[projectname]_2.10
接收器:
Socket数据源,有一个接收器,用户接受Socket的数据。每一个接收器必须占用一个cores来接收数据。如果资源不足,那么任务就会处于等待状态。
CPU 核心:
每个接收器都以 Spark 执行器程序中一个长期运行的任务的形式运行,因此会占据分配给应用的 CPU 核心。此外,我们还需要有可用的 CPU 核心来处理数据。这意味着如果要运行多个接收器,就必须至少有和接收器数目相同的核心数,还要加上用来完成计算所需要的核心数。例如,如果我们想要在流计算应用中运行 10 个接收器,那么至少需要为应用分配 11 个 CPU 核心。所以如果在本地模式运行,不要使用local或者local[1]。
7.Receiver读取kafka数据特点
在spark的executor中,启动一个接收器,专门用于读取kafka的数据,然后存入到内存中,供sparkStreaming消费
1、为了保证数据0丢失,WAL,数据会保存2份,有冗余
2、Receiver是单点读数据,如果挂掉,程序不能运行
3、数据读到executor内存中,增大了内存使用的压力,如果消费不及时,会造成数据积压
如下图:
还有几个需要注意的点:
1、Kafka中topic的partition与Spark Streaming中生成的RDD的partition无关,因此,在KafkaUtils.createStream()中,增加某个topic的partition的数量,只会增加单个Receiver消费topic的线程数,也就是读取Kafka中topic partition的线程数量,它不会增加Spark在处理数据时的并行性。
2、可以使用不同的consumer group和topic创建多个Kafka输入DStream,以使用多个receiver并行接收数据。
3、如果已使用HDFS等复制文件系统启用了“预读日志”,则接收的数据已在日志中复制。因此,输入流的存储级别的存储级别StorageLevel.MEMORY_AND_DISK_SER(即,使用KafkaUtils.createStream(…, StorageLevel.MEMORY_AND_DISK_SER))。
8.Direct方式
Direct:直连模式,在spark1.3之后,引入了Direct方式。不同于Receiver的方式,Direct方式没有receiver这一层,其会周期性的获取Kafka中每个topic的每个partition中的最新offsets,并且相应的定义要在每个batch中处理偏移范围,当启动处理数据的作业时,kafka的简单的消费者api用于从kafka读取定义的偏移范围 。其形式如下图:
这种方法相较于Receiver方式的优势在于:
1、简化的并行:在Receiver的方式中我们提到创建多个Receiver之后利用union来合并成一个Dstream的方式提高数据传输并行度。而在Direct方式中,Kafka中的partition与RDD中的partition是一一对应的并行读取Kafka数据,这种映射关系也更利于理解和优化。
2、高效:在Receiver的方式中,为了达到0数据丢失需要将数据存入Write Ahead Log中,这样在Kafka和日志中就保存了两份数据,浪费!而第二种方式不存在这个问题,只要我们Kafka的数据保留时间足够长,我们都能够从Kafka进行数据恢复。
3、精确一次:在Receiver的方式中,使用的是Kafka的高阶API接口从Zookeeper中获取offset值,这也是传统的从Kafka中读取数据的方式,但由于Spark Streaming消费的数据和Zookeeper中记录的offset不同步,这种方式偶尔会造成数据重复消费。而第二种方式,直接使用了简单的低阶Kafka API,Offsets则利用Spark Streaming的checkpoints进行记录,消除了这种不一致性。
请注意,此方法的一个缺点是它不会更新Zookeeper中的偏移量,因此基于Zookeeper的Kafka监视工具将不会显示进度。但是,您可以在每个批处理中访问此方法处理的偏移量,并自行更新Zookeeper。
直连模式特点:tatch time 每隔一段时间,去kafka读取一批数据,然后消费
简化并行度,rdd的分区数量=topic的分区数量
数据存储于kafka中,没有数据冗余
不存在单点问题
效率高
可以实现仅消费一次的语义 exactly-once语义
代码入门
socket模拟实时不间断接收数据
/**
* SparkStreaming的入门案例
* 通过网络socket来模拟实时不断产生数据并处理
* linux的安装:
* yum -y installl nc
* nc -lk 9999启动端口发送数据
* StreamingContext剖析:
* local :是当前程序分配一个工作线程
* local[*] :给当前程序分配可以用工作线程(通常会>=2)
* 这个改动造成程序只接收数据,而不进行处理,那么也就意味着,streaming需要使用线程资源既要接收数据,还有消费数据,而且优先接收数据,接收数据要独占一个线程字段。
*
* 在本地模式下面,去处理receiver到的数据的时候,线程资源最好设置大于1。
*
*/
object Demo1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("Demo1").setMaster("local[*]")
val batchDuration = Seconds(2)//每2s提交一次sparkstreaming的作业
val sc = new StreamingContext(conf,batchDuration)
//加载外部数据
val value:ReceiverInputDStream[String] = sc.socketTextStream("hadoop003", 9999, storageLevel = StorageLevel.MEMORY_AND_DISK_SER_2)
val ds:DStream[String] = value.flatMap(line => line.split("\\s+"))
val ds1:DStream[(String,Int)] = ds.map(word => (word, 1))
val ds2:DStream[(String,Int)] = ds1.reduceByKey(_ + _)
ds2.print()
/*
start操作,是用来启动streaming程序的计算,如果不执行start操作,程序压根儿不会执行
*/
sc.start()
/*
Adding new inputs, transformations, and output operations after starting a context is not supported
在程序启动之后,也就是start之后,不可以再添加任何的业务逻辑
*/
// ret.print()
/*
如果没有awaitTermination方法,程序不会持续不断的运行,说白就是把driver设置成为一个后台的守护线程
*/
sc.awaitTermination()
}
}
从hdfs读取数据
/**
* 从hdfs读取数据:
* streaming只能做到读取一个目录下的新增文件,不能做到读取文件中的新增数据
*/
object Demo2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local")//因为这次读数据返回是DStream,不是ReceiverInputDStream可以少一个线程
.setAppName("_01StreamingNetWorkOps")
//两次流式计算之间的时间间隔,batchInterval
val batchDuration = Seconds(2) // 每隔2s提交一次sparkstreaming的作业
val ssc = new StreamingContext(conf, batchDuration)
//加载外部数据
val input:DStream[String] = ssc.textFileStream("hdfs://hadoop003:9000/test/spark")
val words: DStream[String] = input.flatMap(line => line.split("\\s+"))
val pairs:DStream[(String, Int)] = words.map(word => (word, 1))
val ret = pairs.reduceByKey(_+_)
ret.print()
ssc.start()
ssc.awaitTermination()
}
}
SparkStremaing从kafka中读取数据
/*
SparkStremaing从kafka中读取数据
PerPartitionConfig
spark.streaming.kafka.maxRatePerPartition
spark.streaming.kafka.minRatePerPartition
都是代表了streaming程序消费kafka速率,
max: 每秒钟从每个分区读取的最大的纪录条数
max=10,分区个数为3,间隔时间为2s
所以这一个批次能够读到的最大的纪录条数就是:10*3*2=60
如果配置的为0,或者不设置,起速率没有上限
min: 每秒钟从每个分区读取的最小的纪录条数
那么也就意味着,streaming从kafka中读取数据的速率就在[min, max]之间
执行过程中出现序列化问题:
serializable (class: org.apache.kafka.clients.consumer.ConsumerRecord, value: ConsumerRecord
spark中有两种序列化的方式
默认的就是java序列化的方式,也就是写一个类 implement Serializable接口,这种方式的优点是非常稳定,但是一个缺点是性能不佳
还有一种高性能的序列化方式——kryo的序列化,性能非常高,官方给出的数据是超过java的序列化性能10倍,同时在使用的时候只需要做一个声明式的注册即可
sparkConf.set("spark.serializer", classOf[KryoSerializer].getName)//指定序列化的方式
.registerKryoClasses(Array(classOf[ConsumerRecord[String, String]]))//注册要序列化的类
*/
object Demo3 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local")
.setAppName("Demo3")
// .set("spark.serializer", classOf[KryoSerializer].getName)
// .registerKryoClasses(Array(classOf[ConsumerRecord[String, String]]))
//两次流式计算之间的时间间隔,batchInterval
val batchDuration = Seconds(2) // 每隔2s提交一次sparkstreaming的作业
val ssc = new StreamingContext(conf, batchDuration)
val topics = Set("hadoop")
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "hadoop003:9092,hadoop004:9092,hadoop005:9092",
"key.deserializer" -> classOf[StringDeserializer],
"value.deserializer" -> classOf[StringDeserializer],
"group.id" -> "spark-kafka-group-0817",
"auto.offset.reset" -> "earliest",
"enable.auto.commit" -> "false"
)
//0 449 1 449 2 445
/*
从kafka中读取数据
locationStrategy:位置策略
制定如何去给特定的topic和partition来分配消费者进行调度,可以通过LocationStrategies来得到实例。
在kafka0.10之后消费者先拉取数据,所以在适当的executor来缓存executor操作对于提高性能是非常重要的。
PreferBrokers:
如果你的executor在kafka的broker实例在相同的节点之上可以使用这种方式。
PreferConsistent:
大多数情况下使用这个策略,会把partition分散给所有的executor
PreferFixed:
当网络或者设备性能等个方便不均衡的时候,可以蚕蛹这种方式给特定executor来配置特定partition。
不在这个map映射中的partition使用PreferConsistent策略
consumerStrategy:消费策略
配置在driver或者在executor上面创建的kafka的消费者。该接口封装了消费者进程信息和相关的checkpoint数据
消费者订阅的时候的策略:
Subscribe : 订阅多个topic进行消费,这多个topic用集合封装
SubscribePattern : 可以通过正则匹配的方式,来订阅多个消费者,比如订阅的topic有 aaa,aab,aac,adc,可以通过a[abc](2)来表示
Assign : 指定消费特定topic的partition来进行消费,是更加细粒度的策略
*/
val message:InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(ssc, LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe(topics, kafkaParams))
// message.print()//直接打印有序列化问题
message.foreachRDD((rdd, bTime) => {
if(!rdd.isEmpty()) {
println("-------------------------------------------")
println(s"Time: $bTime")
println("-------------------------------------------")
rdd.foreach(record => {
println(record)
})
}
})
ssc.start()
ssc.awaitTermination()
}
}
kafka从指定的offset位置来读取数据
/*
SparkStremaing从kafka中读取数据
从指定的offset位置来读取数据
这样就可以重复读取数据
*/
object Demo4 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local")
.setAppName("Demo4")
//两次流式计算之间的时间间隔,batchInterval
val batchDuration = Seconds(2) // 每隔2s提交一次sparkstreaming的作业
val ssc = new StreamingContext(conf, batchDuration)
val topics = Set("hadoop")
val kafkaParams = Map[String, Object](
"bootstrap.servers" -> "hadoop003:9092,hadoop004:9092,hadoop005:9092",
"key.deserializer" -> classOf[StringDeserializer],//key序列化
"value.deserializer" -> //value序列化classOf[StringDeserializer],
"group.id" -> "spark-kafka-group-0817",//用于标识这个消费者属于哪个消费团体
"auto.offset.reset" -> "earliest",//偏移量 latest自动重置偏移量为最新的偏移量
"enable.auto.commit" -> "false"//消费完不更新offset
)
//初始偏移量
val offsets = Map[TopicPartition, Long](
new TopicPartition("hadoop", 0) -> 451,
new TopicPartition("hadoop", 1) -> 452,
new TopicPartition("hadoop", 2) -> 447
)
val message:InputDStream[ConsumerRecord[String, String]] = KafkaUtils.createDirectStream(ssc, LocationStrategies.PreferConsistent,
ConsumerStrategies.Subscribe(topics, kafkaParams, offsets))
// message.print()//直接打印有序列化问题
message.foreachRDD((rdd, bTime) => {
if(!rdd.isEmpty()) {
println("-------------------------------------------")
println(s"Time: $bTime")
println("-------------------------------------------")
rdd.foreach(record => {
println(record)
})
}
})
ssc.start()
ssc.awaitTermination()
}
}
updateStateByKey算子
/**
* DStream的updateStateByKey
* 这个是一个状态算子,
* 状态:说白了就是key所对应的value。
* state operator : 计算当前key对应value,不仅仅基于当前的纪录,还得要看历史的纪录
* 统计截止到目前为止某一个key出现的次数
* none state operator : 计算当前key对应value,仅仅基于当前的纪录
*
* 在sparkstreaming中状态算子涉及的的不多:updateStateByKey,xxxxWindow操作
* 其他都是非状态算子,比如:map,flatMap,filter
* updateStateByKey(updateFunc)
* 按照key,来更新value的状态,所以key所对应的value,可能存在,可能不存在(当前key第一次出现的时候,其历史的value就不存在)
* 统计截止到目前为止某一个key出现的次数
* 该算子需要一个状态更新函数,来更新key对应的value,该函数需要至少两个参数
* updateFunc(currentValue:Seq[T], historyValue:Option[T])
*
* 为了持久化保存历史的状态,那么需要一个checkpoint存储目录来管理历史的状态值,在内存是相对不安全的。
*/
object Demo1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[*]")
.setAppName("Demo1")
//两次流式计算之间的时间间隔,batchInterval
val batchDuration = Seconds(2) // 每隔2s提交一次sparkstreaming的作业
val sc = new StreamingContext(conf, batchDuration)
//持久化保存历史值
sc.checkpoint("C:\\Users\\70201\\Desktop\\test\\checkpoint")
val value: ReceiverInputDStream[String] = sc.socketTextStream("hadoop003", 9999)
val value1 = value.flatMap(line => line.split("\\s+"))
val value2 = value1.map(num => (num, 1))
//使用状态算子,需要传入函数
val value3 = value2.updateStateByKey(updateFunc)
value3.print()
sc.start()
sc.awaitTermination()
}
//实现key的value根据当前value和历史value更新值的函数
//当前key可能有多个,多以用seq集合类型
def updateFunc(current:Seq[Int], history:Option[Int]):Option[Int]={
Option(current.sum+history.getOrElse(0))
}
}
transform算子
/*
transform,是一个transformation操作
transform(p:(RDD[A]) => RDD[B]):DStream[B]
类似的操作
foreachRDD(p: (RDD[A]) => Unit)
transform的一个非常重要的一个操作,就是来构建DStream中没有的操作,DStream的大多数操作都可以用transform来模拟
比如map(p: (A) => B) ---> transform(rdd => rdd.map(p: (A) => B))
最重要的操作:
dstream.join(rdd)
在线黑名单过滤
一个离线的数据集RDD和一个在线的数据集DStream之间的join操作*/
/**
* 黑名单
*27.19.74.143
*110.52.250.126
*8.35.201.164
* 传入数据
*8.35.201.161##2016-05-30 17:38:21##GET /static/image/common/search.png HTTP/1.1##200##3047
*/
object Demo2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[*]")
.setAppName("Demo1")
//两次流式计算之间的时间间隔,batchInterval
val batchDuration = Seconds(2) // 每隔2s提交一次sparkstreaming的作业
val sc = new StreamingContext(conf, batchDuration)
val black:RDD[(String,Int)] = sc.sparkContext.parallelize(List(
"27.19.74.143" -> 1,
"110.52.250.126" -> 1,
"8.35.201.164" -> 0
))
val lines:ReceiverInputDStream[String]= sc.socketTextStream("hadoop003", 9999)
val value: DStream[(String,String)] = lines.map(line => {
val index = line.indexOf('#')
val ip = line.substring(0, index)
val info = line.substring(index + 2)
(ip, info)
})
//黑名单过滤操作 因为dstream的join需要的数据类型是dstream,所以二者是无法直接进行join的,需要用transform转换
val write:DStream[(String,String)] = value.transform(rdd => {
//过滤出没有在黑名单中或者解禁的
val join: RDD[(String, (String, Option[Int]))] = rdd.leftOuterJoin(black).filter { case (ip, (info, flag)) => {
flag.isEmpty || flag.get == 0
}
}
//返回ip,和info
join.map { case (ip, (info, flag)) => (ip, info) }
})
write.print()
sc.start()
sc.awaitTermination()
}
}
window操作
/**
* window窗口操作
* 流式无界,所以我们要向进行全局的统计肯定是行不通的,那么我们可以对这个无界的数据集进行切分,
* 被切分的这每一个小的区间,我们可以理解为window,
* 而sparkstreaming是准实时流计算,微小的批次操作,可以理解为是一个特殊的window窗口操作。
*
* 理论上,对这个窗口window的划分,有两种情况,一种就是按照数据的条数,另外一种就是按照时间。
* 但是在sparkstreaming中目前仅支持后者,也就是说仅支持基于时间的窗口,需要提供两个参数
* 一个参数是窗口的长度:window_length
* 另外一个参数是窗口的计算频率:sliding_interval ,每隔多长时间计算一次window操作
*
* streaming程序中还有一个interval是batchInterval,那这两个interval有什么关系?
* batchInterval,每隔多长时间启动一个spark作业,而是每隔多长时间为程序提交一批数据
*
* 特别需要注意的是:
* window_length和sliding_interval都必须是batchInterval的整数倍。
*
* 总结:
* window操作
* 每隔M长的时间,去统计N长时间内产生的数据
* M被称之sliding_interval,窗口的滑动频率
* N被称之window_length,窗口的长度
* 该window窗口是一个滑动的窗口。
*
* 当sliding_interval > window_length的时候,会出现窗口的空隙
* 当sliding_interval < window_length的时候,会出现窗口的重合
* 当sliding_interval = window_length的时候,两个窗口是严丝合缝的
*
* batchInterval=2s
* sliding_interval=4s
* window_length=6s
*/
object Demo3 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setMaster("local[*]")
.setAppName("Demo3")
//两次流式计算之间的时间间隔,batchInterval
val batchDuration = Seconds(2) // 每隔2s提交一次sparkstreaming的作业
val ssc = new StreamingContext(conf, batchDuration)
val lines = ssc.socketTextStream("hadoop003", 9999)
val words = lines.flatMap(line => line.split("\\s+"))
val pairs = words.map(word => (word, 1))
val ret = pairs.reduceByKeyAndWindow(_+_, windowDuration = Seconds(6), slideDuration = Seconds(4))
ret.print
ssc.start()
ssc.awaitTermination()
}
}