http://spark.apache.org/docs/latest/streaming-programming-guide.html
Spark官网已经已经将用法与概念写的很详细了。这里写一点平时需要注意的要点。
Spark:是以批处理为主,用微批处理来处理流数据
Flink:以流处理为主,用流处理来处理批数据
StreamingContext
ssc ==> Source ==> DStream ==> Transformation ==> Ouput
StreamingContext是所有流功能的主要入口点
val conf = new SparkConf().setMaster("local[2]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
进入StreamingContext源码发现有一个主构造器与两个附属构造器
class StreamingContext private[streaming] (
_sc: SparkContext,
_cp: Checkpoint,
_batchDur: Duration
)
def this(sparkContext: SparkContext, batchDuration: Duration) = {
this(sparkContext, null, batchDuration)
}
def this(conf: SparkConf, batchDuration: Duration) = {
this(StreamingContext.createNewSparkContext(conf), null, batchDuration)
}
要记住的要点:
- 一旦启动了上下文,就不能设置或添加新的流式计算。
- 上下文停止后,无法重新启动。
- 在JVM中只能同时激活一个StreamingContext。
- StreamingContext上的stop()也会停止SparkContext。要仅停止StreamingContext,请将stop()called 的可选参数设置- stopSparkContext为false。
- 只要在创建下一个StreamingContext之前停止前一个StreamingContext(不停止SparkContext),就可以重复使用SparkContext创建多个StreamingContexts。
Input DStreams and Receivers:
Input Dstreams分为两种:一种是有receiver的,一种是没有receiver的(textFileStream读取hdfs文件)。可以源码源码里面这个类的返回值是否返回一个ReceiverInputDStream判断。
要记住的要点
-
在本地运行Spark Streaming程序时,请勿使用“local”或“local [1]”作为主URL。这两种方法都意味着只有一个线程将用于本地运行任务。如果您正在使用基于receiver的输入DStream(例如socket,Kafka,Flume等),那么将使用单个线程来运行接收器,而不留下用于处理接收数据的线程。因此,在本地运行时,始终使用“local [ n ]”作为主URL,其中n >要运行的接收器数量(有关如何设置主服务器的信息,请参阅Spark属性)。
-
将逻辑扩展到在集群上运行时,分配给Spark Streaming应用程序的core数必须大于receiver数。否则系统将接收数据,但无法处理数据。
updateStateByKey:(不推荐使用)
该updateStateByKey操作允许您在使用新信息不断更新时保持任意状态。要使用它,您必须执行两个步骤。
- 定义状态 - 状态可以是任意数据类型。
- 定义状态更新功能 - 使用函数指定如何使用先前状态和输入流中的新值更新状态。
lines.flatMap(_.split(",")).map((_,1)).reduceByKey(_+_).updateStateByKey(updataFunction).print
def updataFunction(newValues:Seq[Int],preValues:Option[Int]):Option[Int]={
val newCount = newValues.sum
val oldCount = preValues.getOrElse(0)
Some(newCount+oldCount)
}
注意:!!
若使用记得指定ssc.checkpoint(“url”),用来存储之前的处理结果。这样会导致产生很多小文件。
如何解决?=》写到外部DB里去,upsert一下;
或者在再处理结果里加个timpstamp字段,这样在某个点执行统计也一样可以出来。
socketTextStream的执行流程:
执行流程:
receiver就收数据源的数据,并根据存储级别进行备份;接着时间到了,receiver将block的块信息发给ssc,ssc提交给sc,sc生成job并将job进行分发
mapWithState: (@Experimental 实验性的)推荐使用,代替updateStateByKey
val result = lines.flatMap(_.split(",")).map((_,1)).reduceByKey(_+_)
def mappingFunc = (word:String,value:option[Int],state:State[Int])=>{
val sum = value.getOrElse(0)+state.getOption().getOrElse(0)
state.update(sum)
(word,sum)
}
val state = result.mapWithState(StateSpec.function(mappingFunc))
Output Operations on DStreams:
print():打印前十条到控制台
!!!foreachRDD(func):通常用来通过网络传输将结果写到DB里面去,传入的func会在java端执行
object ForEachRDD {
def main(args: Array[String]): Unit = {
val conf=new SparkConf().setAppName("ForEacheRDD").setMaster("local[2]")
val ssc=new StreamingContext(conf,Seconds(10))
val DStream=ssc.socketTextStream("192.168.137.130",9998)
//wc
val result=DStream.flatMap(x=>x.split(",")).map(x=>(x,1)).reduceByKey(_+_)
//把结果写入到mysql
//foreachRDD把函数作用在每个rdd上
result.foreachRDD(rdd=>{
rdd.foreach(x=>{
val con=getConnection()
val word=x._1
val count=x._2.toInt
//sql
val sql=s"insert into wc values('$word',$count)"
//插入数据
val pstmt=con.prepareStatement(sql)
pstmt.executeUpdate()
//关闭
pstmt.close()
con.close()
})
})
ssc.start()
ssc.awaitTermination()
}
def getConnection(): Connection={
//加载驱动
Class.forName("com.mysql.jdbc.Driver")
//准备参数
val url="jdbc:mysql://localhost:3306/test"
val username="root"
val password="123456"
val con=DriverManager.getConnection(url,username,password)
con
}
}
以上代码中,如果我们把val con=getConnection()放在foreach外面,大家可以看看结果,会报下面的错误:
Caused by: java.io.NotSerializableException: com.mysql.jdbc.SingleByteCharsetConverter
这是因为foreachRDD(func) func运行在driver里
dstream.foreachRDD { rdd =>
val connection = createNewConnection() //executed at the driver
rdd.foreach { record =>
connection.send(record) //executed at the worker
}
}
,connection这个对象不能跨机器传输。只要是跨网路的都需要序列化。
上面这个案例这样使用连接的话,这样每条记录写入DB时候都会开启连接,这样是非常费资源的。
可通过foreachRDD(rdd =>{rdd.foreachPartition(…)})和创建连接池来优化。
dstream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
// ConnectionPool is a static, lazily initialized pool of connections
val connection = ConnectionPool.getConnection()
partitionOfRecords.foreach(record => connection.send(record))
ConnectionPool.returnConnection(connection) // return to the pool for future reuse
}
}
Window Operations:
Spark Streaming还提供了窗口化计算,这些计算允许您在滑动的数据窗口上应用变换。
如求近三分的数据,每隔一分钟测试一次。
如图所示,每当窗口在源DStream上滑动时,该窗口内的RDD被组合,每一次对三个time(自己设置的)进行计算,间隔两个time进行一次计算。所以要设置两个参数:
1.窗口长度 - 窗口的持续时间(图中的小框框)。
2.滑动间隔 - 执行窗口操作的时间间隔(图中每个框的间隔时间)。
如图所示就可能出现一个问题,设置的间隔太短就可能出现重复计算的可能,或者某些数据没有计算,这些也是很正常的。
transform:
val spamInfoRDD = ssc.sparkContext.newAPIHadoopRDD(...) // RDD containing spam information
val cleanedDStream = wordCounts.transform { rdd =>
rdd.join(spamInfoRDD).filter(...) // join data stream with spam information to do data cleaning
...
}
用于一个RDD与DStream之间相互的操作,综合处理。
DStream <=>RDD
DStream.transform()里面就是拿到了一个rdd,这样就可以对需要的RDD进行关联操作。
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Seconds, StreamingContext}
object Transform {
def main(args: Array[String]): Unit = {
val conf=new SparkConf().setAppName("Transform").setMaster("local[2]")
val ssc=new StreamingContext(conf,Seconds(10))
//黑名单
val black=List("17")
//读取数据生成rdd
val blackRDD=ssc.sparkContext.parallelize(black).map(x=>(x,true))
//输入数据
//(name,age,gender) (zhangsan,3,2)
val DStream=ssc.socketTextStream("192.168.137.130",8888)
val output=DStream.map(x=>(x.split(",")(0),x)).transform(rdd=>{
rdd.leftOuterJoin(blackRDD).
filter(x=>x._2._2.getOrElse(false)!=true).map(x=>x._2._1)
})
//输出
output.print()
ssc.start()
ssc.awaitTermination()
}
}