理解流式计算,最形象的例子,就是小明的往水池中放(入)水又放(出)水的案例。流式计算就像水流⼀样, 数据连绵不断的产生,并被快速处理,所以流式计算拥有如下⼀些特点:
在大数据计算领域中,通常所说的流式计算分为实时计算和准实时计算。所谓实时计算就是来一条记录(⼀个事件 Event)启动⼀次计算;而准实时计算则是介于实时计算和离线计算之间的⼀个计算,所以每次处理的是⼀个微小的批次。
常见的离线计算框架
常见的流式计算框架
1. storm(jstorm)
第⼀代的流式处理框架,每⽣成⼀条记录,提交⼀次作业。实时流处理,延迟低。
2. spark-streaming
第⼆代的流式处理框架,短时间内⽣成mirco-batch,提交⼀次作业。准实时,延迟略⾼,秒级或者亚秒级延迟。
3. flink-datastream(blink)
第三代的流式处理框架,每⽣成⼀条记录,提交⼀次作业。实时,延迟低。
SparkStreaming,和SparkSQL⼀样,也是Spark生态栈中非常重要的一个模块,主要是用来进行流式计算的框架。流式计算框架,从计算的延迟上面,又可以分为纯实时流式计算和准实时流式计算,SparkStreaming属于准实时计算框架。
所谓纯实时的计算,指的是来⼀条记录(event事件),启动⼀次计算的作业;离线计算,指的是每次计算⼀个非常大的⼀批(比如几百G,好几个T)数据;准实时计算,介于纯实时和离线计算之间的⼀种计算⽅式。显然不是每⼀条记录就计算⼀次,显然比起离线计算数据量小的多,使用Micro-batch(微小的批次)来表示。
SparkStreaming是SparkCore的api的⼀种扩展,使用DStream(discretized stream or DStream)作为数据模型, 基于内存处理连续的数据流,本质上还是RDD的基于内存的计算。
DStream,本质上是RDD的序列。SparkStreaming的处理流程可以归纳为下图:
接收实时输入数据流,然后将数据拆分成多个batch,⽐如每收集1秒的数据封装为⼀个batch,然后将每个batch交给Spark的计算引擎进⾏处理,最后会⽣产出⼀个结果数据流,其中的数据,也是由⼀个⼀个的batch所组成的。
Spark Streaming提供了⼀种⾼级的抽象,叫做DStream,英⽂全称为Discretized Stream,中⽂翻译为“离散 流”,它代表了⼀个持续不断的数据流。DStream可以通过输⼊数据源来创建,⽐如Kafka、Flume、ZMQ和 Kinesis;也可以通过对其他DStream应⽤⾼阶函数来创建,⽐如map、reduce、join、window。
DStream的内部,其实⼀系列持续不断产⽣的RDD。RDD是Spark Core的核心抽象,即分布式弹性数据集。DStream中的每个RDD都包含了⼀个时间段内的数据。
对DStream应⽤的算⼦,⽐如map,其实在底层会被翻译为对DStream中每个RDD的操作。⽐如对⼀个 DStream执⾏⼀个map操作,会产⽣⼀个新的DStream。但是,在底层,其实其原理为,对输⼊DStream中每个 时间段的RDD,都应⽤⼀遍map操作,然后⽣成的新的RDD,即作为新的DStream中的那个时间段的⼀个RDD。 底层的RDD的transformation操作。
还是由Spark Core的计算引擎来实现的。Spark Streaming对Spark Core进⾏了⼀层封装,隐藏了细节,然后对 开发⼈员提供了⽅便易⽤的⾼层次的API。
导入Maven依赖
org.apache.spark
spark-streaming_2.11
2.2.2
org.apache.spark
spark-streaming-kafka-0-10_2.11
2.2.2
SparkStreaming的入口类为StreamingContext,实际上其底层仍然需要依赖SparkContext。
object _01SparkStreamingWordCountOps {
def main(args: Array[String]): Unit = {
/*
StreamingContext的初始化,需要⾄少两个参数,SparkConf和BatchDuration
SparkConf不⽤多说
batchDuration:提交两次作业之间的时间间隔,每次会提交⼀个DStream,将数据转化batch---
>RDD
所以说:sparkStreaming的计算,就是每隔多⻓时间计算⼀次数据
*/
val conf = new SparkConf()
.setAppName("SparkStreamingWordCount")
.setMaster("local[*]")
val duration = Seconds(2)
val ssc = new StreamingContext(conf, duration)
//业务
//为了执⾏的流式计算,必须要调⽤start来启动
ssc.start()
//为了不⾄于start启动程序结束,必须要调⽤awaitTermination⽅法等待程序业务完成之后调⽤stop
⽅法结束程序,或者异常
ssc.awaitTermination()
}
}
代码实现
object _01SparkStreamingWordCountOps {
def main(args: Array[String]): Unit = {
if(args == null || args.length < 2) {
println(
"""
|Usage:
""".stripMargin)
System.exit(-1)
}
val Array(hostname, port) = args
/*
StreamingContext的初始化,需要⾄少两个参数,SparkConf和BatchDuration
SparkConf不⽤多说
batchDuration:提交两次作业之间的时间间隔,每次会提交⼀个DStream,将数据转化batch---
>RDD
所以说:sparkStreaming的计算,就是每隔多⻓时间计算⼀次数据
*/
val conf = new SparkConf()
.setAppName("SparkStreamingWordCount")
.setMaster("local[*]")
val duration = Seconds(2)
val ssc = new StreamingContext(conf, duration)
//接⼊数据
val lines:ReceiverInputDStream[String] = ssc.socketTextStream(hostname,
port.toInt)
// lines.print()
val retDStream:DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_,
1)).reduceByKey(_+_)
retDStream.print()
//为了执⾏的流式计算,必须要调⽤start来启动
ssc.start()
//为了不⾄于start启动程序结束,必须要调⽤awaitTermination⽅法等待程序业务完成之后调⽤stop
⽅法结束程序,或者异常
ssc.awaitTermination()
}
}
使用netcat进行测试(需要安装)
...
kafka是做消息的缓存,数据和业务隔离操作的消息队列,⽽sparkstreaming是⼀款准实时流式计算框架,所以二者的整合,是大势所趋。
二者的整合,主要有两大版本。
编码
//基于direct⽅式整合kafka
object _03SparkStreamingWithKafkaDirectOps {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.spark_project").setLevel(Level.WARN)
val conf = new SparkConf()
.setAppName("SparkStreamingWithKafkaDirect")
.setMaster("local[*]")
val duration = Seconds(2)
val ssc = new StreamingContext(conf, duration)
val kafkaParams = Map[String, String](
"bootstrap.servers" -> "bigdata01:9092,bigdata02:9092,bigdata03:9092",
"group.id" -> "g_1903_2",
"auto.offset.reset" -> "largest"
)
val topics = "spark".split(",").toSet
val messages: InputDStream[(String, String)] =
KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc,
kafkaParams, topics)
messages.foreachRDD((rdd, bTime) => {
if(!rdd.isEmpty()) {
val offsetRDD = rdd.asInstanceOf[HasOffsetRanges]
val offsetRanges = offsetRDD.offsetRanges
for(offsetRange <- offsetRanges) {
val topic = offsetRange.topic
val partition = offsetRange.partition
val fromOffset = offsetRange.fromOffset
val untilOffset = offsetRange.untilOffset
println(s"topic:${topic}\tpartition:${partition}\tstart:${fromOffset}\tend:${until
Offset}")
}
rdd.count()
}
})
ssc.start()
ssc.awaitTermination()
}
}
说明
cogroup简要说明:cogroup就是groupByKey的另外⼀种变体,groupByKey是操作⼀个K-V键值对, 而cogroup⼀次操作两个,类似于join,不同之处在于返回值结果:
val ds1:DStream[(K, V)]
val ds2:DStream[(K, w)]
val cg:DStream[(K, (Iterable[V], Iterable[W]))] = ds1.cogroup(ds1)
transform是⼀个transformation算⼦,转换算⼦。
DStream上述提供的所有的transformation操作,都是DStream-2-DStream操作,没有⼀个DStream和 RDD的直接操作,⽽DStream本质上是⼀系列RDD,所以RDD-2-RDD操作是显然被需要的,所以此时官⽅api中提 供了⼀个为了达成此操作的算⼦——transform操作。
最经典的实现就是DStream和rdd的join操作,还有dstream重分区(分区减少,coalesce)。
也就是说transform主要就是⽤来⾃定义官⽅api没有提供的⼀些操作。
案例:
动态黑名单过滤
广告计费系统,是电商必不可少的⼀个功能点。为了防⽌恶意的⼴告点击(假设商户A和B同时在某电商做了⼴告,A和B为竞争对⼿,那么如果A使⽤点击机器⼈进⾏对B的⼴告的恶意点击,那么B的⼴告费⽤将很快被⽤完),必须对⼴告点击进⾏⿊名单过滤。⿊名单的过滤可以是ID,可以是IP等等,⿊名单就是过滤的条件,利⽤SparkStreaming的流处理特性,可实现实时⿊名单的过滤实现。可以使⽤leftouter join 对⽬标数据和⿊名单数据进⾏关联,将命中⿊名单的数据过滤掉。
代码实现
/**
* 在线⿊名单过滤
*
* 类名起名规范
* ⾸字⺟⼤写,多单词,采⽤驼峰
* ⼀律名词,不能动词
* 并且单数不能复数
* ⽅法名起名规范
* ⾸字⺟⼩写,多单词,采⽤驼峰
* ⼀般采⽤动宾短语(动词+名词)
* 尽量少⽤⼀些汉语拼⾳,中⽂
*
* 需求:
* 从⽤户请求的nginx⽇志中过滤出⿊名单的数据,保留⽩名单数据进⾏后续业务统计。
* data structure
* 27.19.74.143##2016-05-30 17:38:20##GET /static/image/common/faq.gif
HTTP/1.1##200##1127
110.52.250.126##2016-05-30 17:38:20##GET /data/cache/style_1_widthauto.css?y7a
HTTP/1.1##200##1292
*/
object _01OnlineBlacklistFilterOps {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.spark_project").setLevel(Level.WARN)
val conf = new SparkConf()
.setAppName("OnlineBlacklistFilter")
.setMaster("local[*]")
val duration = Seconds(2)
val ssc = new StreamingContext(conf, duration)
//⿊名单RDD
val blacklistRDD:RDD[(String, Boolean)] = ssc.sparkContext.parallelize(List(
("27.19.74.143", true),
("110.52.250.126", true)
))
//接⼊外部的数据流
val lines:DStream[String] = ssc.socketTextStream("bigdata01", 9999)
//⿊名单过滤
// 110.52.250.126##2016-05-30 17:38:20##GET /data/cache/style_1_widthauto.css?
y7a HTTP/1.1##200##1292
val ip2OtherDStream:DStream[(String, String)] = lines.map(line => {
val index = line.indexOf("##")
val ip = line.substring(0, index)
val other = line.substring(index + 2)
(ip, other)
})
val filteredDStream:DStream[(String, String)] = ip2OtherDStream.transform(rdd
=> {
val join = rdd.leftOuterJoin(blacklistRDD)
join.filter{case (ip, (left, right)) => {
!right.isDefined
}}.map{case (ip, (left, right)) => {
(ip, left)
}}
})
filteredDStream.print()
//重分区
// filteredDStream.transform(_.coalesce(8))
ssc.start()
ssc.awaitTermination()
}
}
updateStateByKey(func) 根据于key的前置状态和key的新值,对key进⾏更新,返回⼀个新状态的Dstream。
统计截⽌到⽬前为⽌key的状态。
通过分析,我们需要清楚:在这个操作中需要两个数据,⼀个是key的前置状态,⼀个是key的新增(当前批次的 数据);还有历史数据(前置状态)得需要存储在磁盘,不应该保存在内存中。
同时key的前置状态可能有可能没有。
案例:wordcount
/**
* 统计,截⽌到⽬前为⽌出现的每⼀个key的次数
*/
object _02WordCountUpdateStateByKeyOps {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.spark_project").setLevel(Level.WARN)
val conf = new SparkConf()
.setAppName("WordCountUpdateStateByKey")
.setMaster("local[*]")
val duration = Seconds(2)
val ssc = new StreamingContext(conf, duration)
ssc.checkpoint("file:/E:/data/out/1903/chk")
val lines:DStream[String] = ssc.socketTextStream("bigdata01", 9999)
val pairs:DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_, 1))
val usb:DStream[(String, Int)] = pairs.updateStateByKey(updateFunc)
usb.print()
ssc.start()
ssc.awaitTermination()
}
/*
状态更新函数
根据key的前置状态和key的最新值,聚合得到截⽌到⽬前为⽌key的状态
seq:为当前key的状态
option为key对应的历史值
*/
def updateFunc(seq: Seq[Int], option: Option[Int]): Option[Int] = {
println("option:" + option + "> seq: " + seq.mkString("[", ",", "]"))
// var sum = 0
// for(i <- seq) sum += i
// if(option.isDefined) {
// sum += option.get
// }
// Option(sum)
Option(seq.sum + option.getOrElse(0))
}
}
window操作就是窗口函数。Spark Streaming提供了滑动窗⼝操作的⽀持,从⽽让我们可以对⼀个滑动窗⼝内 的数据执⾏计算操作。每次掉落在窗⼝内的RDD的数据,会被聚合起来执⾏计算操作,然后⽣成的RDD,会作为 window DStream的⼀个RDD。⽐如下图中,就是对每三秒钟的数据执⾏⼀次滑动窗⼝计算,这3秒内的3个RDD会 被聚合起来进⾏处理,然后过了两秒钟,⼜会对最近三秒内的数据执⾏滑动窗⼝计算。所以每个滑动窗⼝操作,都 必须指定两个参数,窗⼝⻓度以及滑动间隔,⽽且这两个参数值都必须是batch间隔的整数倍。
/**
* 统计,截⽌到⽬前为⽌出现的每⼀个key的次数
* window窗⼝操作,每个多⻓M时间,通过过往N⻓时间内产⽣的数据
* M就是滑动⻓度sliding interval
* N就是窗⼝⻓度window length
*/
object _03WordCountWindowsOps {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.spark_project").setLevel(Level.WARN)
val conf = new SparkConf()
.setAppName("WordCountUpdateStateByKey")
.setMaster("local[*]")
val batchInterval = 2
val duration = Seconds(batchInterval)
val ssc = new StreamingContext(conf, duration)
val lines:DStream[String] = ssc.socketTextStream("bigdata01", 9999)
val pairs:DStream[(String, Int)] = lines.flatMap(_.split("\\s+")).map((_, 1))
val ret:DStream[(String, Int)] = pairs.reduceByKeyAndWindow(_+_,
windowDuration = Seconds(batchInterval * 3),
slideDuration = Seconds(batchInterval * 2))
ret.print()
ssc.start()
ssc.awaitTermination()
}
/*
状态更新函数
根据key的前置状态和key的最新值,聚合得到截⽌到⽬前为⽌key的状态
seq:为当前key的状态
option为key对应的历史值
*/
def updateFunc(seq: Seq[Int], option: Option[Int]): Option[Int] = {
println("option:" + option + "> seq: " + seq.mkString("[", ",", "]"))
// var sum = 0
// for(i <- seq) sum += i
// if(option.isDefined) {
// sum += option.get
// }
// Option(sum)
Option(seq.sum + option.getOrElse(0))
}
}
Spark最强大的地方在于,可以与Spark Core、Spark SQL整合使用,之前已经通过transform、foreachRDD等算子看到,如何将DStream中的RDD使用Spark Core执行批处理操作。现在就来看看,如何将DStream中的RDD 与Spark SQL结合起来使⽤。
案例:top3的商品排序(最新的top3)
这⾥就是基于updatestateByKey,统计截⽌到⽬前为⽌的不同品类下的商品销量top3
/**
* SparkStreaming整合SparkSQL的案例之,热⻔品类top3排⾏
*/
object _04StreamingIntegerationSQLOps {
def main(args: Array[String]): Unit = {
Logger.getLogger("org.apache.hadoop").setLevel(Level.WARN)
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.spark_project").setLevel(Level.WARN)
val conf = new SparkConf()
.setAppName("StreamingIntegerationSQL")
.setMaster("local[*]")
val batchInterval = 2
val duration = Seconds(batchInterval)
val spark = SparkSession.builder()
.config(conf)
.getOrCreate()
val ssc = new StreamingContext(spark.sparkContext, duration)
ssc.checkpoint("file:/E:/data/out/1903/chk-1")
val lines:DStream[String] = ssc.socketTextStream("bigdata01", 9999)
//001 mi moblie
val pairs:DStream[(String, Int)] = lines.map(line => {
val fields = line.split("\\s+")
if(fields == null || fields.length != 3) {
("", -1)
} else {
val brand = fields(1)
val category = fields(2)
(s"${category}_${brand}", 1)
}
}).filter(t => t._2 != -1)
val usb:DStream[(String, Int)] = pairs.updateStateByKey(updateFunc)
usb.foreachRDD((rdd, bTime) => {
if(!rdd.isEmpty()) {//category_brand count
import spark.implicits._
val df = rdd.map{case (cb, count) => {
val category = cb.substring(0, cb.indexOf("_"))
val brand = cb.substring(cb.indexOf("_") + 1)
(category, brand, count)
}}.toDF("category", "brand", "sales")
df.createOrReplaceTempView("tmp_category_brand_sales")
val sql =
"""
|select
| t.category,
| t.brand,
| t.sales
| t.rank
|from (
| select
| category,
| brand,
| sales,
| row_number() over(partition by category order by sales desc)
rank
| from tmp_category_brand_sales
|) t
|where t.rank < 4
""".stripMargin
spark.sql(sql).show()
}
})
ssc.start()
ssc.awaitTermination()
}
def updateFunc(seq: Seq[Int], option: Option[Int]): Option[Int] = {
Option(seq.sum + option.getOrElse(0))
}
}
SparkStreaming的缓存,说白了就是DStream的缓存,DStream的缓存就只有⼀个方面,DStream对应的RDD的缓存,RDD如何缓存?rdd.persist(),所以DStream的缓存其实就是RDD的缓存,使用persist()指定,及其需 要指定持久化策略,大多算子默认情况下,持久化策略为MEMORY_AND_DISK_SER_2。
总结:
元数据checkpoint主要是为了从driver失败中进⾏恢复;⽽RDD checkpoint主要是为了使⽤到有状态的transformation操作时,能够在其⽣产出的数据丢失时,进⾏快速的失败恢复。
很多情况下Streaming程序需要的内存不是很多,但是需要的CPU要很多。在Streaming程序中,CPU资源的使用可以分为两⼤类:
如果在计算的任何stage中使⽤的并⾏task的数量没有⾜够多,那么集群资源是⽆法被充分利⽤的。举例来说,对于分布式的reduce操作,⽐如reduceByKey和reduceByKeyAndWindow,默认的并⾏task的数量是由 spark.default.parallelism参数决定的。你可以在reduceByKey等操作中,传⼊第⼆个参数,⼿动指定该操作的并行度,也可以调节全局的spark.default.parallelism参数。
该参数说的是,对于那些shuffle的⽗RDD的最⼤的分区数据。对于parallelize或者textFile这些输⼊算⼦,因为没有⽗RDD,所以依赖于ClusterManager的配置。如果是local模式,该默认值是local[x]中的x;如果是mesos的细粒度模式,该值为8,其它模式就是Math.max(2, 所有的excutor上的所有的core的总数)。
数据序列化造成的系统开销可以由序列化格式的优化来减⼩。在流式计算的场景下,有两种类型的数据需要序列化。
上述场景中,使⽤Kryo序列化类库可以减⼩CPU和内存的性能开销。使⽤Kryo时,⼀定要考虑注册⾃定义的 类,并且禁⽤对应引⽤的tracking(spark.kryo.referenceTracking=false 跟踪对同⼀个对象的引⽤情况,这对发现 有循环引⽤或同⼀对象有多个副本的情况是很有⽤的。设置为false可以提⾼性能)。
内存调优的另外⼀个⽅⾯是垃圾回收。对于流式应⽤来说,如果要获得低延迟,肯定不想要有因为JVM垃圾回收导 致的⻓时间延迟。有很多参数可以帮助降低内存使⽤和GC开销:
设置最大接收速率 - 如果集群资源不够多,streaming 应用程序能够像接收到的那样快速处理数据,则可以通过设置记录 /秒的最大速率限制来对 receiver 进行速率限制。
详细内容请参阅 receiver 的 spark.streaming.receiver.maxRate 和用于 Direct Kafka 方法的spark.streaming.kafka.maxRatePerPartition 的配置参数。
Spark 1.5中,引入了⼀个称为背压的功能,无需设置此速率限制,因为Spark Streaming会自动计算速率限制,并在处理条件发生变化时动态调整速率限制。可通过将配置参数 spark.streaming.backpressure.enabled 设置为 true 来启用此 backpressure。
这样就可以解决数据积压和Job等待问题,动态感知数据量的大小,并动态调节Spark每个批次处理的数据量。