Spark分布式计算原理

  • 一、RDD依赖与DAG工作原理
  • 1、RDD的依赖关系
  • 2、DAG工作原理
  • 二、RDD优化
  • 1、RDD持久化
  • 1.1、RDD缓存机制cache
  • 1.2 检查点
  • 2、RDD共享变量
  • 2.1、广播变量
  • 2.2、累加器
  • 3、RDD分区设计
  • 4、数据倾斜
  • 三、装载常见数据源
  • 3.1、装载CSV数据源
  • 3.1.1 使用SparkContext
  • 3.1.2使用SparkSession
  • 3.2、装载JSON数据源


一、RDD依赖与DAG工作原理

1、RDD的依赖关系

  • 1.1 Lineage:血统、遗传
  • RDD最重要的特性之一,保存了RDD的依赖关系
  • RDD实现了基于Lineage的容错机制
  • 1.2 依赖关系
  • 宽依赖,一个父RDD的分区被子RDD的多个分区使用

spark 分布式缓存 spark分布式计算_spark 分布式缓存

  • 窄依赖,一个父RDD的分区被子RDD的一个分区使用

spark 分布式缓存 spark分布式计算_文件结构_02

  • 1.3 宽依赖对比窄依赖
  • 宽依赖对应shuffle操作,需要在运行时将同一个父RDD的分区传入到不同的子RDD分区中,不同的分区可能位于不同的节点,就可能涉及多个节点间数据传输
  • 当RDD分区丢失时,Spark会对数据进行重新计算,对于窄依赖只需重新计算一次子RDD的父RDD分区
  • 宽依赖容错图

spark 分布式缓存 spark分布式计算_文件结构_03

  • 通过上图能看出来,当b1分区宕机后,不会影响b2的正常操作,所以得出结论:相比于宽依赖,窄依赖对优化更有利

2、DAG工作原理

  • 2.1 原理
  • 根据RDD之间的依赖关系,形成一个DAG(有向无环图)
  • DAGScheduler将DAG划分为多个Stage
  • 划分依据:是否发生宽依赖(Shuffle)
  • 划分规则:从后往前,遇到宽依赖切割为新的Stage
  • 每个Stage由一组并行的Task组成

spark 分布式缓存 spark分布式计算_spark 分布式缓存_04

  • 2.2 Stage
  • 数据本地化
  • 移动计算,而不是移动数据
  • 保证一个Stage内不会发生数据移动
  • 最佳实践

spark 分布式缓存 spark分布式计算_缓存_05

  • 尽量避免Shuffle
  • 提前部分聚合减少数据移动
  • 2.3 Spark Shuffle过程
  • 在分区之间重新分配数据
  • 父RDD中同一分区中的数据按照算子要求重新进入子RDD的不同分区中
  • 中间结果写入磁盘
  • 由子RDD拉取数据,而不是由父RDD推送
  • 默认情况下,Shuffle不会改变分区数量
  • 尽量使用窄依赖,避免使用宽依赖以减少shuffle过程,减少内存的消耗

二、RDD优化

1、RDD持久化

1.1、RDD缓存机制cache

- 1.1.1 RDD缓存机制:缓存数据至内存/磁盘,可大幅度提升Spark应用性能
- cache=persist(MEMORY)
- persist
- 缓存在遇到动作算子后,才会执行

val conf = new SparkConf().setMaster("local[*]").setAppName("cachedemo")
val sc = new SparkContext(conf)

val rdd1 = sc.textFile("in/users.csv")
rdd1.cache()

  println("不设置缓存,进行第一次读取")
  var start = System.currentTimeMillis()
  println(rdd1.count())
  var end = System.currentTimeMillis()
  println("读取时间:",end-start)
  
  println("第二次读取,将数据user.csv缓存后读取")
  start = System.currentTimeMillis()
  println(rdd1.count())
  end = System.currentTimeMillis()
  println("读取时间:",end-start)
  
  println("第三次读取,设置unpersist")
  rdd1.unpersist()
  start = System.currentTimeMillis()
  println(rdd1.count())
  end = System.currentTimeMillis()
  println("读取时间:",end-start)

