Apache Spark分布式计算原理
Spark WordCount运行原理
RDD的依赖关系(一)
Lineage:血统、遗传
- RDD最重要的特性之一,保存了RDD的依赖关系
- RDD实现了基于Lineage的容错机制
依赖关系
- 宽依赖
- 窄依赖
RDD的依赖关系(二)
宽依赖对比窄依赖
- 宽依赖对应shuffle操作,需要在运行时将同一个父RDD的分区传入到不同的子RDD分区中,不同的分区可能位于不同的节点,就可能涉及多个节点间数据传输
- 当RDD分区丢失时,Spark会对数据进行重新计算,对于窄依赖只需重新计算一次子RDD的父RDD分区
结论:相比于宽依赖,窄依赖对优化更有利
判断RDD依赖关系
- map (窄)
- flatMap (窄)
- filter (窄)
- distinct (宽)
- reduceByKey (宽)
- groupByKey (宽)
- sortByKey (宽)
- union (窄)
- join (窄)
DAG工作原理
根据RDD之间的依赖关系,形成一个DAG(有向无环图)
DAGScheduler将DAG划分为多个Stage
- 划分依据:是否发生宽依赖(Shuffle)
- 划分规则:从后往前,遇到宽依赖切割为新的Stage
- 每个Stage由一组并行的Task组成
为什么需要划分Stage
数据本地化
- 移动计算,而不是移动数据
- 保证一个Stage内不会发生数据移动
最佳实践
- 尽量避免Shuffle
- 提前部分聚合减少数据移动
Spark Shuffle过程
在分区之间重新分配数据
- 父RDD中同一分区中的数据按照算子要求重新进入子RDD的不同分区中
- 中间结果写入磁盘
- 由子RDD拉取数据,而不是由父RDD推送
- 默认情况下,Shuffle不会改变分区数量
RDD优化
RDD持久化
RDD共享变量
RDD分区设计
数据倾斜
RDD持久化(一)
RDD缓存机制:缓存数据至内存/磁盘,可大幅度提升Spark应用性能
- cashe=persist(MEMORY)
- persist
val u1 = sc.textFile("file:///root/data/users.txt").cache
u1.collect//删除users.txt,再试试
u1.unpersist()
缓存策略StorageLevel
- MEMORY_ONLY(默认)
- MEMORY_AND_DISK
- DISK_ONLY
- …
示例
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("cachedemo")
val sc: SparkContext = new SparkContext(conf)
/*val rdd1: RDD[(String, Int)] = sc.parallelize(List(("a",1),("a",2),("b",1),("b",2)))
rdd1.cache()
rdd1.collect.foreach(println)
println(rdd1.count())*/
val rdd2: RDD[String] = sc.textFile("in/users.csv")
rdd2.cache()
//rdd2.count()
println("第一次不将rdd2缓存后,求读取count时间")
var start: Long = System.currentTimeMillis()
println(rdd2.count())
var ends: Long = System.currentTimeMillis()
println("读取时间:"+(ends-start))
println("第二次将rdd2缓存,求读取count时间")
start = System.currentTimeMillis()
println(rdd2.count())
ends= System.currentTimeMillis()
println("读取时间:"+(ends-start))
println("第三次将rdd2缓存失效,求读取count时间")
rdd2.unpersist()
start = System.currentTimeMillis()
println(rdd2.count())
ends= System.currentTimeMillis()
println("读取时间:"+(ends-start))
/*rdd2.persist(StorageLevel.MEMORY_ONLY)*/
}
}
结果展示:
RDD持久化(二)
缓存应用场景
- 从文件加载数据之后,因为重新获取文件成本较高
- 经过较多的算子交换之后,重新计算成本较高
- 单个非常消耗资源的算子之后
使用注意事项
- cache()或persist()后不能再有其他算子
- cache()或persist()遇到Action(行动)算子后完成后才生效
RDD持久化(三)
检查点:类似于快照
sc.setCheckpointDir("hdfs:/checkpoint0918")
val rdd=sc.parallelize(List(('a',1), ('a',2), ('b',3), ('c',4)))
rdd.checkpoint
rdd.collect //生成快照
rdd.isCheckpointed
rdd.getCheckpointFile
检查点与缓存的区别
- 检查点会删除RDD Lineage,而缓存不会
- SparkContext被销毁后,检查点数据不会被删除
示例
package nj.zb.kb09.gaoji
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("CheckPointDemo")
val sc: SparkContext = new SparkContext(conf)
sc.setCheckpointDir("file:///D:/KB09checkpoint")
val rdd1: RDD[(String, Int)] = sc.parallelize(List(("a",1),("a",2),("b",1),("b",2)))
rdd1.checkpoint()
rdd1.collect() //生成快照
println(rdd1.isCheckpointed)
println(rdd1.getCheckpointFile)
}
}
结果展示:
RDD共享变量(一)
广播变量:允许开发者将一个只读变量(Driver端)缓存到每个节点(Executor)上,而不是每个任务传递一个副本
val broadcastVar=sc.broadcast(Array(1,2,3)) //定义广播变量
broadcastVar.value //访问方式
注意事项:
1、Driver端变量在每个Executor每个Task保存一个变量副本
2、Driver端广播变量在每个Executor只保存一个变量副本
示例
package nj.zb.kb09.gaoji
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("BroadcastDemo")
val sc: SparkContext = new SparkContext(conf)
//广播
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.toString())
arr(2)*/
println(x)
println(hei.value.toList)
hei.value(2)+":"+x
})
rdd2.foreach(println)
}
}
结果展示:
RDD共享变量(二)
累加器:只允许added操作,常用于实现计数
val accum = sc.accumulator(0,"My Accumulator")
sc.parallelize(Array(1,2,3,4)).foreach(x=>accum+=x)
accum.value
示例
package nj.zb.kb09.gaoji
import org.apache.spark.rdd.RDD
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
object AccumulatorDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("AccumulatorDemo")
val sc: SparkContext = new SparkContext(conf)
val acumValue: Accumulator[Int] = sc.accumulator(0,"aaaa")
val rdd1: RDD[Int] = sc.makeRDD(List(1,2,3,4))
rdd1.glom().collect.foreach(x=>println(x.toList))
println("----------------------------------")
sc.makeRDD(List(1,2,3,4)).foreach(x=>acumValue+=x)
println(acumValue.value)
}
}
结果展示:
RDD分区设计
分区大小限制为2GB
分区太少
- 不利于开发
- 更容易受数据倾斜影响
- groupBy、reduceByKey、sortByKey等内存压力增大
分区过多
- Shuffle开销越大
- 创建任务开销越大
经验
- 每个分区大约128MB
- 如果分区小于但接近2000,则设置为大于2000
数据倾斜
指分区中的数据分配不均匀,数据集中在少数分区中
- 严重影响性能
- 通常发生在groupBy、join等之后
解决方案
- 使用新的Hash值(如对key加盐)重新分区
装载CSV数据源
文件预览
使用SparkContext
val lines = sc.textFile("file:///home/kgc/data/users.csv")
val fields = lines.mapPartitionsWithIndex((idx, iter) => if (idx == 0) iter.drop(1) else iter).map(l => l.split(","))
val fields = lines.filter(l=>l.startsWith("user_id")==false).map(l=>l.split(",")) //移除首行,效果与上一行相同
使用SparkSession
val df = spark.read.format("csv").option("header", "true").load("file:///home/kgc/data/users.csv")
示例
package nj.zb.kb09.gaoji
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 = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("CsvDemo")
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("in/users.csv")
println("------------打印移除首行前的字数统计------------")
println("lines:"+lines.count())
val fields: RDD[Array[String]] = lines.mapPartitionsWithIndex((i, v) => {
if (i == 0)
v.drop(1)
else
v
}).map(x => x.split(","))
val fields2: RDD[Array[String]] = lines.filter(v=>v.startsWith("user_id")==false).map(x=>x.split(","))
println("-----------打印移除首行的字数统计------------------")
println("fields:"+ fields2.count())
println("-----------去除首行---------------")
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val df: DataFrame = spark.read.format("csv").option("header","true").load("in/users.csv")
df.printSchema()
df.select("user_id","locale","birthyear").show(5)
println("--------------不去除首行-------------")
val df2: DataFrame = spark.read.format("csv").option("header","false").load("in/users.csv")
df2.printSchema()
df2.show(5)
println("-------------把_c0改为id,把类型改为long类型-------------")
val frame = df2.withColumnRenamed("_c0","id")
frame.printSchema()
val frame1 = frame.withColumn("id",frame.col("id").cast("long"))
frame1.printSchema()
}
}
结果展示:
装载JSON数据源
文件预览
使用SparkContext
val lines = sc.textFile("file:///home/kgc/data/users.json")
//scala内置的JSON库
import scala.util.parsing.json.JSON
val result=lines.map(l=>JSON.parseFull(l))
使用SparkSession
val df = spark.read.format("json").option("header", "true").load("file:///home/kgc/data/users.json")
示例
package nj.zb.kb09.gaoji
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.{DataFrame, SparkSession}
import org.apache.spark.{SparkConf, SparkContext}
object JsonDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("JsonDemo")
val sc: SparkContext = new SparkContext(conf)
val lines: RDD[String] = sc.textFile("in/users.json")
println("----------------读取文件----------------------")
lines.collect.foreach(println)
println("---------------使用scala内置JSON库------------")
import scala.util.parsing.json._
val rdd: RDD[Option[Any]] = lines.map(x=>JSON.parseFull(x))
rdd.collect.foreach(println)
val spark: SparkSession = SparkSession.builder().config(conf).getOrCreate()
val df: DataFrame = spark.read.format("json").option("header","true").load("in/users.json")
df.printSchema()
df.show()
println("-----------------修改列名------------------")
val df2: DataFrame = df.withColumnRenamed("Age","age")
df2.printSchema()
df2.select("age","name").show()
println("------------------------修改列数据类型------------------")
println("把Age的long类型改为int类型")
val df3: DataFrame = df.withColumn("age", df.col("Age").cast("int"))
df3.printSchema()
df3.show()
println("把Age的long类型改为string类型")
val df4: DataFrame = df.withColumn("age",df.col("Age").cast("String"))
df4.printSchema()
df4.show()
println("把name的string类型改为int类型")
val df5: DataFrame = df.withColumn("name",df.col("name").cast("int"))
df5.printSchema()
df5.show()
println("把Age的加5赋给age1,把name赋给name1")
val df6: DataFrame = df.withColumn("age1",df.col("Age")+5).withColumn ("name1",df.col("name"))
df6.printSchema()
df6.show()
}
}
结果展示:
基于RDD的Spark应用程序开发(一)
典型开发步骤
开发环境
- IDEA、Maven、Scala
基于RDD的Spark应用程序开发(二)
词频计数
- 需求:统计HDFS上的某个文件的词频
- 数据格式:制表符作为分隔符
实现思路
- 首先需要将文本文件中的每一行转化成单词数组
- 其次是对每一个出现的单词进行计数
- 最后把所有相同单词的计数相加
spark-submit
--class com.kgc.bigdata.spark.core.WordCount
--master spark://hadoop000:7077
/home/hadoop/lib/spark-1.0.SNAPSHOT.jar /data/wordcount
示例
- word2.txt文件
hello java
hello spark
hello scala
hello python
hello mysql
hello hadoop
hello C
hello hive
hello hbse
- userset.properties文件
loadfile:hdfs://hadoop100:9000/kb09file/word2.txt
outfile:hdfs://hadoop100:9000/kb09file/outfile
- wordcount代码展示
package nj.zb.kb09.gaoji
import java.io.FileInputStream
import java.util.Properties
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object WordCount {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("WordCountScala")
val sc: SparkContext = new SparkContext(conf)
val properties=new Properties()
properties.load(new FileInputStream("/opt/kb09file/userset.properties"))
val loadFilePath: String = properties.get("loadfile").toString
println(loadFilePath)
val outFilePath: String = properties.get("outfile").toString
println(outFilePath)
val rdd1: RDD[String] = sc.textFile(loadFilePath)
val rdd2: RDD[(String, Int)] = rdd1.flatMap(x=>x.split(" ")).map((_,1)).reduceByKey(_+_)
rdd2.collect.foreach(println)
rdd2.saveAsTextFile(outFilePath)
sc.stop()
}
}
- 把WordCount.scala打成jar包
- 把jar包、word.txt、serset.properties三个文件放到Linux上的根目录下的opt目录下的kb9file文件下
- 在hdfs上创建文件夹
[root@hadoop100 kb09file]# hdfs dfs -mkdir /kb09file
- 给文件夹赋权
[root@hadoop100 kb09file]# hdfs dfs -chmod -R 777 /kb09file
- 把word2.txt文件上传到kb09file文件夹下
[root@hadoop100 kb09file]# hdfs dfs -put word2.txt /kb09file
- 开启hadoop
[root@hadoop100 ~]# start-all.sh
- 开启spark
[root@hadoop100 kb09file]# cd /opt/spark245/sbin/
[root@hadoop100 sbin]# ./start-all.sh
- 把jar包中META-INF下的.DSA文件和.SF文件删掉
[root@hadoop100 kb09file]# zip -d /opt/kb9file/sparkdemo.jar 'META-INF/.SF' 'META-INF/.RSA' 'META-INF/*SF'
- 提交运行
[root@hadoop100 kb09file]# spark-submit --class nj.zb.kb09.gaoji.WordCter local[*] sparkdemo.jar
- 结果展示
- 查看文件
[root@hadoop100 opt]# hdfs dfs -cat /kb09file/outfile/part-00000