Spark Streaming 是基于spark的流式批处理引擎。其基本原理是:将实时输入数据流以时间片为单位进行拆分,然后经Spark引擎以类似批处理的方式处理每个时间片数据。
流式系统的特点:
低延迟。秒级或更短时间的响应
高性能
分布式
可扩展。伴随着业务的发展,数据量、计算量可能会越来越大,所以要求系统是可扩展的
容错。分布式系统中的通用问题,一个节点挂了不能影响应用
1、同一套系统,安装spark之后就一切都有了
2、spark较强的容错能力;strom使用较广、更稳定
3、storm是用Clojure语言去写的,它的很多扩展都是使用java完成的
4、任务执行方面和strom的区别是:
spark steaming数据进来是一小段时间的RDD,数据进来之后切成一小块一小块进行批处理
storm是基于record形式来的,进来的是一个tuple,一条进来就处理一下
5、中间过程实质上就是spark引擎,只不过sparkstreaming在spark之后引擎之上动了一点手脚:对进入spark引擎之前的数据进行了一个封装,方便进行基于时间片的小批量作业,交给spark进行计算
Spark Streaming最主要的抽象是DStream(Discretized Stream,离散化数据流),表示连续不断的数据流。
在内部实现上,Spark Streaming的输入数据按照时间片(如1秒)分成一段一段,每一段数据转换为Spark中的RDD,这些分段就是DStream,并且对DStream的操作都最终转变为对相应的RDD的操作。
Spark Streaming提供了被称为离散化流或者DStream的高层抽象,这个高层抽象用于表示数据的连续流;
创建DStream的方式:由文件、Socket、Kafka、Flume等取得的数据作为输入数据流;或其他DStream进行的高层操作;
在内部,DStream被表达为RDDs的一个序列。
1、Dstream叫做离散数据流,是一个数据抽象,代表一个数据流。这个数据流可以从对输入流的转换获得
2、Dstream是RDD在时间序列上的一个封装
3、DStream的内部是通过一组时间序列上连续的RDD表示,每个都包含了特定时间间隔的数据流,RDD代表按照规定时间收集到的数据集
4、DStream这种数据流抽象也可以整体转换,一个操作结束后转换另外一种DStream
5、DStream的默认存储级别为<内存+磁盘>
6、sparkstreaming有一种特别的操作:windows操作,称为窗口操作,实质是对固定的以时间片积累起来的几个RDD作为一整体操作
7、可以使用persist()函数进行序列化(KryoSerializer)
Spark Streaming可整合多种输入数据源,如:
文件系统(本地文件、HDFS文件)
TCP套接字
Flume
Kafka
处理后的数据可存储至文件系统、数据库等系统中
在Spark Streaming中,有一个组件Receiver,作为一个长期运行的task跑在一个Executor上;
每个Receiver都会负责一个input DStream(比如从文件中读取数据的文件流,比如套接字流,或者从Kafka中读取的一个输入流等等);
Spark Streaming通过input DStream与外部数据源进行连接,读取相关数据。这项工作由Receiver完成。
1、创建输入DStream来定义输入源
2、通过对DStream应用转换操作和输出操作来定义流计算
3、用streamingContext.start()来开始接收数据和处理流程;start之后不能再添加业务逻辑。
4、通过streamingContext.awaitTermination()方法来等待处理结束(手动结束或因为错误而结束)
5、可以通过streamingContext.stop()来手动结束流计算进程
如果要运行一个Spark Streaming程序,就需要首先生成一个StreamingContext对象,它是Spark Streaming程序的入口;
可以从一个SparkConf对象创建一个StreamingContext对象;
进入spark-shell以后,就已经获得了一个默认的SparkConext,即sc。也可以采用如下方式来创建StreamingContext对象:备注:Streaming的程序不适合在spark-shell中编写!
如果是编写一个独立的Spark Streaming程序(IDEA),而不是在spark-shell中运行,需要通过如下方式创建StreamingContext对象:
import org.apache.spark._
import org.apache.spark.streaming._
val conf = new SparkConf().setAppName(“TestDStream”).setMaster(“local[2]”)
val ssc = new StreamingContext(conf, Seconds(1))
通过spark SQL
//创建一个 SparkConf 配置
val sparkConf = new
SparkConf().setAppName(“StreamingRecommender”).setMaster(config(“spark.cores”))
val spark = SparkSession.builder().config(sparkConf).getOrCreate()
val sc = spark.sparkContext
val ssc = new StreamingContext(sc,Seconds(2))
通过 sparkContext
val sparkConf = new
SparkConf().setAppName(“StreamingRecommender”).setMaster(config(“spark.cores”))
val sc = new sparkContext(sparkConf)
val ssc = new StreamingContext(sc,Seconds(2))
注意
StreamingContext 对象通过SparkContext对象创建。在context创建之后,可以接着开始如下的工作:
定义 input sources,通过创建 input Dstreams 完成
定义 streaming 计算,通过DStreams的 transformation 和 output 操作实现
启动接收数据和处理,通过 streamingContext.start()
等待处理停止 (通常因为错误),通过streamingContext.awaitTermination()
处理过程可以手动停止,通过 streamingContext.stop()
备注:
一旦context启动, 没有新的 streaming 计算可以被设置和添加进来
一旦context被停止, 它不能被再次启动
只有一个StreamingContext在JVM中在同一时间可以被激活
StreamingContext.stop()执行时,同时停止了SparkContext
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object StreamingWordCount {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("StreamingWordCount").setMaster("local[*]")
// 创建StreamingContext
// StreamingContext是所有流功能函数的主要访问点,这里使用多个执行线程和2秒的批次间隔来创建本地的StreamingContext
// 时间间隔为2秒,即2秒一个批次
val ssc = new StreamingContext(conf, Seconds(2))
// 这里采用本地文件,也可以采用HDFS文件
//使用StreamingContext textFileStream方法实时监听指定的Hdfs /sogou500w/目录,当该目录有新的文件增加会读取它,并完成单词计数的操作
val lines = ssc.textFileStream(“hdfs://master:9000/sogou500w/")
val words = lines.map(_.split(“\t"))
val wordCounts = words.map(x => (x(1), 1)).reduceByKey(_ + _)
// 打印单位时间所获得的计数值
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
备注:
1、必须是文本文件;
2、文件必须是cp到指定的路径中,不能是mv。新建文件也可以。
hdfs、本地文件系统都可以;
3、一旦文件被“移动”或“重命名”至目录中,文件不可以被改变;
4、文件流不需要运行接收器,可以不分配核数,即可以使用local[1],这是特例;
Spark Streaming可以通过Socket端口监听并接收数据,然后进行相应处理
编写基于套接字的WordCount程序
新开一个命令窗口,启动nc程序:
nc -lk 9999
(nc 需要安装 yum install nc)
随后可以在nc窗口中随意输入一些单词,监听窗口会自动获得单词数据流信息,在监听窗口每隔x秒就会打印出词频统计信息,可以在屏幕上出现结果。
备注:使用local[],可能存在问题。
如果给虚拟机配置的cpu数为1,使用local[]也只会启动一个线程,该线程用于receiver task,此时没有资源处理接收达到的数据。
【现象:程序正常执行,不会打印时间戳,屏幕上也不会有其他有效信息】
PrintStream主要操作byte流,PrintWriter用来操作字符流。
读取文本文件时一般用后者。
发送
object SocketLikeNC {
def main(args: Array[String]): Unit = {
val words = "Hello World Hello Hadoop Hello spark kafka hive zookeeper hbase flume sqoop".split(" ")
val n = words.length
val port = 9999
val random = new Random()
val ss = new ServerSocket(port)
val socket = ss.accept()
println("成功连接到本地主机:" + socket.getInetAddress)
while (true) {
val out = new PrintWriter(socket.getOutputStream)
out.println(words(random.nextInt(n)) + " "+ words(random.nextInt(n)))
out.flush()
Thread.sleep(400)
}
}
}
接收
object StreamingSocketWordCount {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("StreamingWordCount").setMaster("local[*]")
// 创建StreamingContext
val ssc = new StreamingContext(conf, Seconds(10))
val lines = ssc.socketTextStream("node1", 9999)
val words = lines.flatMap(_.split(" "))
val wordCounts = words.map(x => (x, 1)).reduceByKey(_ + _)
// 打印单位时间所获得的计数值
wordCounts.print()
ssc.start()
ssc.awaitTermination()
}
}
有几个问题:
日志信息太多,不爽,能改善吗?
加入 setLogLevel
可以从别的机器发送字符串吗,可以监听别的机器的端口吗?
nc –lk 9999
ssc.socketTextStream(“node1”, 9999)
nc命令只能将字符串发送到本地的端口;
streaming程序可以监听其他机器的端口
每次都需要手动输入字符串,实在不爽!能写一个模仿nc的程序,向固定端口发送数据吗?
调试Spark Streaming应用程序的时候,可使用streamingContext.
queueStream(queueOfRDD)创建基于RDD队列的Dstream;
新建一个RDDQueueStream.scala代码文件,功能是:每秒创建一个RDD,Streaming每隔5秒就对数据进行处理;
这种方式多用来测试streaming程序。
备注:
oneAtATime:缺省为true,一次处理一个RDD,
设为false,一次处理全部RDD;
RDD队列流可以使用local[1];
涉及到同时出队和入队操作,所以要做同步;
object RDDQueueStream {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName("RDDQueueStream").setMaster("local[2]")
// 每隔5秒对数据进行处理
val ssc = new StreamingContext(sparkConf, Seconds(5))
ssc.sparkContext.setLogLevel("WARN")
val rddQueue = new Queue[RDD[Int]]()
val queueStream = ssc.queueStream(rddQueue)
val mappedStream = queueStream.map(r => (r % 10, 1))
val reducedStream = mappedStream.reduceByKey(_ + _)
reducedStream.print()
ssc.start()
// 每秒产生一个RDD
for (i <- 1 to 5){
rddQueue.synchronized {
rddQueue += ssc.sparkContext.makeRDD(1 to 100, 2)
}
Thread.sleep(1000)
}
ssc.stop()
}
}
和RDDs一样,各种转换允许数据从inputDstream得到之后进行各种变换。DStreams支持各种转换,它们是基于Spark的RDD的,一些常规的转换如下:
map(func) :对源DStream的每个元素,采用func函数进行转换,得到一个新的DStream
flatMap(func) :与map相似,但是每个输入项可用被映射为0个或者多个输出项
filter(func) :返回一个新的DStream,仅包含源DStream中满足函数func的项repartition(numPartitions) :通过创建更多或者更少的分区改变DStream的并行程度
reduce(func) :利用函数func聚集源DStream中每个RDD的元素,返回一个包含单元素RDDs的新DStream
count() :统计源DStream中每个RDD的元素数量
union(otherStream) :返回一个新的DStream,包含源DStream和其他DStream的元素
countByValue() :应用于元素类型为(K,V)的DStream上,返回一个(K,V)键值对类型的新DStream,每个键的值是在原DStream的每个RDD中的出现次数
reduceByKey(func, [numTasks]) :当在一个由(K,V)键值对组成的DStream上执行该操作时,返回一个新的由(K,V)键值对组成的DStream,每一个key的值均由给定的recuce函数(func)聚集起来
join(otherStream, [numTasks]) :当应用于两个DStream(一个包含(K,V)键值对,一个包含(K,W)键值对),返回一个包含(K, (V, W))键值对的新Dstream
cogroup(otherStream, [numTasks]) :当应用于两个DStream(一个包含(K,V)键值对,一个包含(K,W)键值对),返回一个包含(K, Seq[V], Seq[W])的元组
rdd => other RDD
transform(func) :通过对源DStream的每个RDD应用RDD-to-RDD函数,创建一个新的DStream。支持在新的DStream中做任何RDD操作
这是一个很牛X的函数,它可以让你直接操作其内部的RDD,也就是说,如果这些无状态操作都不够用的时候,你想要的东西在API中却没有的时候,你可以自己提供任意一个RDD到RDD的函数,这个函数在数据流每个批次中都被调用,生成一个新的流。
transform常见应用就是让重用之前为RDD写的批处理代码
无状态转换操作实例:
文件、套接字流(Socket)中的词频统计,就是采用无状态转换;
每次统计,都是只统计当前批次到达的单词的词频,和之前批次无关,不会进行累计。
假设:arr1为黑名单数据,true表示数据生效,需要被过滤掉;false表示数据未生效
val arr1 = Array((“spark”, true), (“scala”, false))
val rdd1 = sc.makeRDD(arr1)
假设:arr2为原始的数据,格式为(time, name),需要根据arr1中的数据将arr2中的数据过滤。如"2 spark"要被过滤掉
val arr2 = Array(“1 hadoop”, “2 spark”, “3 scala”, “4 java”, “5 hive”)
val rdd2 = sc.makeRDD(arr2)
结果:Array(3 scala, 5 hive, 4 java, 1 hadoop)
如何实现?
val rdd3 = rdd2.map(x=>(x.split(" ")(1), x)).leftOuterJoin(rdd1)
rdd3.filter(x=>if (x._2.2.getOrElse(false)) false else true).map(._2.1).collect
rdd3.filter(!._2._2.getOrElse(false)).collect // 简洁的写法
备注:
1、leftOuterJoin(要求参与运算的RDD是pairs类型)、filter(函数返回boolean类型)
2、Option类型的处理(getOrElse)
x.getOrElse(false) 如果x中有值就获取,如果没有默认获取false
方法一:
val blacklst = rdd1.filter(_.2).map(._1).collect
rdd2.map(line => (line.split("\s+")(1), line)).
filter(x => !blacklst.contains(x.1)).map(._2).collect
方法三:
用SQL处理
object BlackListFilter {
def main(args: Array[String]) {
val conf = new SparkConf().setAppName("blackListFilter").setMaster("local[2]")
val ssc = new StreamingContext(conf, Seconds(10))
ssc.sparkContext.setLogLevel("WARN")
val blackList = Array(("spark", true), ("scala", true))
val blackListRDD = ssc.sparkContext.makeRDD(blackList)
val clickStream = ssc.socketTextStream(“master", 9999)
val clickStreamFormatted = clickStream.map(value =>(value.split(" ")(1), value))
clickStreamFormatted.transform(clickRDD =>{
// 通过leftOuterJoin操作既保留了左侧RDD的所有内容,又获得了内容是否在黑名单中
val joinedBlackListRDD = clickRDD.leftOuterJoin(blackListRDD)
val validClicked = joinedBlackListRDD.filter(joinedItem =>
if (joinedItem._2._2.getOrElse(false)) false
else true )
validClicked.map(validClicked =>{ validClicked._2._1 })
}).print()
//DStream.transform(RDD 操作).print() 转换输出
ssc.start()
ssc.awaitTermination()
}
}
注意:
可以通过transform算子,对Dstream做RDD到RDD的任意操作。其实就是DStream的类型转换。 算子内,拿到的RDD算子外,代码是在Driver端执行的,每个batchInterval执行一次,可以做到动态改变广播变量。
为SparkStreaming中每一个Key维护一份state状态,通过更新函数对该key的状态不断更新。
DStream.transform(RDD 操作).print() 转换输出
使用 ConstantInputDStream & SQL
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.ConstantInputDStream
import org.apache.spark.sql.SparkSession
object DSTest01 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("DSTest01").setMaster("local[2]")
val ssc = new StreamingContext(conf, Seconds(1))
ssc.sparkContext.setLogLevel("WARN")
val rdd = ssc.sparkContext.parallelize(0 to 9)
val clicks = new ConstantInputDStream(ssc, rdd)
clicks.transform(rdd => {
val spark = SparkSession.builder().config(rdd.sparkContext.getConf).getOrCreate()
println(s">>> rdd: $rdd")
import spark.implicits._
rdd.toDF("num").show
rdd
}).print
ssc.start()
ssc.awaitTermination()
}
}
事先设定一个滑动窗口的长度(即窗口的持续时间)
设定滑动窗口的时间间隔(每隔多长时间执行一次计算),让窗口按照指定时间间隔在源DStream上滑动
每次窗口停放的位置上,都会有一部分DStream(或者一部分RDD)被框入窗口内,形成一个小段的DStream
启动对这个小段 DStream 的计算
假设时间间隔为10s。那么在图中有如下操作:每隔20s(2个批次间隔),就对前30s(3个批次间隔)的数据进行整合计算[time1+time2+time3]。由此可知,一般的window操作会涉及两个参数。
对多少个批次进行整合?—window length 窗口长度 - 窗口的持续时间 30s
间隔多久进行整合操作?—sliding interval 滑动间隔 - 执行窗口操作的时间间隔 20s
备注:两者都必须是批次间隔的整数倍
窗口操作需要checkpoint的支持,用于保存以前的状态
socketLikeNC1:每秒发送一个递增的数字
WindowDemo:
每个1秒(time)接收一次数据(即每次接收1个数字)
滑动间隔为2 * time
窗口长度设置为3 * time
要求打印:
1、每次接收到的数据
2、窗口中的数据
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object WindowDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("WindowDemo")
val ssc = new StreamingContext(conf, Seconds(1))
ssc.sparkContext.setLogLevel("ERROR")
ssc.sparkContext.setCheckpointDir("hdfs://node1:8020/user/checkpoint")
val lines = ssc.socketTextStream("node1", 1521)
lines.foreachRDD(rdd => {
rdd.foreach(value => println(value))
})
//窗口长度为3s,是ssc处理单位的3倍3*1,滑动窗口的时间间隔是2秒,是ssc处理单位的2倍2*1
val res = lines.reduceByWindow(_+_, Seconds(3), Seconds(2))
res.print()
ssc.start()
ssc.awaitTermination()
}
}
Transform | 解释 |
---|---|
Window (windowLength,slideInterval) | 返回一个基于源DStream的窗口批次计算后得到新的DStream |
countByWindow (windowLength,slideInterval) | 返回基于滑动窗口的DStream中的元素的数量 |
reduceByWindow (func,windowLength,slideInterval) | 基于滑动窗口对源DStream中的元素进行聚合操作,得到一个新的DStream |
reduceByKeyAndWindow (func,windowLength, slideInterval, [numTasks]) | 基于滑动窗口对(K,V)键值对类型的DStream中的值按K使用聚合函数func进行聚合操作,得到一个新的Dstream |
reduceByKeyAndWindow (func, invFunc,windowLength, slideInterval, [numTasks]) | 一个更高效的reduceByKkeyAndWindow()的实现版本,先对滑动窗口中新的时间间隔内数据增量聚合并移去最早的与新增数据量的时间间隔内的数据统计量。例如,计算t+4秒这个时刻过去5秒窗口的WordCount,那么我们可以将t+3时刻过去5秒的统计量加上[t+3,t+4]的统计量,在减去[t-2,t-1]的统计量,这种方法可以复用中间三秒的统计量,提高统计的效率。 |
// reduceByWindow : 计算窗口中的累计数
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object WindowDemo2 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("WindowDemo")
val ssc = new StreamingContext(conf, Seconds(2))
ssc.sparkContext.setLogLevel("ERROR")
ssc.sparkContext.setCheckpointDir("hdfs://node1:8020/user/checkpoint")
val lines = ssc.socketTextStream("node2", 1521)
val res1 = lines.reduceByWindow(_+ ", " +_, Seconds(10), Seconds(4))
val res2 = lines.map(_.toInt).reduceByWindow(_+_, Seconds(10), Seconds(4))
res1.print()
res2.print()
ssc.start()
ssc.awaitTermination()
}
}
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval, [numTasks])
这是reduceByKeyAndWindow函数更为高效的版本,需提供一个逆函数invFunc,如+的逆函数是-。
time3重复计算了,影响效率!
那么此种方法,就是避免了这些重复计算,它只考虑新进来的和离开的,不考虑之前已经计算过的。
window1=time1+time2+time3 => time3=window1-time1-time2
window2=window1-time1-time2+time4+time5
+是对新产生的时间分片(time4,time5内RDD)进行统计,而-是对上一个窗口中,过时的时间分片(time1,time2) 进行统计,直接利用了上个窗口的计算结果而不需要重新计算。这种方式称为增量方式。
import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming.{Seconds, StreamingContext}
// 使用socket数据源(socket程序后启动)
object StatefulNetworkWordCount {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[2]").setAppName("Stateful Network WordCount")
val ssc = new StreamingContext(conf, Seconds(2))
ssc.sparkContext.setLogLevel("ERROR")
//设置检查点,检查点具有容错机制
ssc.checkpoint("hdfs://node1:8020/user/checkpoint/")
val lines = ssc.socketTextStream("node2", 4444, StorageLevel.DISK_ONLY_2)
val words = lines.flatMap(_.split(" "))
val pair = words.map(x => (x,1))
// 通过reduceByKeyAndWindow算子,每隔8秒统计最近20秒的词出现的次数,分区数为2
// 后3个参数:窗口时间长度、滑动窗口时间、分区数
val wordCounts1 = pair.reduceByKeyAndWindow(_ + _, _ - _, Seconds(20), Seconds(10), 2)
val wordCounts2 = pair.reduceByKeyAndWindow((a:Int, b:Int) => a + b, Seconds(20), Seconds(10), 2)
wordCounts1.print
wordCounts2.print
ssc.start()
ssc.awaitTermination()
}
}
Spark Streaming 是按Batch Duration来划分Job的,但有时需要根据业务要求按照另外的时间周期(比如说,对过去24小时、或者过去一周的数据,等等这些大于Batch Duration的周期),对数据进行处理(比如计算最近24小时的销售额排名、今年的最新销售量等)。这需要根据之前的计算结果和新时间周期的数据,计算出新的计算结果。
词频统计:
对于有状态转换操作而言,本批次的词频统计,会在之前批次的词频统计结果的基础上进行不断累加,所以,最终统计得到的词频,是所有批次的单词的总的词频统计结果
updateStateByKey和mapWithState(1.6)二者之间有10倍左右的性能差异
updateStateByKey 解释:
以DStream中的数据进行按key做reduce操作,然后对各个批次的数据进行累加
在有新的数据信息进入或更新时。能够让用户保持想要的状态。使用这个功需要两步:
对于有状态操作,要不断的把当前和历史的时间切片的RDD累加计算,随着时间的流失,计算的数据规模会变得越来越大
如果数据很多的时候不建议使用updateStateByKey。
import org.apache.spark._
import org.apache.spark.streaming._
// 使用了updateStateByKey,还可以加一个排序(从大到小)
// 使用socket数据源
object StatefulNetworkWordCount2 {
def main(args: Array[String]) {
// 定义状态更新函数
// 函数常量定义,返回类型是Some(Int),表示的含义是最新状态
// 函数的功能是将当前时间间隔内产生的Key的value集合,加到上一个状态中,得到最新状态
val updateFunc = (currValues: Seq[Int], prevValueState: Option[Int]) => {
//通过Spark内部的reduceByKey按key规约,然后这里传入某key当前批次的Seq/List,再计算当前批次的总和
val currentCount = currValues.sum
// 已累加的值
val previousCount = prevValueState.getOrElse(0)
Some(currentCount + previousCount)
}
val conf = new SparkConf().setMaster("local[2]").setAppName("StatefulNetworkWordCount2")
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("ERROR")
ssc.checkpoint("hdfs://node1:8020/user/checkpoint/")
val lines = ssc.socketTextStream("node1", 9999)
val words = lines.flatMap(_.split(" "))
val wordDstream = words.map(x => (x, 1))
val stateDstream = wordDstream.updateStateByKey[Int](updateFunc)
stateDstream.print()
// 把DStream保存到文本文件中,会生成很多的小文件
val outputDir = "file:///home/spark/streaming/output"
stateDstream.repartition(1).saveAsTextFiles(outputDir)
ssc.start()
ssc.awaitTermination()
}
}
import org.apache.spark.SparkConf
import org.apache.spark.streaming._
object StatefulNetworkWordCount3 {
def main(args: Array[String]) {
val sparkConf = new SparkConf().setAppName(“StatefulNetworkWordCount”).setMaster(“local[2]”)
val ssc = new StreamingContext(sparkConf, Seconds(1))
ssc.sparkContext.setLogLevel("ERROR")
ssc.checkpoint("hdfs://master:9000/user/checkpoint")
// Initial state RDD for mapWithState operation 可以不定义
val initialRDD = ssc.sparkContext.parallelize(List(("hello", 1), ("world", 1)))
val lines = ssc.socketTextStream("node1", 9999) //nc –lk node1 9999
val words = lines.flatMap(_.split(" "))
val wordDstream = words.map(x => (x, 1))
// 函数返回的类型即为 mapWithState 的返回类型
val mappingFunc = (word: String, one: Option[Int], state: State[Int]) => {
val sum = one.getOrElse(0) + state.getOption.getOrElse(0)
val output = (word, sum)
state.update(sum)
output
}
val stateDstream = wordDstream.mapWithState(
StateSpec.function(mappingFunc).initialState(initialRDD))
stateDstream.stateSnapshots().print(100)
ssc.start()
ssc.awaitTermination()
}
}
操作 | 解释 |
---|---|
print() | 在Driver中打印出DStream中数据的前10个元素 |
foreachRDD(func) | 最常用的输出操作,将func函数应用于DStream中的RDD上,这个操作会输出数据到外部系统,比如保存RDD到文件或者网络数据库等。需要注意的是func函数是在运行该streaming应用的Driver进程里执行的。 |
saveAsTextFiles(prefix, [suffix]) | 将DStream中的内容以文本的形式保存为文本文件,其中每次批处理间隔内产生的文件以prefix-TIME_IN_MS[.suffix]的方式命名 |
saveAsObjectFiles(prefix, [suffix]) | 将DStream中的内容按对象序列化并且以SequenceFile的格式保存。其中每次批处理间隔内产生的文件以prefix-TIME_IN_MS[.suffix]的方式命名 |
saveAsHadoopFiles(prefix, [suffix]) | 将DStream中的内容以文本的形式保存为Hadoop文件,其中每次批处理间隔内产生的文件以prefix-TIME_IN_MS[.suffix]的方式命名 |
import java.sql.{Connection, DriverManager, PreparedStatement}
import org.apache.spark.{SparkConf}
import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.dstream.ConstantInputDStream
object DBUtils {
def wordcountSaveAsMySQL(iter: Array[(String, Int)]): Unit = {
val url = "jdbc:mysql://node3:3306/spark"
val user = "hive"
val password = "hive"
var conn: Connection = null
var stmt: PreparedStatement = null
val sql = "insert into wordcount values (?, ?)"
stmt = conn.prepareStatement(sql)
try{
conn = DriverManager.getConnection(url, user, password)
iter.foreach(record => {
stmt.setString(1, record._1)
stmt.setInt(2, record._2)
stmt.executeUpdate()
})
} catch {
case e: Exception => e.printStackTrace()
} finally {
if (stmt != null) stmt.close()
if (conn != null) conn.close()
}
}
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("DBUtils").setMaster("local[2]")
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("WARN")
val arr = Array(("spark", 10), ("hello", 15), ("hbase", 15))
val rdd = ssc.sparkContext.parallelize(arr)
val ds = new ConstantInputDStream(ssc, rdd)
//*********************************************************************
ds.foreachRDD((rdd, time) => {
rdd.foreachPartition(arr => {
wordcountSaveAsMySQL(arr.toArray)
})
})
//*********************************************************************
ssc.start()
ssc.awaitTermination()
}
}
#flume-to-spark-push.conf: A single-node Flume configuration
#Name the components on this agent
a1.sources = r1
a1.sinks = k1
a1.channels = c1
#Describe/configure the source
#把Flume Source类别设置为netcat,绑定到node3的33333端口
#可以通过“telnet node3 33333”命令向Flume Source发送消息
a1.sources.r1.type = netcat
a1.sources.r1.bind = node3
a1.sources.r1.port = 33333
#Describe the sink
#Flume Sink类别设置为avro,绑定到node2的44444端口
#Flume Source把采集到的消息汇集到Flume Sink以后,Sink会把消息推送给node2的44444端口
#Spark Streaming程序一直在监听node2的44444端口,一旦有消息到达,就会被Spark Streaming应用程序取走进行处理
a1.sinks.k1.type = avro
a1.sinks.k1.hostname = slave
a1.sinks.k1.port = 44444
#Use a channel which buffers events in memory
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000000
a1.channels.c1.transactionCapacity = 1000000
a1.sources.r1.channels = c1
a1.sinks.k1.channel = c1
scala代码
import org.apache.spark.SparkConf
import org.apache.spark.storage.StorageLevel
import org.apache.spark.streaming._
import org.apache.spark.streaming.flume._
object FlumeEventCount {
def main(args: Array[String]) {
val host = “slave"
val port = 44444
// Create the context and set the batch size
val conf = new SparkConf().setAppName("FlumeEventCount").setMaster("local[2]")
val ssc = new StreamingContext(conf, Seconds(10))
// 减少终端的输出信息。设置为ERROR时,由于flume没有启动,仍有大量的输出信息
ssc.sparkContext.setLogLevel("ERROR")
// Create a flume stream
val stream = FlumeUtils.createStream(ssc, host, port, StorageLevel.MEMORY_ONLY_SER_2)
// Print out the count of events received from this server in each batch
stream.count().map(cnt => "Received " + cnt + " flume events." ).print()
ssc.start()
ssc.awaitTermination()
}
}// 备注:host (node1),必须是Spark集群中的一台节点,Spark会在这台机器上启动NettyServer
注意
将spark-streaming-flume-sink_2.11-2.3.0.jar、scala-library-2.11.8.jar拷贝到$FLUME_HOME/lib中
备注 scala-library-2.10.5.jar 删除
启动flume:
flume-ng agent --conf-file $FLUME_HOME/conf/flume-to-spark-pull.conf --name a1 -Dflume.root.logger=INFO,console
定义配置文件 flume-to-spark-pull.conf
# agent名称,source、channel、sink的名称
a1.sources = r1
a1.channels = c1
a1.sinks = k1
# 定义具体的source
a1.sources.r1.type = netcat
a1.sources.r1.bind = node3
a1.sources.r1.port = 22222
a1.sources.r1.channels = c1
# 定义具体的channel
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100
# 定义具体的sink
a1.sinks.k1.type = org.apache.spark.streaming.flume.sink.SparkSink
a1.sinks.k1.hostname = node3
a1.sinks.k1.port = 11111
a1.sinks.k1.channel = c1
# 备注:node3是安装了flume的节点
scala
import org.apache.spark.SparkConf
import org.apache.spark.streaming._
import org.apache.spark.streaming.flume._
object FlumePullingEventCount {
def main(args: Array[String]) {
val host = "node3"
val port = 11111
val conf = new SparkConf().setAppName("FlumePullingEventCount").setMaster("local[*]")
val ssc = new StreamingContext(conf, Seconds(5))
ssc.sparkContext.setLogLevel("ERROR")
val stream = FlumeUtils.createPollingStream(ssc, host, port)
stream.count().map(cnt => "Received " + cnt + " flume events." ).print()
ssc.start()
ssc.awaitTermination()
}
}
High Level Consumer API(屏蔽细节管理)
不需要自己管理offset;
默认实现最少一次消息传递语义(At least once);
consumer数量 大于 partition数量,浪费;因为一个partition不允许两个consumer同时读,partition不支持并发读取数据。
consumer数量 小于 partition数量,一个consumer对应多个partition;
最好partition数目是consumer数目的整数倍;
High Level Consumer API围绕着Consumer Group这个逻辑概念展开,它屏蔽了Offset管理(自动读取zookeeper中该Consumer group的last offset )、Broker失败转移以及增减Partition、Consumer时的负载均衡(当Partition和Consumer增减时,Kafka自动进行负载均衡);
增减consumer,broker,partition会导致rebalance,rebalance后consumer对应的partition会发生变化;
High-level接口中获取不到数据时发生阻塞(block);
Low Level Consumer API(Simple Consumer API) (细节需要自己处理)
需要自己管理offset
可以实现各种消息传递语义
Low Level Consumer API,作为底层的Consumer API,提供了消费Kafka Message更大的控制,如:重复读取、跳读、Exactly Once等;
Low Level Consumer API提供更大灵活控制是以复杂性为代价的:
Offset不再透明
Broker自动失败转移需要处理
增加Consumer、Partition、Broker需要自己做负载均衡
Spark Streaming 从 Kafka 中接收数据,有两种方法,这两种方法有不同的编程模型:
使用Receivers 和 Kafka高层次的API
使用Receivers接收数据。Receivers的实现使用Kafka高层次的消费者API。对于所有的Receivers,接收到的数据将会保存在Spark executors中,然后由Spark Streaming启动的Job来处理这些数据。
在默认的配置下,这种方法在失败的情况下会丢失数据,为了保证零数据丢失,你可以在Spark Streaming中使用WAL日志,这是在Spark 1.2.0引入的功能,这使得我们可以将接收到的数据保存到WAL中(WAL日志可以存储在HDFS上),在失败的时候,可以从WAL中恢复,而不至于丢失数据。
利用Receiver来接收kafka中的数据,使用Kafka高级API接口;
对于所有的接收器,从kafka接收来的数据会存储在spark的executor中,之后spark streaming提交的job会处理这些数据;
有几个需要注意的地方:
Spark中的partition和kafka中的partition并不是相关的,所以如果我们加大每个topic的partition数量,仅仅是增加线程来处理由单一Receiver消费的主题。但是这并没有增加Spark在处理数据上的并行度;
对于不同的Group和topic我们可以使用多个Receiver创建不同的Dstream来并行接收数据,之后可以利用union来统一成一个Dstream;
如果启用了Write Ahead Logs复制到文件系统如HDFS,那么storage level需要设置成 StorageLevel.MEMORY_AND_DISK_SER;
使用Direct API,这是使用低层次的 Kafka API,并没有使用到Receivers,是Spark 1.3.0中开始引入的与基于Receiver接收数据不一样,这种方式定期地从Kafka的 topic + partition 中查询最新的偏移量,再根据定义的偏移量范围在每个batch里面处理数据。当作业需要处理的数据来临时,spark通过调用Kafka的简单消费者API读取一定范围的数据。
Spark 官方在Spark 1.3时引入了Direct方式的Kafka数据消费方式。Direct方式采用Kafka低阶的consumer api方式来读取数据,无需经由ZooKeeper,此种方式不再需要专门Receiver来持续不断读取数据;
当batch任务触发时,由Executor读取数据,并参与到其他Executor的数据计算过程中去;
driver来决定读取多少offsets,并将offsets交由checkpoints来维护。触发下次batch任务,再由Executor读取Kafka数据并计算。
此过程无需Receiver读取数据,而是需要计算时再读取数据。所以Direct方式的数据消费对内存的要求不高,只需要考虑批量计算所需要的内存即可;另外batch任务堆积时,也不会影响数据堆积。
这种方法相较于Receiver方式的优势在于:
备注:
没有管理offset
可以获得offset的信息
import kafka.serializer.StringDecoder
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.{HasOffsetRanges, KafkaUtils}
import org.apache.spark.streaming.{Seconds, StreamingContext}
// 1、没人管理offset(该API接口不支持指定Offset),每次不是从头开始就是从最后开始消费数据
// 2、可以获得offset的信息
// 3、可以不指定消费组
object KafkaDirectNothing1 {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.WARN)
val conf = new SparkConf()
.setAppName(s"${this.getClass.getCanonicalName}")
.setMaster("local[2]")
val ssc = new StreamingContext(conf, Seconds(2))
val brokers = "node1:9092"
// largest : 表示接受接收最大的offset,即最新消息 (defaults)
// smallest : 表示最小offset,即从topic的开始位置消费所有消息
// 没有使用任何的外部存储,也没有使用checkpoint
val kafkaParams = Map(("metadata.broker.list", brokers),
("auto.offset.reset", “earliest"))
val topics = Set("mykafka1")
// 使用map后,不能提取Offset的信息
val kafkaDS = KafkaUtils.createDirectStream[String,
String, StringDecoder, StringDecoder](ssc, kafkaParams, topics)//.map(_._2)
kafkaDS.foreachRDD((rdd, time) => {
if (!rdd.isEmpty()){
// 打印每批次接收到的总数据量 和 时间戳
println(s"************** rdd.count = ${rdd.count()}; time = $time **************")
// 打印offset的情况(topic, partition, from, until)
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
for (o <- offsetRanges) {
println(s"${o.topic} ${o.partition} ${o.fromOffset} ${o.untilOffset}")
}
println()
}
})
ssc.start()
ssc.awaitTermination()
}
}
备注:
从指定的offset开始消费
可以保存offset【未实现】
import kafka.common.TopicAndPartition
import kafka.message.MessageAndMetadata
import kafka.serializer.StringDecoder
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.{HasOffsetRanges, KafkaUtils}
import org.apache.spark.streaming.{Seconds, StreamingContext}
// Kafka Direct接口方式二(使用了另一种接口API,允许指定offset的值),获取offset
object KafkaDirectNothing2 {
def main(args: Array[String]): Unit = {
Logger.getLogger("org").setLevel(Level.WARN)
val conf = new SparkConf()
.setAppName(s"${this.getClass.getCanonicalName}")
.setMaster("local[2]")
val ssc = new StreamingContext(conf, Seconds(10))
val brokers = "node1:9092"
val kafkaParams = Map(("metadata.broker.list", brokers),
("auto.offset.reset", "smallest"))
val topics = Set("mykafka1")
// 在这里可以自定义各partiton的offset。存在越界的问题
// 实际应用中应该是到,我们所用的外部存储中获取
val fromOffsets: Map[TopicAndPartition, Long] = Map(
(TopicAndPartition("mykafka1", 0), 300L),
(TopicAndPartition("mykafka1", 1), 300L),
(TopicAndPartition("mykafka1", 2), 300L))
// 将kafka的消息进行转换,这里将数据变成 (topic_name, message) 的形式. 也可以使用其他的形式
val messageHandler = (mmd: MessageAndMetadata[String, String]) => (mmd.topic, mmd.message())
val kafkaDS = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder,
(String, String)](ssc, kafkaParams, fromOffsets, messageHandler)
kafkaDS.foreachRDD((rdd, time) => {
if (!rdd.isEmpty()){
// 执行DS的Transformation后,在这里完成数据的输出。本程序中仅打印
println(s"************** rdd.count = ${rdd.count()}; time = $time **************")
rdd.foreach(println _)
}
// 获取 offset 的值(topic, partition, from, until)
val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
for (o <- offsetRanges) {
println(s"${o.topic} ${o.partition} ${o.fromOffset} ${o.untilOffset}")
}
println()
// 在下面完成Offset值的保存。即将offset保存到我们所用的外部存储中。本例未实现
})
ssc.start()
ssc.awaitTermination()
}
}
流计算系统是长期运行、且不断有数据流入,因此Spark守护进程(Driver)的可靠性至关重要,它决定了Streaming程序能否一直正确地运行下去。
Driver实现HA的解决方案就是将元数据持久化,以便重启后的状态恢复。如下图所示,Driver持久化的元数据包括:
Block元数据(图中的绿色箭头):Receiver从网络上接收到的数据,组装成Block后产生的Block元数据;
Checkpoint数据(图中的橙色箭头):包括配置项、DStream操作、未完成的Batch状态、和生成的RDD数据等;
Driver失败重启后:
恢复计算(图中的橙色箭头):使用Checkpoint数据重启driver,重新构造上下文并重启接收器;
恢复元数据块(图中的绿色箭头):恢复Block元数据;
恢复未完成的作业(图中的红色箭头):使用恢复出来的元数据,再次产生RDD和对应的job,然后提交到Spark集群执行;
通过如上的数据备份和恢复机制,Driver实现了故障后重启、依然能恢复Streaming任务而不丢失数据,因此提供了系统级的数据高可靠;
1、给streamingContext设置checkpoint的目录,该目录必须是HADOOP支持的文件系统,用来保存WAL和做Streaming的checkpoint;
2、spark.streaming.receiver.writeAheadLog.enable 设置为 true;
当WAL被启动了以后,所有的接收器接收的数据可以很稳定的恢复,推荐的内存备份可以关闭了(需要给输入流设置合适的持久化级别),因为WAL保存在可容错的文件系统上,数据已经备份了。
此外,如果想要恢复缓冲的数据,必须使用支持应答的数据源(flume、kafka)。 当数据存储到日志以后那些支持应答接收器可以向数据源确认。内置的flume和kafka接收器已经实现了这些功能。
最后,值得注意的是WAL开启了以后会减少Spark Streaming处理数据的吞吐,因为所有接收的数据会被写到到容错的文件系统上,这样文件系统的吞吐和网络带宽将成为瓶颈。
使用Kafka高阶API数据读取方式让用户专注于所读数据,而不用关注或维护consumer的offsets,减少用户的工作量以及代码量而且相对比较简单;
在刚开始引入Spark Streaming计算引擎时,可考虑采用此方式来读取数据;
采用Reveiver-based方式满足一些场景应用需求,spark官方也对此方式做了一些优化:
但是同时因为这两方面以及其他方面的一些因素,导致也会出现各种情况的问题:
3. 启用WAL,每次处理之前需要将该batch内的日志备份到checkpoint目录中,降低了数据处理效率,加重了Receiver端的压力;另外由于数据备份机制,会受到负载影响,负载一高就会出现延迟的风险,导致应用崩溃;
4. 采用MEMORY_AND_DISK_SER降低对内存的要求。但是在一定程度上影响计算的速度;
5. 单Receiver内存。由于receiver也是属于Executor的一部分,那么为了提高吞吐量,提高Receiver的内存。但是在每次batch计算中,参与计算的batch并不会使用到这么多的内存,导致资源严重浪费。
6. 提高并行度,采用多个Receiver来保存Kafka的数据。Receiver读取数据是异步的,并不参与计算。如果开较高的并行度来平衡吞吐量很不划算;
7. Receiver和计算的Executor的异步的,遇到网络等问题,导致计算延迟,计算队列一直在增加,而Receiver则在一直接收数据,这非常容易导致程序崩溃;
8. 在程序失败恢复时,有可能出现数据部分落地,但是程序失败,未更新offsets的情况,这导致数据重复消费;
对于一些streaming应用程序,如实时活动监控,只需要当前
最新的数据,这种情况不需要管理offset;
在这种场景下:
1、可以将offset保存在HDFS上;
2、与其他系统(Zookeeper、Hbase)相比, HDFS具有更高
的延迟。此外,在HDFS上写入每个批次的offsetRanges可能会导致小文件问题;
测试步骤:
1、在kafka中存放一批数
2、使用spark streaming程序消费
3、关闭spark streaming程序
4、再向kafka存放一批数
5、启动spark streaming程序,可以接着消费数据
代码所做的修改:
1、getOrCreate
2、设置checkpoint
import kafka.serializer.StringDecoder
import org.apache.log4j.{Level, Logger}
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Duration, Seconds, StreamingContext}
object KafkaDirectWithCheckpoint {
def main(args: Array[String]) {
Logger.getLogger("org").setLevel(Level.WARN)
val processingInterval = 10
val brokers = "node1:9092"
val topics = Set("mykafka1")
val kafkaParams = Map[String, String](
"metadata.broker.list" -> brokers,
"auto.offset.reset" -> "smallest")
val checkpointPath = "hdfs://node1:8020/user/checkpoint/KafkaWithCheckpoint4"
val conf = new SparkConf().setAppName(s"${this.getClass.getCanonicalName}").setMaster("local[2]")
def getOrCreateContext(): StreamingContext = {
val ssc = new StreamingContext(conf, Seconds(processingInterval))
val msgDS = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](
ssc, kafkaParams, topics)
ssc.checkpoint(checkpointPath)
// 设置时间间隔,通常设置成DStream的滑动间隔的5~10倍
msgDS.checkpoint(Duration(8*processingInterval.toInt*1000))
msgDS.foreachRDD(rdd => {
if(!rdd.isEmpty()){
println(s"################################ ${rdd.count()} ################################")
}
})
ssc
}
// 如果有checkpoint则checkpoint中记录的信息恢复StreamingContext
val context = StreamingContext.getOrCreate(checkpointPath, getOrCreateContext)
context.start()
context.awaitTermination()
}
}
1、启用Spark Streaming的checkpoint是存储偏移量最简单的方法;
2、流式checkpoint专门用户保存应用程序的状态,数据保存在HDFS上,在故障时能恢复;
3、Spark Streaming的checkpoint无法跨越应用程序进行恢复;
4、Spark 升级也将导致无法恢复;
5、在关键生产应用,不建议使用spark检查的管理offset;
Offset保存在外部存储中,自己管理offset。主要步骤如下:
(1)streaming程序第一次启动时,首先从外部存储中读取offsets。因为是第一次消费数据,外部存储中没有任何信息,此时需要给offsets赋初值;
(2)streaming程序停止后再次启动,首先还是要从外部存储中读取offsets。但是由于在streaming程序停止的过程中,可能在topic中添加了partition 或者 外部存储中的offset与Kafka中的offsets不匹配(简单的说就是各种越界),此时需要对外部存储中的offsets进行校验。校验完成后再从Kafka中读取数据;
(3)在foreachRDD里面,对每一个批次的数据处理之后,更新外部存储中的offsets;
注意:以上3个步骤,1和2只会加载一次,第3个步骤是每个批次里面都会执行一次。
幂等性的数学表达:f(f(x)) = f(x)
幂等性指的是,使用相同参数对同一资源重复调用某个接口的结果与调用一次的结果相同;
如果消息具有操作幂等性,也就是一个消息被应用多次与应用一次产生的效果是一样的。假如消息处理失败,那么就消息重播,由于幂等性,应用多次也能产生正确的结果。
生产者的幂等
在Kafka 0.11.0.0引入了EOS(exactly once semantics,精确一次处理语义)的特性;
幂等性引入目的:生产者进行retry会产生重试时,会重复产生消息。有了幂等性之后,在进行retry重试时,只会生成一个消息。
create table sparksql.user
(id serial not null primary key,
name varchar(20),
age int);
insert into sparksql.user values (1, "tom", 19);
insert into sparksql.user values (1, "andy", 20)
on duplicate key update name="andy", age = 20;
insert into sparksql.user values (1, "lily", 22)
on duplicate key update name="lily", age = 22;
select * from sparksql.user;
备注:非标准SQL,有的数据库不支持on duplicate key update语句