/*查看结果
不设置缓存,进行第一次读取
(文件行数:,305680)
(读取时间:,695)

第二次读取,将数据user.csv缓存后读取
(文件行数:,305680)
(读取时间:,51)

第三次读取,设置unpersist
(文件行数:,305680)
(读取时间:,95)
*/

我们从上面的读取结果来看,在未进行缓存前,读取文件的速度会非常慢;
而我们第二次读取,在经过了第一次的rdd.count()的动作算子后,执行了cache的缓存操作,所以读取速度变得非常快;
而我们第三次的时候,设置了unpersist,删除了缓存后,我们来看读取速度相较于缓存后的也有了较为明显的降低。我们这边的示例文件有点小是主要原因,文件大了后会变得更加明显。

  • 1.1.2 缓存策略StorageLevel
rdd1.cache()  //与下列代码等效

rdd1.persist(StorageLevel.MEMORY_ONLY)
  • StorageLevel内容

级别

使用空间

CPU时间

是否在内存中

是否在磁盘上

备注

MEMORY_ONLY





MEMORY_ONLY_2





数据存2份

MEMORY_ONLY_SER





数据序列化

MEMORY_ONLY_SER_2





数据序列化,数据存2份

MEMORY_AND_DISK


中等

部分

部分

如果数据在内存中放不下,则溢写到磁盘

MEMORY_AND_DISK_2


中等

部分

部分

数据存2份

MEMORY_AND_DISK_SER



部分

部分

MEMORY_AND_DISK_SER_2



部分

部分

数据存2份

DISK_ONLY





DISK_ONLY_2





数据存2份

NONE

OFF_HEAP

  • 1.1.3 缓存应用场景
  • 从文件加载数据之后,因为重新获取文件成本较高
  • 经过较多的算子变换之后,重新计算成本较高
  • 单个非常消耗资源的算子之后
  • 1.1.4 使用注意事项
  • cache()或persist()后不能再有其他算子
  • cache()或persist()遇到Action算子完成后才生效
1.2 检查点
  • 1.2.1检查点:类似于快照
val conf = new SparkConf().setMaster("local[*]").setAppName("checkpointdemo")
val sc = new SparkContext(conf)

sc.setCheckpointDir("file:///E:/test/KB09checkpoint")	//设置检查点的路径
val rdd1: RDD[(String, Int)] = sc.parallelize(List(("a",1),("a",2),("b",3),("b",4)))

rdd1.checkpoint()
rdd1.collect()  //动作算子,生成快照
println(rdd1.isCheckpointed)  //判断是否为快照
println(rdd1.getCheckpointFile)

/*查看结果
true
Some(file:/E:/test/KB09checkpoint/b52d6c45-5d4e-4e56-bf55-ad789dff08ae/rdd-0)
*/

spark 分布式缓存 spark分布式计算_spark 分布式缓存_06


我们在去到设置的路径查看,可以看到在该路径已经生成了该检查点文件。

  • 1.2.2检查点与缓存的区别
  • 检查点会删除RDD lineage,而缓存不会
  • SparkContext被销毁后,检查点数据不会被删除

2、RDD共享变量

2.1、广播变量
  • 广播变量:允许开发者将一个只读变量(Driver端)缓存到每个节点(Executor)上,而不是每个任务传递一个副本

spark 分布式缓存 spark分布式计算_文件结构_07

我们通过上面的图简单看一下
现在存在三台workerNode,每个workerNode默认存在一个Executor,可以看到每个Executor中都有很多的Task任务
而我们存在一个Array("hello","hi","good afternoon"),我们把他变成广播变量broadcast后,那么这个变量在每一个Executor中都有且只保存了一份,在每个Task中没有进行单独的保存,可以防止资源的浪费。

val conf = new SparkConf().setMaster("local[1]").setAppName("broadcastdemo")
val sc = new SparkContext(conf)

//定义广播变量
val arr = Array("hello","hi","good afternoon")
val hei: Broadcast[Array[String]] = sc.broadcast(arr)

