文章目录
- 一 shuffle
- 1.1 Spark的Shuffle过程
- 1.2 shuffle write 和 shuffle read
- 1.3 shuffle过程中的分区排序问题
- 1.4 会导致发生shuffle的算子
- 1.5 shuffle调优
- 1.6 使用任务管理器 观察task, stage, shufflewrite, shuffleread
- 二 checkPoint
- 2.1 checkPoint应用场景
- 2.2 checkPoint实现步骤
- 三 JDBCRDD
- 四 自定义排序(二次排序)
- 4.1 第一种:使用隐式转换
- 4.2 第二种: 继承Ordered
- 五 广播变量
一 shuffle
1.1 Spark的Shuffle过程
在进行一个key对应的values的聚合时,首先,上一个stage的每个map task就必须保证将自己处理的当前分区中的数据相同key写入一个分区文件中,可能会多个不同的分区文件,接着下一个stage的reduce task就必须从上一个stage的所有task所在的节点上,将各个task写入的多个分区文件中找到属于自己的分区文件,然后将属于自己的分区数据拉取过来,这样就可以保证每个key对应的所有values都汇聚到一个节点上进行处理和聚合,这个过程就称之为shuffle!!!
shuffle操作,会导致大量的数据在不同的节点之间进行传输,因此,shuffle过程是Spark中最复杂、最消耗性能的一种操作
比如:reduceByKey算子会将上一个RDD中的每个key对应的所有value都聚合成一个value,然后生成一个新的RDD,新的RDD的元素类型就是<key, value>的格式,每个key对应一个聚合起来的value,在这里,最大的问题在于,对于上一个RDD来说,并不是一个key对应的所有的value都在一个partition中的,更不太可能key的所有value都在一个节点上,对于这种情况,就必须在集群中将各个节点上同一个key对应的values统一传输到一个节点上进行聚合处理,这个过程势必会发生大量的网络IO。
1.2 shuffle write 和 shuffle read
为了实时shuffle操作,spark才有stage的概念,在发生shuffle操作的算子中,需要进行stage的划分
shuffle操作的前半部分,属于上一个stage的范围,通常称之为map task,shuffle操作的后半部分,属于下一个stage的范围,通常称之为reduce task,其中map task负责数据的组织,也就是将同一个key对应的value都写入同一个下游task对应的分区文件中,其中reduce task负责数据的聚合,也就是将上一个stage的task所在的节点上,将属于自己的各个分区文件都拉取过来进行聚合
map task会将数据先保存在内存中,如果内存不够时,就溢写到磁盘文件中,reduce task会读取各个节点上属于自己的分区磁盘文件到自己节点的内存中进行聚合。
task的生成,一定是在stage范围内,不会跨越stage, task的数量可以这样计算:RDD分区的数量乘以stage的数量(必须是没有重分区的操作)
- shuffle过程中分为shuffle write和shuffle read,而且会在不同的stage中进行的
- shuffle write发生在map task。为什么要发生写操作?
1. 为了避免大量的分区文件占用大量的内存而导致oom。
2. 保存到磁盘可以保证数据的安全性。 - shuffle read发生在reduce task端,是指下游RDD读取上游RDD的过程,也就是reduce task读取并合并的过程
1.3 shuffle过程中的分区排序问题
默认情况下,shuffle操作是不会对每个分区中的数据进行排序的, 如果想要对每个分区中的数据进行排序,可以使用三种方法:
- 使用mapPartitions算子把每个partition取出来进行排序
- 使用repartitionAndSortWithinPartitions(该算子是对RDD进行重分区的算子),在重分区的过程中同时就进行分区内数据的排序
- 使用sortByKey对所有分区的数据进行全局排序
以上三种方法,mapPartitions代价比较小,因为不需要进行额外的shuffle操作,repartitionAndSortWithinPartitions和sortByKey可能会进行额外的shuffle操作,因此性能并不是很高
1.4 会导致发生shuffle的算子
- byKey类的算子:比如reduceByKey、groupByKey、sortByKey、aggregateByKey、combineByKey
- repartition类的算子:比如repartition(少量分区变成多个分区会发生shuffle)、repartitionAndSortWithinPartitions、coalesce(需要指定是否发生shuffle)、partitionBy
- join类的算子:比如join(先groupByKey后再join就不会发生shuffle)、cogroup
注意:
- 首先对于上述操作,能不用shuffle操作,就尽量不用,尽量使用不发生shuffle的操作。
- 其次,如果使用了shuffle操作,那么肯定要进行shuffle的调优,甚至是解决遇到的数据倾斜问题。
1.5 shuffle调优
shuffle操作是spark中唯一最消耗性能的过程, 因此也就成了最需要进行性能调优的地方,最需要解决线上报错的地方,也就是唯一可能出现数据倾斜的地方
shuffle操作会消耗大量的内存,因为无论是网络传输数据之前还是之后,都会使用大量内存中数据结构来实施聚合操作,在聚合过程中,如果内存不够,只能溢写到磁盘文件中去,此时就会发生大量的网络IO,降低性能。
此外,shuffle过程中,会产生大量的中间文件,也就是map side写入的大量分区文件,这些文件会一直保留着,直到RDD不再被使用,而且被gc回收掉了,才会去清理中间文件,这主要是为了:如果要重新计算shuffle后RDD,那么map side不需要重新再做一次磁盘写操作,但是这种情况下,如果在应用程序中一直保留着对RDD的引用,导致很长的时间以后才会进行回收操作,保存中间文件的目录,由spark.local.dir属性指定
所以,spark性能的消耗体现在:内存的消耗、磁盘IO、网络的IO
设置参数两种方式
- 写到spark-env.sh中
- 在程序中使用set设置
属性名称 | 默认值 | 属性说明 |
spark.reducer.maxSizeInFlight | 48m | reduce task的buffer缓冲,代表了每个reduce task每次能够拉取的map side数据最大大小,如果内存充足,可以考虑加大,从而减少网络传输次数,提升性能 |
spark.shuffle.blockTransferService | netty | shuffle过程中,传输数据的方式,两种选项,netty或nio,spark 1.2开始,默认就是netty,比较简单而且性能较高,spark 1.5开始nio就是过期的了,而且spark 1.6中会去除掉 |
spark.shuffle.compress | true | 是否对map side输出的文件进行压缩,默认是启用压缩的,压缩器是由spark.io.compression.codec属性指定的,默认是snappy压缩器,该压缩器强调的是压缩速度,而不是压缩率 |
spark.shuffle.consolidateFiles | false | 默认为false,如果设置为true,那么就会合并map side输出文件,对于reduce task数量特别的情况下,可以极大减少磁盘IO开销,提升性能 |
spark.shuffle.file.buffer | 32k | map side task的内存buffer大小,写数据到磁盘文件之前,会先保存在缓冲中,如果内存充足,可以适当加大,从而减少map side磁盘IO次数,提升性能 |
spark.shuffle.io.maxRetries | 3 | 网络传输数据过程中,如果出现了网络IO异常,重试拉取数据的次数,默认是3次,对于耗时的shuffle操作,建议加大次数,以避免full gc或者网络不通常导致的数据拉取失败,进而导致task lost,增加shuffle操作的稳定性 |
spark.shuffle.io.retryWait | 5s | 每次重试拉取数据的等待间隔,默认是5s,建议加大时长,理由同上,保证shuffle操作的稳定性 |
spark.shuffle.io.numConnectionsPerPeer | 1 | 机器之间的可以重用的网络连接,主要用于在大型集群中减小网络连接的建立开销,如果一个集群的机器并不多,可以考虑增加这个值 |
spark.shuffle.io.preferDirectBufs | true | 启用堆外内存,可以避免shuffle过程的频繁gc,如果堆外内存非常紧张,则可以考虑关闭这个选项 |
spark.shuffle.manager | sort | ShuffleManager,Spark 1.5以后,有三种可选的,hash、sort和tungsten-sort,sort-based ShuffleManager会更高效使用内存,并且避免产生大量的map side磁盘文件,从Spark 1.2开始就是默认的选项,tungsten-sort与sort类似,但是内存性能更高 |
spark.shuffle.memoryFraction | 0.2 | 如果spark.shuffle.spill属性为true,那么该选项生效,代表了executor内存中,用于进行shuffle reduce side聚合的内存比例,默认是20%,如果内存充足,建议调高这个比例,给reduce聚合更多内存,避免内存不足频繁读写磁盘 |
spark.shuffle.service.enabled | false | 启用外部shuffle服务,这个服务会安全地保存shuffle过程中,executor写的磁盘文件,因此executor即使挂掉也不要紧,必须配合spark.dynamicAllocation.enabled属性设置为true,才能生效,而且外部shuffle服务必须进行安装和启动,才能启用这个属性 |
spark.shuffle.service.port | 7337 | 外部shuffle服务的端口号,具体解释同上 |
spark.shuffle.sort.bypassMergeThreshold | 200 | 对于sort-based ShuffleManager,如果没有进行map side聚合,而且reduce task数量少于这个值,那么就不会进行排序,如果你使用sort ShuffleManager,而且不需要排序,那么可以考虑将这个值加大,直到比你指定的所有task数量都打,以避免进行额外的sort,从而提升性能 |
spark.shuffle.spill | true | 当reduce side的聚合内存使用量超过了spark.shuffle.memoryFraction指定的比例时,就进行磁盘的溢写操作 |
spark.shuffle.spill.compress | true | 同上,进行磁盘溢写时,是否进行文件压缩,使用spark.io.compression.codec属性指定的压缩器,默认是snappy,速度优先 |
1.6 使用任务管理器 观察task, stage, shufflewrite, shuffleread
spark自带的任务管理器, 使用UI界面可以查看任务的运行信息, 例如task任务, stage的划分, DAG, shuffleWriter, shuffleRead等等详细信息, 先运行一个Wordcount任务,然后在UI界面查看.
sc.textFile("hdfs://hadoop:8020/word.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).collect
二 checkPoint
2.1 checkPoint应用场景
在应用程序执行过程中, 有时候某些RDD的数据需要在其他地方多次用到(包括其他Job), 为了使得整个依赖链条不至于很长导致执行缓慢, 可以用checkPoint来缩短依赖链条, 推荐将数据checkPoint到HDFS中, 保证了数据的安全性(便于在使用数据的时候进行拉取).
在代码执行时, 如果用到某个RDD的数据的时候, 首先会检查是否做了缓存, 如果做了缓存, 会直接调用RDD, 如果没有做缓存, 会判断是否做了checkPoint, 如果做了checkPoint, 会在checkPoint指定的路径下拉取数据, 如果checkPoint也没有做, 那么只能重新计算获取数据
2.2 checkPoint实现步骤
- 设置一个checkPoint目录
- 把要checkPoint的RDD的数据进行cache
- checkPoint
val rdd = sc.textFile("hdfs://hadoop:8020/word.txt").flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)
sc.setCheckpointDir("hdfs://hadoop:8020/checkpoint")
rdd.persist
rdd.checkpoint
rdd.isCheckpointed
rdd.take(1)
rdd.getCheckpointFile
rdd.isCheckpointed
三 JDBCRDD
spark中提供了JDBCRDD, 用于和关系型数据库进行交互, 只能查询, 不能进行增删改
import java.sql.{Date, DriverManager}
import org.apache.spark.rdd.JdbcRDD
import org.apache.spark.{SparkConf, SparkContext}
object JdbcRDDDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("JdbcRDDDemo").setMaster("local[2]")
val sc = new SparkContext(conf)
val url = "jdbc:mysql://localhost/test"
val user = "root"
val pwd = "12312300.."
val sql = "select * from weather where temperature>? and temperature <?"
val conn = () => {
Class.forName("com.mysql.jdbc.Driver").newInstance()
DriverManager.getConnection(url, user, pwd)
}
val res = new JdbcRDD(
sc, conn, sql, 0, 30, 1,
res => {
val id: Int = res.getInt("id")
val recordDate: Date = res.getDate("recordDate")
val temperature: Int = res.getInt("temperature")
(id, recordDate, temperature)
}
)
println(res.collect.toBuffer)
sc.stop()
}
}
四 自定义排序(二次排序)
如果有一个自定义对象, 我们创建了多个该对象的实例, 现在需要对这些实例 进行排序, 先对实例中的某个字段进行比较, 如果 两个字段正好相等, 那我需要用另外一个字段进行排序,如果用sortBy算子是无法实现该功能的,此时可以用Spark提供的自定义排序。
4.1 第一种:使用隐式转换
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object CustomSortDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("CustomSortDemo").setMaster("local[2]")
val sc = new SparkContext(conf)
val girlInfo: RDD[(String, Int, Int)] = sc.parallelize(List(("mimi", 99, 33), ("bingbing", 80, 35), ("yuanyuan", 80, 32)))
// 第一种排序
// godess => Girl(godess._2, godess._3) 仅仅是指定了用于比较的字段, 没有比较规则
import MyPredef.grilOrdering
val res: RDD[(String, Int, Int)] = girlInfo.sortBy(godess => Girl(godess._2, godess._3), false)
println(res.collect.toBuffer)
}
}
case class Girl(fv: Int, age: Int)
object MyPredef {
//第一种排序方式
implicit val grilOrdering = new Ordering[Girl] {
override def compare(x: Girl, y: Girl): Int = {
if (x.fv != y.fv) x.fv - y.fv else y.age - x.age
}
}
}
4.2 第二种: 继承Ordered
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object CustomSortDemo {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("CustomSortDemo").setMaster("local[2]")
val sc = new SparkContext(conf)
val girlInfo: RDD[(String, Int, Int)] = sc.parallelize(List(("mimi", 99, 33), ("bingbing", 80, 35), ("yuanyuan", 80, 32)))
// 第二种排序
val res: RDD[(String, Int, Int)] = girlInfo.sortBy(godess => Girl(godess._2, godess._3), false)
println(res.collect.toBuffer)
}
}
case class Girl(fv: Int, age: Int) extends Ordered[Girl]{
override def compare(y: Girl): Int = {
if (this.fv != y.fv) this.fv - y.fv else y.age - this.age
}
}
运行结果
五 广播变量
如果需要将Driver端的某个变量的值在Executor端多次使用, 可以将Driver端的某个变量的值以广播的方式传给多个Executor端, Executor端在使用该值的时候就可以不经过网络IO从Driver端获取, 而是直接从本地的缓存读取该值即可, 这样既可以减少网络IO, 又可以节省内存(因为一个Executor只有一份广播变量就可以)
- 广播过来的值会保存到Executor端的BlockManager
- 广播变量不可以广播RDD, 因为RDD不会封装具体的值, 只能广播确切的值
- 广播变量的值不宜太大, 如果太大, 就会把Executor端的缓存占用太多, 而导致计算时的内存太少导致计算速度太慢或出现oom
- 广播变量只能在Driver端定义, 不能在Executor端定义
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/*
* @Description: 广播变量
* ClassName BroadcastTest
* @Author: WCH
* @CreateDate: 2019/1/5$ 14:28$
* @Version: 1.0
*/
/**
* 如果不使用广播变量
* 首先arr是在Driver端, 在task每次执行过程中, 都会从Driver端拉取arr的数据,到Executor端进行计算
* 有多少task就会有多少次拉取arr的过程, 如果arr的数据量特别大, 此时就有可能在Executor发生oom
*/
object BroadcastTest {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("BroadcastTest").setMaster("local[2]")
val sc = new SparkContext(conf)
val arr = Array("hello","zhangsan")
val broadCast: Broadcast[Array[String]] = sc.broadcast(arr)
val lines: RDD[String] = sc.textFile("hdfs://hadoop:8020/word.txt")
val filtered: RDD[String] = lines.filter(_.contains(broadCast.value(0)))
println(filtered.collect.toBuffer)
}
}