1、简介
Spark Streaming抽象、架构与原理
StreamingContext 是 Spark Streaming 程序的入口,其指定sparkConf、确定DStream生成的间隔、设定 InputDStream 的源以创建 DStream;DStream 在 ssc.start() 后才会开始接收数据并计算,使用ssc.awaitTermination() 等待计算结束。
DStreamGraph 维护 InputDStream 和 OutputDStream 的实例,DStreamGraph 记录 DStream 依赖关系(静态DAG);DStreamGraph 通过 generateJobs 方法生成每个 batch 对应的 jobs,会由 JobScheduler 调度启动执行任务。
ReceiverTracker 和 Executor 通信启动 ReceiverSupvisor 实例,ReceiverSupvisor 启动后运行 Receiver 实例接收数据(接收后的数据使用 ReceiverdBlockHandler 写到 Execute 的磁盘或内存),Executor 接收完数据后将数据块的元数据报给 ReceiverTracker 。
Streaming 会维护一个 Timer,固定的时间到达后通知 Receiver 将收到的数据暂存;通过 DStreamGraph 复制出一套新的 RDD DAG(多个相关联 DStream 定时产生的);将每个 batch 对应的 jobs(ReceiverTracker 收到拟处理数据的元数据信息和DStreamGraph 生成的 RDD DAG)一同交由 JobScheduler 去调度执行。
Receiver Tracker 通过 JobScheduler 以 job 的形式将 Receiver Supervisor(job 包含的 task) 调度到不同的 Executor 上分布式运行, 其会拿到用户指定的 Receiver 对象(Receiver 是一个对象,表示了如何获取数据),并启动 Receiver 来获取外部数据,获取的数据通过 ReceiverdBlockHandler 存储完成后发送元数据给 Driver 端的 ReceiverTracker。
如何容错?
1)热备:
当 Receiver 获取的数据交给 BlockManager 存储时,如果设置了StorageLevel.MEMORY_AND_DISK_SER, 则意味着 BlockManager 不仅会在本机存储, 也会发往其它的主机进行存储, 本质就是冗余备份,如果某一个计算失败了, 通过冗余的备份, 再次进行计算即可。
2)冷备:
当 Receiver 获取的数据交给 BlockManager 存储时,存储之前先写到 WAL 中,WAL 中保存了 Redo Log,其记录了数据怎么产生的, 当出错的时候, 通过 Redo Log 去重放数据。
3)重放
有一些上游的外部系统是支持重放的(如 Kafka),Kafka 可以根据 Offset 来获取数据,当 SparkStreaming 处理过程中出错了, 只需要通过 Kafka 再次读取即可。
Spark Streaming 背压(Back Pressure)机制
Spark Streaming 反压机制(Back Pressure)
RateController 组件是 JobScheduler 的监听器,获取的信息交给速率估算器(RateEstimator)做速率的估算。Spark 2.x 中RateEstimator只支持基于 PID 的速率估算器。基于 PID 的速率估算器简单地说就是它把收集到的数据(当前批次速率)和一个设定值(上一批次速率)进行比较,然后用它们之间的差计算新的输入值,估算出一个合适的用于下一批次的流量阈值。这里估算出来的值就是流量的阈值,用于更新每秒能够处理的最大记录数如。果用户配置了 spark.streaming.receiver.maxRate 或 spark.streaming.kafka.maxRatePerPartition,那么最后到底接收多少数据取决于这两个参数和RateEstimator估算阈值中的最小值。
Spark中CheckPoint、Cache、Persist的用法、区别
Spark中的cache、persist、checkPoint三个持久化方法的区别,总的来说Cache就是Persist,而Persist有多种存储级别支持内存、磁盘的存储,也支持备份。Persist没有切断RDD的血缘关系,在数据出错时可以通过RDD血缘重新计算出来,而且落盘的文件在程序执行结束会自动删除。而CheckPoint会切断血缘,落盘的数据以文件的形式永久保存。
2、Spark检查点存储到HDSF
环境配置同020 Spark SQL,并启动HIVE
package com.jieky.studySpark
import org.apache.spark.SparkConf
import org.apache.spark.streaming.{Duration, Seconds, StreamingContext}
object App {
def main(args: Array[String]): Unit = {
// AccessControlException: Permission denied: user=Administrator, access=EXECUTE
System.setProperty("HADOOP_USER_NAME", "atguigu")
// idea关闭log4j2有颜色的日志输出
System.setProperty("log4j.skipJansi", "true")
// 定义更新状态方法,参数values为当前批次单词频度,state为以往批次单词频度
val updateFunc = (values: Seq[Int], state: Option[Int]) => {
val currentCount = values.foldLeft(0)(_ + _)
val previousCount = state.getOrElse(0)
Some(currentCount + previousCount)
}
val conf = new SparkConf().setMaster("local[3]").setAppName("NetworkWordCount")
val ssc = new StreamingContext(conf, Seconds(1))
// 将文件保存到 HDFS,若用户不主动删除是不会消失的
ssc.checkpoint("hdfs://hadoop102:9820/ck")
// Create a DStream that will connect to hostname:port, like hadoop102:9999
val lines = ssc.socketTextStream("hadoop102", 9999)
// Split each line into words
val words = lines.flatMap(_.split(" "))
// 窗口间隔Seconds(3) 和 滑动间隔Seconds(1) 一定得设置为 批处理间隔Seconds(1) 的整数倍
val windowCounts01 = words.window(Seconds(3),Seconds(1))
windowCounts01.foreachRDD(_.foreach({
println("Element:", _)
}))
val windowCounts02 = words.countByWindow(Seconds(3),Seconds(1))
windowCounts02.foreachRDD(_.foreach({
println("Count:", _)
}))
val windowCounts03 = words.map(_.toInt)
.reduceByWindow(_+_,Seconds(3),Seconds(1))
windowCounts03.foreachRDD(_.foreach({
println("Sum1:", _)
}))
val windowCounts04 = words.map(x=>(x.toInt%3,1))
.reduceByKeyAndWindow((a:Int,b:Int)=>(a+b),Seconds(3),Seconds(1))
windowCounts04.foreachRDD(_.foreach({
println("Sum2:", _)
}))
val windowCounts05 = words.map(x=>(x.toInt%3,1))
.reduceByKeyAndWindow((a:Int,b:Int)=>(a+b),(a:Int,b:Int)=>(a-b),Seconds(3),Seconds(1))
windowCounts05.foreachRDD(_.foreach({
println("Sum3:", _)
}))
val windowCounts06 = words.map(x=>(x,x.toInt%3)).countByValueAndWindow(Seconds(3),Seconds(1))
windowCounts06.foreachRDD(_.foreach({
println("Count:", _)
}))
ssc.start() // Start the computation
ssc.awaitTermination() // Wait for the computation to terminate
}
}
指定端口生成数据:nc -lk 9999
-l, --listen Bind and listen for incoming connections
-k, --keep-open Accept multiple connections in listen mode
[atguigu@hadoop102 ~]$ lsof -i:9999
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nc 1901 atguigu 3u IPv6 31183 0t0 TCP *:distinct (LISTEN)
nc 1901 atguigu 4u IPv4 31184 0t0 TCP *:distinct (LISTEN)
[atguigu@hadoop102 ~]$ kill -9 1901
[atguigu@hadoop102 ~]$ nc -lk 9999
3、滑动窗口操作
DStream由RDD 序列构成,DStream并不处理数据而是处理RDD;针对 DStream 的计算函数, 会作用于每一个 DStream 中的 RDD。DStream算子与RDD算子最大的区别在于,DStream算子名的后缀带有“AndWindow”,算子中要设定窗口间隔和滑动间隔两个参数。
sparkStreaming 连接数据库 --设计模式
Spark Streaming中使用HikariCP数据库连接池与MySQL交互
在流式计算过程中,难免会连接第三方存储平台(redis,mysql…)。在操作过程中,大部分情况是在foreachPartition/mapPartition算子中做连接操作。每一个分区只需要连接一次第三方存储平台就可以了。假如,当前streaming有100分区,当前流式计算宫分配了20个cpu,有4个cpu负责接收数据。那么,在一个批次中一共需要对第三方平台创建100次连接,同时最大并行连接第三方平台个数20-4=16个。假如30s一个批次,一天就需要频繁释放连接246060/2=43200次,假如每次创建连接和释放连接总共需要100ms,那么一天中有43200/10/60/60=4.8h的cpu时间在做创建连接和释放连接操作。这个消耗还是比较大的。
那个,该如何优化这个问题?
大家可以这样想一下:既然每一个executor就是一个JVM进程。那么,流式计算每一个批次结束,会销毁执行任务的executor吗?答案显然不会! 既然executor不会被销毁,在executor(JVM)中保持一个连接池达到连接池共享就有了可能。其次,一个executor可能会分配多于一个cpu core的情况,在执行前期,每一个executor(JVM)会同时执行多于一个的task。每一个task都需要一个连接。那么在executor中保持一个连接池,不仅可以达到跨batch的连接池共享,而且还可以达到同一个批次,被分到同一个executor(JVM)的任务的连接池共享。这样的优化可以大大减少,因为频繁连接第三方存储平台的压力,其次还可以节省频繁创建连接所消耗的时间。缺点就是,连接不释放,第三方平台需要更高的内存,才能提供更高的连接要求。
别告诉我说你会在driver端创建好连接,然后通过广播将连接广播到executor中。因为大部分连接类在实现过程没有考虑序列化的问题(实现Externalizable或Serializable接口),所以无法将连接池广播。即使能够广播,这个方式在获取连接时也会遇到千奇百怪的错误。
为每个Executor创建单例的数据库连接池,Executor每次需要使用数据库连接的时候都会先从数据库连接池中获取数据库连接对象,避免数据库连接对象的平凡创建和回收。如下解决方案用例:
① spark streaming 流式计算—跨batch连接池共享(JVM共享连接池)
② Spark Streaming 中使用c3p0连接池操作mysql数据库
③ Spark Streaming通过JDBC操作数据库