val rdd1 = sc.parallelize(List((1,"zhangsan"),(2,"lisi"),(3,"wangwu")))
val rdd2: RDD[(Int, String)] = rdd1.mapValues(x => {
  println(x)
  println(hei.value.toList)
  hei.value(2) + ":" + x		//	.value的方式访问
})
rdd2.foreach(println)

/*查看结果
zhangsan
List(hello, hi, good afternoon)
(1,hi:zhangsan)
lisi
List(hello, hi, good afternoon)
(2,hi:lisi)
wangwu
List(hello, hi, good afternoon)
(3,hi:wangwu)
*/

注意事项:

  • 1、Driver端变量在每个Executor每个Task保存一个变量副本
  • 2、Driver端广播变量在每个Executor只保存一个变量副本
2.2、累加器

累加器:只允许added操作,常用于实现计数

val conf = new SparkConf().setMaster("local[2]").setAppName("AccumulatorDemo")
val sc = new SparkContext(conf)

val acumValue = sc.accumulator(0,"My Accumulator")
val rdd1 = sc.makeRDD(List(1,2,3,4))

rdd1.foreach(x=>acumValue+=x)
println(acumValue.value)

//查看结果	10

3、RDD分区设计

  • 分区大小限制为2GB
  • 分区太少
  • 不利于并发
  • 更容易受数据倾斜影响
  • groupBy, reduceByKey, sortByKey等内存压力增大
  • 分区过多
  • Shuffle开销越大
  • 创建任务开销越大
  • 经验
  • 每个分区大约128MB,与hdfs的DataNode的块大小一致
  • 如果分区小于但接近2000,则设置为大于2000

4、数据倾斜

  • 指分区中的数据分配不均匀,数据集中在少数分区中
  • 严重影响性能
  • 通常发生在groupBy,join等之后
  • 解决方案
  • 使用新的Hash值(如对key加盐)重新分区

三、装载常见数据源

3.1、装载CSV数据源

  • 查看示例文件,可以看到该文件是有表头的csv文件

spark 分布式缓存 spark分布式计算_缓存_08

3.1.1 使用SparkContext

val conf = new SparkConf().setMaster("local[*]").setAppName("Csvdemo")
val sc = new SparkContext(conf)

val lines = sc.textFile("in/users.csv")
println("lines:",lines.count())	//查看文件总条数

val field = lines.mapPartitionsWithIndex((i, v) => {
  if (i == 0)
    v.drop(1)
  else
    v
}).map(x => x.split(","))
println("field:",field.count())

//等同于上面的方法,个人感觉下面用filter过滤的方法会更好理解和更好用
val fields: RDD[Array[String]] = lines.filter(v=>v.startsWith("user_id")==false).map(x=>x.split(","))
println("fields:",fields.count())

/*查看结果
(lines:,305680)
(field:,305679)
(fields:,305679)
*/

3.1.2使用SparkSession

  • 个人将SparkSession理解为把文件当作是一种类似于表的文件。
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()

//加载文件,这边的option中,我们把表头设为true
val df: DataFrame = spark.read.format("csv").option("header"true).load("in/users.csv")

//查看结构
println("查看文件结构")
df.printSchema()

//查看数据
println("查看文件前3条数据")
df.show(3)		//可传入参数,设置查看多少条数据,默认20条,不足则全部显示

//查看特定列的数据
println("查看user_id和locale两列的前3条数据")
df.select("user_id","locale").show(3)

