Spark高级
Spark
宽依赖和窄依赖
- 窄依赖(Narrow Dependency): 指父RDD的每个分区只被 子RDD的一个分区所使用, 例如map、 filter等
- 宽依赖(Shuffle Dependency): 父RDD的每个分区都可能被 子RDD的多个分区使用, 例如groupByKey、 reduceByKey。产生 shuffle 操作。
Stage
每当遇到一个action算子时启动一个 Spark Job
Spark Job会被划分为多个Stage,每一个Stage是由一组并行的Task组成的,使用 TaskSet 进行封装
Stage的划分依据就是看是否产生了Shuflle(即宽依赖),遇到一个Shuffle操作就会被划分为前后两个Stage
图中 ABCDEFG 都是 RDD,中间通过算子进行关联。Stage划分规则:从后向前,遇到宽依赖就切割Stage。Stage3依赖于Stage1和Stage2。
Spark Job的三种提交模式
- standalone模式
spark-submit --master spark://ubuntu-02:7077
- yarn client模式
spark-submit --master yarn --deploy-mode client
主要用于开发测试,日志会直接打印到控制台上。Driver任务只运行在提交任务的本地Spark节点,Driver调用job并与yarn集群产生大量通信,这种通信效率不高,影响效率。 - yarn cluster模式(推荐)
spark-submit --msater yarn --deploy-mode cluster
Driver 进程会运行在集群的某台机器上,日志查看需要访问集群web控制界面。
Shuffle
产生shuffle的情况:reduceByKey,groupByKey,sortByKey,countByKey,join 等操作
Spark shuffle 一共经历了这几个过程:
- 未优化的 Hash Based Shuflle
- 优化后的 Hash Based Shuffle
- Sort-Based Shuffle
https://zhuanlan.zhihu.com/p/67061627
未优化的 Hash Based Shuflle
每个 MapTask 针对每一个 ResultTask 生成一个 Bucket 缓存,Bucket缓存的个数 = MapTask个数 * ResultTask个数。这样会产生频繁的磁盘IO。ShuffleMapTask会将所有数据写入Bucket缓存后再写入磁盘文件,如果MapTask数据过多会导致Bucket数据溢出。此处Bucket大小可以根据业务场景进行优化。
- 生成大量文件,消耗文件描述符和内存
- Reduce Task 合并时使用HashMap,可能造成OOM
优化后的 Hash Based Shuffle
Bucket个数 = cpu核心数 * ResultTask个数。这样大大减小了IO操作次数。但仍有可能造成下游Reduce Task的OOM
Sort-Based Shuffle
针对每个ShuffleMapTask都只创建一个文件,可以将数据写入磁盘。文件内部按照 Partition Id 排序,内部再按照 Key 进行排序。上游Task在运行期间会顺序写入不同分区的数据,并生成索引文件记录每个分区的大小和偏移。下游Task拉去并合并数据时不再采用 HashMap 而是采用 ExternalAppendOnlyMap,该数据结构在内存不足时会写磁盘,避免了OOM
checkpoint
针对Spark Job,如果我们担心某些关键的,在后面会反复使用的RDD,因为节点故障导致数据丢失,那么可以针对该RDD启动checkpoint机制,实现容错和高可用
首先调用SparkContext的setCheckpointDir()方法,设置一个容错的文件系统目录(HDFS),然后对RDD调用checkpoint()方法。在RDD所属job结束后启动一个新的job,将checkpoint设置过的那个RDD数据写入我们之前设置的文件系统中。
checkpoint 与持久化区别
- lineage(血缘关系)是否发生改变,即RDD的依赖关系
持久化只是将数据写入磁盘,不改变RDD依赖关系;执行checkpoint后就不再有依赖的RDD - 丢失数据的可能性
持久化任然可能丢失数据,毕竟磁盘可能损坏;checkpoint通常将数据保存在高可用的文件系统。
建议:对需要checkpoint的RDD,先执行persist(StorageLevel.DISK_ONLY)。如果某个RDD设置了checkpoint却没有持久化,当Spark任务已经执行结束却由于中间的RDD没有持久化,checkpoint期间为了将这个RDD的数据写入外部存储系统,需要重新计算RDD的数据。如果进行了基于磁盘或内存的持久化,checkpoint操作时会直接从存储上读取RDD的数据,无需重新计算。
使用方法
CheckpointOpScala
package org.example
import org.apache.spark.{SparkConf, SparkContext}
object CheckpointOpScala {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
conf.setAppName("CheckpointOpScala").setMaster("local")
val sc = new SparkContext(conf)
// 1. 设置checkpoint目录,建议使用hdfs
sc.setCheckpointDir("hdfs://ubuntu:9000/checkpointDir")
val dataRDD = sc.textFile("hdfs://ubuntu:9000/file.txt")
.persist(StorageLevel.DISK_ONLY)
// 2. 对RDD调用checkpoint操作。在此之前先执行持久化可以大大减少时间消耗
dataRDD.checkpoint()
dataRDD.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
.saveAsTextFile("hdfs://ubuntu:9000/output.txt")
sc.stop()
}
}
CheckpointOpJava
package org.example;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import scala.Tuple2;
import java.util.Arrays;
public class CheckpointOpJava {
public static void main(String[] args) {
SparkConf conf = new SparkConf();
conf.setAppName("AccumulatorOpJava").setMaster("local");
JavaSparkContext sc = new JavaSparkContext(conf);
// 1. 设置checkpoint目录,建议使用hdfs
sc.setCheckpointDir("hdfs://ubuntu:9000/checkpointDir");
JavaRDD<String> dataRDD = sc.textFile("hdfs://ubuntu:9000/file.txt");
// 2. 对RDD调用checkpoint操作
dataRDD.checkpoint();
dataRDD.flatMap(s -> Arrays.asList(s.split(" ")).iterator())
.mapToPair(s -> new Tuple2<String, Integer>(s, 1))
.reduceByKey(Integer::sum)
.saveAsTextFile("c://local/path");
sc.stop();
}
}
Spark 性能优化
spark的运算主要在内存中进行,一般情况下性能瓶颈出现在内存上。将数据加载到内存中,对象结构本身也要占用空间,所以数据在内存中占用的资源大于其本身的大小。使用持久化操作cache()可看到对象在内存中占用的真实空间大小。
优化方法:
- 高性能java序列化库 Kryo
初始化是在Driver进行,实际工作在Worker的Executor进程进行,这之间需要网络传输。spark默认提供java默认序列化和kryo两种方式,kyro更快更节省空间,但是需要对要序列化的类预先进行注册,否则kryo保存类型的全名反而占用更多内存。spark默认在kryo注册scala常用类,其他的要手动注册
val conf = new SparkConf() // 开启kyro序列化 .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer") // 设置 kyro 的buffer .set("spark.kryoserializer.buffer.mb", "2") // 注册类到其中,注册时会默认开启 kyro .registerKryoClasses(Array(classOf[SparkContext]))
- 持久化或checkpoint
对多次被transformation或action操作的RDD进行持久化,避免对一个RDD反复计算。开启checkpoint保证高可用 - JVM垃圾回收调优
- 提高并行度
可通过 testFile() parallelize() 等方法的第二个参数设置并行度,也可通过 spark.default.parallelism 设置同一并行度,建议每个cpu核2~3个task - 数据本地化
如果不能实现高等级本地化,等待空闲cpu,一段时间后仍未能得到运行几乎,降低对本地化的要求选择其他节点,等待时间可设置 - 算子优化
算子优化
map vs mapPartitions & foreach vs foreachPartition
map, Transformation 算子,一次处理一条数据,运行时不断GC已处理过的数据,不会造成OOM
mapPartition, Transformation 算子,一次处理一个分区的数据,一旦元素很多而内存不足会造成OOM,适用于初始化或数据库连接,数据库链接初始化如果在Driver完成,算子在执行时是无法获得的
foreach,一次处理一条 Transformation 算子
foreachPartition,一次处理一分区 Action 算子
rdd.foreachPartition(it => {
// 获取数据库连接
it.foreach(item => {
// 使用数据库连接
})
// 关闭数据库连接
})
repartition
用于RDD的重分区,会产生shuffle操作
- 调整RDD并行度
- 解决RDD数据倾斜问题
- 控制输出数据产生的文件个数
rdd.repartition(3).foreachPartition(i => {
println("====") // 循环输出3次
})
rdd.saveAsTextFile("hdfs://xxx") // 在路径下生成3个文件
rdd.repartition(1).saveAsTextFile("hdfs://...") // 只生成一个文件
reduceByKey & groupByKey
reduceByKey:shuffle前对数据进行聚合,大大减少要传输到reduce端的数据量,减少网络传输开销,优先使用
Spark SQL
Spark的一个模块,用于结构化数据处理,最核心的抽象模型就是DataFrame。DataFrame=RDD+Schema,和关系型数据库的表很类似,可通过多来源进行构建。使用Spark SQL时要先创建一个SparkSession对象,其包括SparkContext和SqlContext两部分,SqlContext用于通过Spark操作Hive。Spark2.0开始DataFrame和DataSet是一样的。
开发时要添加依赖
<!-- https://mvnrepository.com/artifact/org.apache.spark/spark-sql -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.13</artifactId>
<version>3.3.2</version>
<scope>provided</scope>
</dependency>
DataFrame常见算子操作
https://spark.apache.org/docs/latest/sql-getting-started.html#untyped-dataset-operations-aka-dataframe-operations
package org.example.bigdata.scala
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
object SqlDemoScala {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local")
val session = SparkSession.builder()
.appName("SqlSessionDemo")
.config(conf)
.getOrCreate()
// 从json获取DataFrame
val dataFrame = session.read.json("hdfs://xxx/xx.json")
// 展示数据
dataFrame.select("name").show()
import session.implicits._ // 隐式转换
dataFrame.select($"name", $"age"+1).show()
dataFrame.filter($"age" > 20).show()
dataFrame.groupBy("age").count().show()
session.stop()
}
}
DataFrame 的sql操作
- 将DataFrame注册为一个临时表
- 使用sparkSession中sql函数执行sql语句
object SqlDemoScala {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local")
val session = SparkSession.builder().appName("SqlSessionDemo").config(conf).getOrCreate()
val dataFrame = session.read.json("hdfs://xxx/xx.json")
// 创建新一张抽象表,用于进行sql操作
dataFrame.createOrReplaceTempView("student")
session.sql("select age, count(*) as num from student group by age").show()
session.stop()
}
}
RDD 转换为 DataFrame
- 反射方式
适合写代码时已知具体属性,Spark SQL 的Scala接口自动隐式地将 case class 的RDD转换为DataFrame
object SqlDemoScala { def main(args: Array[String]): Unit = { val conf = new SparkConf().setMaster("local") val session = SparkSession.builder().appName("SqlSessionDemo").config(conf).getOrCreate() val sc = session.sparkContext val dataRDD = sc.parallelize(Array(("aaa", 1), ("bbb", 2), ("ccc", 3))) import session.implicits._ // RDD 到 DataFrame 的转换 val dataFrame = dataRDD.map(tup => Student(tup._1, tup._2)).toDF() // 此处的转换方法需要导入隐式转换包 // 基于dataFrame创建一个表,然后就可以写sql了 dataFrame.createOrReplaceTempView("student") val resDF = session.sql("select name, age from student where age > 18") // DataFrame 转 RDD val resRDD = resDF.rdd resRDD.map(row => Student(row(0).toString, row(1).toString.toInt)) .collect().foreach(println) session.stop() } }
- 编程方式
object SqlDemoScala { def main(args: Array[String]): Unit = { val conf = new SparkConf().setMaster("local") val session = SparkSession.builder().appName("SqlSessionDemo").config(conf).getOrCreate() val sc = session.sparkContext val dataRDD = sc.parallelize(Array(("aaa", 18), ("bbb", 20), ("ccc", 30))) // 组装 rowRDD val rowRDD = dataRDD.map(tup => Row(tup._1, tup._2)) // 指定元数据 val schema = StructType(Array( StructField("name", StringType, true), StructField("age", IntegerType, true), )) // 组装DataFrame val dataFrame = session.createDataFrame(rowRDD, schema) dataFrame.createOrReplaceTempView("student") val resDF = session.sql("xxx") val rdd = resDF.rdd session.stop() } }
load & save
DataFrame 存储到文件,从文件加载。不指定format时,默认使用parquet格式读写,默认支持json jdbc csv等多种格式。
object SqlDemoScala {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local")
val session = SparkSession.builder().appName("SqlSessionDemo").config(conf).getOrCreate()
val sc = session.sparkContext
val stuDF = session.read.format("json").load("xxx.json")
stuDF.select("name", "age")
.write
.format("csv")
.mode(SaveMode.Append) // 追加,指定SaveMode
.save("hdfs://xxx/xxx.csv")
session.stop()
}
}
SaveMode
目标位置已经有数据时如何处理。
TopN 主播案例
使用SQL实现 https://www.cnblogs.com/zhh567/p/16364732.html 中TopN主播问题
- 使用sparksession加载json数据
- 对这两份数据注册临时表
- 执行sql
- 打印
package org.example.bigdata.scala
import org.apache.spark.SparkConf
import org.apache.spark.sql.SparkSession
object TopN {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf()
.setMaster("local")
val sparkSession = SparkSession.builder().appName("TopN").config(sparkConf).getOrCreate()
val giftDF = sparkSession.read.json("D:\\tmp\\gift.json")
val videoDF = sparkSession.read.json("D:\\tmp\\video.json")
giftDF.createOrReplaceTempView("gift")
videoDF.createOrReplaceTempView("video")
val sql = """select
| t4.area,
| concat+_ws(',', collect_list(t4.topn)) as topn_list
|from (
| select
| t3.area, concat(t3.uid,':',cast(t3.gold_sum_all as int) ) as topn
| from (
| select
| t2.uid, t2.area, t2.gold_sum_all, row_numer() over (partition by area order by gold_sum_all desc) as num
| from (
| select
| t1.uid, max(t1.area) as area, sum(t1.gold_sum) as gold_sum_all
| from (
| select
| v.uid, v.area, g.gold_sum
| from
| video as v
| join (
| select
| g.vid, sum(g.gold) as gold_sum
| from
| gift as g
| group by
| g.vid
| )
| on
| v.vid = g.vid
| ) as t1
| group by t1.uid
| ) as t2
| ) as t3
| where
| t3.num <= 3
|) as t4
|group by t4.area""".stripMargin
val resDF = sparkSession.sql(sql)
resDF.foreach(println)
sparkSession.stop()
}
}