分布式计算原理
一 宽依赖和窄依赖
1、宽窄依赖含义
- Spark中RDD的高效与DAG(有向无环图)有着很大的关系,在DAG调度中需要对计算过程划分stage,而划分依据就是RDD之间的依赖关系。针对不同的转换函数,RDD之间的依赖关系分类窄依赖(narrow dependency)和宽依赖(wide dependency, 也称 shuffle dependency)
- 窄依赖是指父RDD的每个分区只被子RDD的一个分区所使用,子RDD分区通常对应有限个父RDD分区(O(1),与数据规模无关)
- 相应的,宽依赖是指父RDD的每个分区都可能被多个子RDD分区所使用,子RDD分区通常对应所有的父RDD分区(O(n),与数据规模有关)
- 具体可以查看下图所示
2、窄依赖的优化有利性
-
相比于宽依赖,窄依赖对优化很有利 ,主要基于以下两点:
-
宽依赖往往对应着shuffle操作,需要在运行过程中将同一个父RDD的分区传入到不同的子RDD分区中,中间可能涉及多个节点之间的数据传输;而窄依赖的每个父RDD的分区只会传入到一个子RDD分区中,通常可以在一个节点内完成转换。
-
当RDD分区丢失时(某个节点故障),spark会对数据进行重算。
- 对于窄依赖,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重算和子RDD分区对应的父RDD分区即可,所以这个重算对数据的利用率是100%的;
- 对于宽依赖,重算的父RDD分区对应多个子RDD分区,这样实际上父RDD 中只有一部分的数据是被用于恢复这个丢失的子RDD分区的,另一部分对应子RDD的其它未丢失分区,这就造成了多余的计算;更一般的,宽依赖中子RDD分区通常来自多个父RDD分区,极端情况下,所有的父RDD分区都要进行重新计算。
- 如下图所示,b1分区丢失,则需要重新计算a1,a2和a3,这就产生了冗余计算(a1,a2,a3中对应b2的数据)。
-
-
总结:首先,窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区。例如,逐个元素地执行map、然后filter操作;而宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle,这与MapReduce类似。第二,窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,而且不同节点之间可以并行计算;而对于一个宽依赖关系的Lineage图,单个节点失效可能导致这个RDD的所有祖先丢失部分分区,因而需要整体重新计算。所以也就是说对于窄依赖,它的父RDD的重算都是必须的,不会产生冗余。
3、款窄依赖算子
- 窄依赖算子:map, flatMap, filter, union, join(父RDD是hash-partitioned ), mapPartitions, mapValues
- 宽依赖算子:distinct, …ByKey, join(父RDD不是hash-partitioned), partitionBy,groupBy
4、WordCount运行中的宽窄依赖
- 宽依赖对应Shuffle过程,由此产生另一个Stage。
二 DAG(有向无环图)工作原理
1、有向无环图
- 根据RDD之间的依赖关系,形成一个DAG(有向无环图)
- DAGScheduler将DAG划分为多个Stage
- 划分依据:是否发生宽依赖(Shuffle)
- 划分规则:从后往前,遇到宽依赖切割为新的Stage
- 每个Stage由一组并行的Task组成
2、划分Stage
- 划分Stage的必要性
- 移动计算,而不是移动数据
- 保证一个Stage内不会发生数据移动
- 分析上图
- A—>B是宽依赖,对应Stage1—>Stage3
- B—>G是窄依赖,对应Stage3内部过程
- C—>D,D—>F,E—>F都是是窄依赖,对应Stage2内部过程
- F—>G是宽依赖,对应Stage2—>Stage3
3、Shuffle过程
- 在分区之间重新分配数据
- 父RDD中同一分区中的数据按照算子要求重新进入子RDD的不同分区中
- 中间结果写入磁盘
- 由子RDD拉取数据,而不是由父RDD推送
- 默认情况下,Shuffle不会改变分区数量
4、Shuffle实践
- 比较下方两段代码
sc.textFile("hdfs:/data/test/input/names.txt")
.map(name=>(name.charAt(0),name))
.groupByKey()
.mapValues(names=>names.toSet.size)
.collect()
sc.textFile("hdfs:/data/test/input/names.txt")
.distinct(numPartitions=6)
.map(name=>(name.charAt(0),1))
.reduceByKey(_+_)
.collect()
- 结论:
- 第一段代码只有一个宽依赖算子groupByKey,所以只有一个Shuffle过程,那么也就只有两个Stage;
- 第二段代码有两个宽依赖算子distinct和reduceByKey,所以产生两个Shuffle过程,会得到三个Stage;
- Shuffle过程极其耗费资源,所以虽然两段代码的最后结果是一致的,但是第一段代码更节省资源,对优化更有利。
5、Spark的Job调度
- 集群(Standalone|Yarn)
- 一个Spark集群可以同时运行多个Spark应用
- 应用
- 我们所编写的完成某些功能的程序
- 一个应用可以并发的运行多个Job
- Job
- Job对应着我们应用中的行动算子,每次执行一个行动算子,都会提交一个Job
- 一个Job由多个Stage组成
- Stage
- 一个宽依赖做一次阶段的划分
- 阶段的个数=宽依赖个数+1
- 一个Stage由多个Task组成
- Task
- 每一个阶段的最后一个RDD的分区数,就是当前阶段的Task个数
三 RDD持久化之cache&persist&checkpoint
1、cache和persist
- cache和persist都是用于将一个RDD进行缓存的,这样在之后使用的过程中就不需要重新计算了,可以大大节省程序运行时间。
- 设置cache或者persist后,都是遇到第一个行动算子开始生效,第一个行动算子结束后完成生效,当遇到第二个行动算子才能看出效果。
- 缓存本身耗费一定时间,所以完成第一个行动算子设置缓存要比不设置缓存耗费时间长。
- cache源码
/**
* Persist this RDD with the default storage level (`MEMORY_ONLY`).
*/
def cache(): this.type = persist()
def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
- persist源码
def persist(newLevel: StorageLevel): this.type = {
if (isLocallyCheckpointed) {
// This means the user previously called localCheckpoint(), which should have already
// marked this RDD for persisting. Here we should override the old storage level with
// one that is explicitly requested by the user (after adapting it to use disk).
persist(LocalRDDCheckpointData.transformStorageLevel(newLevel), allowOverride = true)
} else {
persist(newLevel, allowOverride = false)
}
}
- 从上面代码总结区别:cache()调用了persist(),但cache只有一个默认的缓存级别MEMORY_ONLY ,而persist可以根据情况设置其它的缓存级别。也就是说rdd.cache()等价于rdd.persist(StorageLevel.MEMORY_ONLY),而persist还有MEMORY_AND_DISK,DISK_ONLY等缓存级别。
- 缓存应用场景
- 从文件加载数据之后,因为重新获取文件成本较高
- 经过较多的算子变换之后,重新计算成本较高
- 单个非常消耗资源的算子之后
- 使用注意事项
- cache()或persist()后不要再有其他算子
- cache()或persist()遇到Action算子完成后才生效,也就是说它遇到第二个Action算子才会看到效果,如运算速度变快等。
- 缓存实践
package nj.zb.kb09.gaoji
import org.apache.spark.rdd.RDD
import org.apache.spark.storage.StorageLevel
import org.apache.spark.{SparkConf, SparkContext}
object CacheDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("cache")
val sc = new SparkContext(conf)
val rdd1: RDD[String] = sc.textFile("in/users.csv")
rdd1.cache() //放到内存中缓存,遇到第一个行动算子完成后生效,也就是遇到第二个行动算子才会更快
//仅等价于rdd1.persist(StorageLevel.MEMORY_ONLY)
//而persist还有其他缓存级别可使用。
println("将rdd1缓存后,求读取count时间,即遇到第一个行动算子时缓存开始生效")
var start: Long = System.currentTimeMillis()
println(rdd1.count())
var end: Long = System.currentTimeMillis()
println(end-start)
println("完成第二个行动算子时耗费的时间")
start= System.currentTimeMillis()
println(rdd1.count())
end = System.currentTimeMillis()
println(end-start)
println("设置缓存失效,完成第三个行动算子耗费时间")
rdd1.unpersist()
start= System.currentTimeMillis()
println(rdd1.count())
end = System.currentTimeMillis()
println(end-start)
println("-------------persist(StorageLevel.MEMORY_ONLY)-------------")
rdd1.persist(StorageLevel.MEMORY_ONLY) //等价于rdd1.cache()
rdd1.count()
start=System.currentTimeMillis()
println(rdd1.count())
end=System.currentTimeMillis()
println(end-start)
println("-----persist(StorageLevel.DISK_ONLY_2)-----")
rdd1.unpersist()
rdd1.persist(StorageLevel.DISK_ONLY_2) //缓存两份到硬盘
rdd1.count()
start=System.currentTimeMillis()
println(rdd1.count())
end=System.currentTimeMillis()
println(end-start)
}
}
/*
将rdd1缓存后,求读取count时间,即遇到第一个行动算子时缓存开始生效
152840
376
完成第二个行动算子时耗费的时间
152840
31
设置缓存失效,完成第三个行动算子耗费时间
152840
43
-------------persist(StorageLevel.MEMORY_ONLY)-------------
152840
9
-----persist(StorageLevel.DISK_ONLY_2)-----
152840
29
2、checkpoint
- Cache缓存只是将数据保存起来,不切断血缘依赖。Checkpoint检查点切断血缘依赖。
- Cache缓存的数据通常存储在磁盘、内存等地方,可靠性低。Checkpoint的数据通常存储在HDFS等容错、高可用的文件系统,可靠性高。
- 建议对checkpoint()的RDD使用Cache缓存,这样checkpoint的job只需从Cache缓存中读取数据即可,否则需要再从头计算一次RDD。
- 如果使用完了缓存,可以通过unpersist()方法释放缓存
- SparkContext被销毁后,检查点数据不会被删除,因为其不在内存中,落地为文件了。
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object CheckPointDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("cache")
val sc = new SparkContext(conf)
sc.setCheckpointDir("file:///E:/KB09checkpoint") //设置检查路径
val rdd1: RDD[(String, Int)] = sc.parallelize(List(("a",1),("a",2),("a",3),("b",4),("b",5)))
rdd1.checkpoint()
rdd1.collect() //遇到动作算子,生成快照
println(rdd1.isCheckpointed)
println(rdd1.getCheckpointFile)
}
}
/*
true
Some(file:/E:/KB09checkpoint/89288478-b267-4eb4-b36b-a1c8ea432389/rdd-0)
四 RDD共享变量
1、广播变量broadcast
- 允许开发者将一个只读变量(Driver端)缓存到每个节点(Executor)上,而不是每个任务传递一个副本
- Driver端变量在每个Executor上的每个Task保存一个变量副本
- Driver端广播变量在每个Executor只保存一个变量副本
val broadcastVar=sc.broadcast(Array(1,2,3)) //定义广播变量
broadcastVar.value //访问方式
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object BroadcastDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("cache")
val sc = new SparkContext(conf)
//广播变量,在每个executor执行一次
val arr=Array("hello","hi","good afternoon")
val hei: Broadcast[Array[String]] = sc.broadcast(arr)
val rdd1: RDD[(Int, String)] = sc.parallelize(List((1,"zhangsan"),(2,"lisi"),(3,"wangwu")))
val rdd2: RDD[(Int, String)] = rdd1.mapValues(x => {
println(x)
println(arr.mkString(",")) //数组打印需要mkString
arr(1)+" : "+x //调用常规变量,事先不声明,每个Task得到一个变量副本
println(hei.value.mkString(","))
hei.value(0) + " : " + x //调用广播变量,事先声明,每个Executor的得到一个变量副本,更节省资源。
})
rdd2.foreach(println)
}
}
2、累加器accumulator
- 只允许added操作,常用于实现计数
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
object AccumulatorDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("accumulator")
val sc = new SparkContext(conf)
val acumValue: Accumulator[Int] = sc.accumulator(0,"aaaaa")
sc.makeRDD(List(1,2,3,4)).foreach(x=>{acumValue+=x;acumValue})
println(acumValue.value)
}
}
/*
10
五 RDD分区设计
1、分区设计原则
- 分区大小限制为2GB
- 分区太少
- 不利于并发
- 更容易受数据倾斜影响
- groupBy, reduceByKey, sortByKey等内存压力增大
- 分区过多
- Shuffle开销越大
- 创建任务开销越大
- 经验
- 每个分区大约128MB
- 如果分区小于但接近2000,则设置为大于2000
2、分区中的数据倾斜
- 指分区中的数据分配不均匀,数据集中在少数分区中
- 严重影响性能
- 通常发生在groupBy,join等之后
- 解决方案
- 使用新的Hash值(如对key加盐)重新分区
六 加载常用外部数据源
1、装载CSV数据源
- 文件预览
- 操作
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.{SparkConf, SparkContext}
object CsvDemo {
def main(args:Array[String]):Unit={
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("in/users.csv")
println("总行数:",lines.count()) //逗号居然也可以
println("一、通过分区索引来,认为第一行分到分区0,其实逻辑上是有问题的,凭什么第一行就到分区0,除非自定义分区规则")
val fields: RDD[Array[String]] = lines.mapPartitionsWithIndex((index, value) => { //各分区并行操作
if (index == 0) //当为0分区时
value.drop(1) //删除该分区内第一行数据,即字段名
else
value //否则返回原数据
}).map(x => x.split(","))
println("fields:"+fields.count())
println("二、采用filter过滤器就更严谨,而且如果不合理的数据有多条,用上面分区索引很难做到")
val value: RDD[String] = lines.filter(v=>v.startsWith("user_id")==false) //元素为每一行,该元素不以user_id开头就保留下来
val field: RDD[Array[String]] = value.map(x=>x.split(","))
println(field.count())
println("三、采用SparkSession加载文件")
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
println("1.使header为true,表示第一行为表头")
val df: DataFrame = spark.read.format("csv").option("header",true).load("in/users.csv")
df.printSchema()
df.select("user_id","locale","birthyear").show(3)
println("2.使header为false,表示系统生成表头")
val df1: DataFrame = spark.read.format("csv").option("header",false).load("in/users.csv")
df1.printSchema()
df1.show(3)
println("3.不改变列数,只改变列名")
val frame: DataFrame = df1.withColumnRenamed("_c0","id")
println("4.列名相同用来修改数据类型,前后列名不相同则会数据拿过来再添加一列")
val frame1: DataFrame = frame.withColumn("id",frame.col("id").cast("long"))
frame1.printSchema()
println("5.复制数据添加一列并删除原有的列")
val frame3: DataFrame = df.withColumn("id",df.col("user_id").cast("long")).drop("user_id")
frame3.printSchema()
}
}
/*
(总行数:,152840)
一、通过分区索引来,认为第一行分到分区0,其实逻辑上是有问题的,凭什么第一行就到分区0,除非自定义分区规则
fields:152839
二、采用filter过滤器就更严谨,而且如果不合理的数据有多条,用上面分区索引很难做到
152836
三、采用SparkSession加载文件
1.使header为true,表示第一行为表头
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)
+----------+------+---------+
| user_id|locale|birthyear|
+----------+------+---------+
|3197468391| id_ID| 1993|
|3537982273| id_ID| 1992|
| 823183725| en_US| 1975|
+----------+------+---------+
only showing top 3 rows
2.使header为false,表示系统生成表头
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)
+----------+------+---------+------+--------------------+----------------+--------+
| _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|
+----------+------+---------+------+--------------------+----------------+--------+
only showing top 3 rows
3.不改变列数,只改变列名
4.列名相同用来修改数据类型,前后列名不相同则会数据拿过来再添加一列
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)
5.复制数据添加一列并删除原有的列
root
|-- locale: string (nullable = true)
|-- birthyear: string (nullable = true)
|-- gender: string (nullable = true)
|-- joinedAt: string (nullable = true)
|-- location: string (nullable = true)
|-- timezone: string (nullable = true)
|-- id: long (nullable = true)
2、装载json数据源
- 数据准备
- 使用SparkContext
object JsonDemo {
def main(args: Array[String]): Unit = {
//1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
//2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("in/users.json")
println("--打印每行数据--")
lines.collect().foreach(println)
import scala.util.parsing.json._ //导入scala内置JSON库
val rdd: RDD[Option[Any]] = lines.map(x=>JSON.parseFull(x))
println("--JSON解析打印成map键值对对象--")
rdd.collect().foreach(println)
/*
--打印每行数据--
{"name":"Michael"}
{"name":"Andy","Age":30}
{"name":"Justin","Age":19}
--JSON解析打印成map键值对对象--
Some(Map(name -> Michael))
Some(Map(name -> Andy, Age -> 30.0))
Some(Map(name -> Justin, Age -> 19.0))
- 使用SparkSession
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val df: DataFrame = spark.read.format("json").option("header",true).load("in/users.json")
df.printSchema()
println("修改列名")
val df2: DataFrame = df.withColumnRenamed("Age","age")
df2.printSchema()
df2.select("age","name").show()
println("修改列名和类型")
val df3: DataFrame = df.withColumn("age",df.col("Age").cast("int"))
df3.printSchema()
println("int转string可以正常打印")
val df4: DataFrame = df.withColumn("age",df.col("Age").cast("string"))
df4.printSchema()
df4.show()
println("string转int原先是非数字String,只能用null来代替")
val df5: DataFrame = df.withColumn("Name",df.col("name").cast("int"))
df5.printSchema()
df5.show()
println("withColumn修改,列名如果相同则列数不变,不同则添加一列新列")
val df6: DataFrame = df.withColumn("age1",df.col("age")+5).withColumn("name1",df.col("name"))
df6.printSchema()
df6.show()
/*
root
|-- Age: long (nullable = true)
|-- name: string (nullable = true)
修改列名
root
|-- age: long (nullable = true)
|-- name: string (nullable = true)
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
修改列名和类型
root
|-- age: integer (nullable = true)
|-- name: string (nullable = true)
int转string可以正常打印
root
|-- age: string (nullable = true)
|-- name: string (nullable = true)
+----+-------+
| age| name|
+----+-------+
|null|Michael|
| 30| Andy|
| 19| Justin|
+----+-------+
string转int原先是非数字String,只能用null来代替
root
|-- Age: long (nullable = true)
|-- Name: integer (nullable = true)
+----+----+
| Age|Name|
+----+----+
|null|null|
| 30|null|
| 19|null|
+----+----+
withColumn修改,列名如果相同则列数不变,不同则添加一列新列
root
|-- Age: long (nullable = true)
|-- name: string (nullable = true)
|-- age1: long (nullable = true)
|-- name1: string (nullable = true)
+----+-------+----+-------+
| Age| name|age1| name1|
+----+-------+----+-------+
|null|Michael|null|Michael|
| 30| Andy| 35| Andy|
| 19| Justin| 24| Justin|
+----+-------+----+-------+