/*查看结果
查看文件结构
root
 |-- user_id: string (nullable = true)
 |-- locale: string (nullable = true)
 |-- birthyear: string (nullable = true)
 |-- gender: string (nullable = true)
 |-- joinedAt: string (nullable = true)
 |-- location: string (nullable = true)
 |-- timezone: string (nullable = true)

查看文件前3条数据
+----------+------+---------+------+--------------------+------------------+--------+
|   user_id|locale|birthyear|gender|            joinedAt|          location|timezone|
+----------+------+---------+------+--------------------+------------------+--------+
|3197468391| id_ID|     1993|  male|2012-10-02T06:40:...|  Medan  Indonesia|     480|
|3537982273| id_ID|     1992|  male|2012-09-29T18:03:...|  Medan  Indonesia|     420|
| 823183725| en_US|     1975|  male|2012-10-06T03:14:...|Stratford  Ontario|    -240|
+----------+------+---------+------+--------------------+------------------+--------+
only showing top 3 rows

查看user_id和locale两列的前3条数据
+----------+------+
|   user_id|locale|
+----------+------+
|3197468391| id_ID|
|3537982273| id_ID|
| 823183725| en_US|
+----------+------+
*/
  • 我们在上面的过程中将表头设为了true,下面我们设为false来看看效果。
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val df: DataFrame = spark.read.format("csv").option("header",false).load("in/users.csv")

println("查看文件结构")
df.printSchema()
println("查看文件前3条数据")
df.show(3)

/*查看结果
查看文件结构
root
 |-- _c0: string (nullable = true)
 |-- _c1: string (nullable = true)
 |-- _c2: string (nullable = true)
 |-- _c3: string (nullable = true)
 |-- _c4: string (nullable = true)
 |-- _c5: string (nullable = true)
 |-- _c6: string (nullable = true)

查看文件前3条数据
+----------+------+---------+------+--------------------+----------------+--------+
|       _c0|   _c1|      _c2|   _c3|                 _c4|             _c5|     _c6|
+----------+------+---------+------+--------------------+----------------+--------+
|   user_id|locale|birthyear|gender|            joinedAt|        location|timezone|
|3197468391| id_ID|     1993|  male|2012-10-02T06:40:...|Medan  Indonesia|     480|
|3537982273| id_ID|     1992|  male|2012-09-29T18:03:...|Medan  Indonesia|     420|
+----------+------+---------+------+--------------------+----------------+--------+
*/
  • 我们可以看到,将表头设为false后,他就将原本表头的那一行当成了数据进行输出,而列名也改为了默认的_c0等。
  • withColumnRenamedwithColumn,对列进行操作,我们以表头为false为例进行下面的操作。

修改列名

println("df原文件结构")
df.printSchema()

val frame = df.withColumnRenamed("_c0","id")
println("改名后文件结构")
frame.printSchema()

/*查看结构
df原文件结构
root
 |-- _c0: string (nullable = true)
 |-- _c1: string (nullable = true)
 |-- _c2: string (nullable = true)
 |-- _c3: string (nullable = true)
 |-- _c4: string (nullable = true)
 |-- _c5: string (nullable = true)
 |-- _c6: string (nullable = true)

改名后文件结构
root
 |-- id: string (nullable = true)
 |-- _c1: string (nullable = true)
 |-- _c2: string (nullable = true)
 |-- _c3: string (nullable = true)
 |-- _c4: string (nullable = true)
 |-- _c5: string (nullable = true)
 |-- _c6: string (nullable = true)

修改列数据类型

println("df原文件结构")
df.printSchema()

val frame = df.withColumnRenamed("_c0","id")
val frame1 = frame.withColumn("id",frame.col("id").cast("long"))
println("修改后文件结构")
frame1.printSchema()

/*查看结果
df原文件结构
root
 |-- _c0: string (nullable = true)
 |-- _c1: string (nullable = true)
 |-- _c2: string (nullable = true)
 |-- _c3: string (nullable = true)
 |-- _c4: string (nullable = true)
 |-- _c5: string (nullable = true)
 |-- _c6: string (nullable = true)

修改后文件结构
root
 |-- id: long (nullable = true)
 |-- _c1: string (nullable = true)
 |-- _c2: string (nullable = true)
 |-- _c3: string (nullable = true)
 |-- _c4: string (nullable = true)
 |-- _c5: string (nullable = true)
 |-- _c6: string (nullable = true)
*/

/*下面代码效果等同上面的操作
val frame2 = df.withColumn("id", df.col("_c0")
.cast("long")).drop("_c0")
*/

3.2、装载JSON数据源

基本等同与上面csv装载操作,这边也就不多做叙述。