例如以上转换过程:RDDA==>RDDB==>RDDC
rdd的变换过程中分区不会有变化
假如变化过程中,第二步6 8 的分区挂了,它会从源头重新计算,它能知道这个数据是从哪个分区过来的。既中间数据坏了,会从前面找
Spark Lieage:一个RDD是如何从父RDD计算过来的
在RDD源码中有:
protected def getDependencies: Seq[Dependency[_]] = deps
compute chain
在整个RDD作业运行中,会知道每个RDD怎么来的。
RDD依赖
sc.textFile读取hdfs数据流程
sc.textFile("hdfs://").flapMap("").map().reduceByKey("")
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text], minPartitions)读取,MapReduce内容
pair._1是 LongWritable
pair._2是 Text
对于离线处理,偏移量无用,我们只关心真正的内容
然后做了map操作取第二个数据即text内容
所以textFile第一步是个hadoopRDD,hadoopRDD里会经过MapPartitionRDD
flapMap产生MapPartitionRDD
map产生MapPartitionRDD
reduceByKey产生ShuffledRDD
什么是Dependence
Dependence.scala源码可以看出,依赖是一个抽象类
@DeveloperApi abstract class Dependency[T] extends Serializable { def rdd: RDD[T] }
Dependence.scala这个抽象类有如下实现
窄依赖
/**
* :: DeveloperApi ::
* Base class for dependencies where each partition of the child RDD depends on a small number
* of partitions of the parent RDD. Narrow dependencies allow for pipelined execution.
*/
@DeveloperApi
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
/**
* Get the parent partitions for a child partition.
* @param partitionId a partition of the child RDD
* @return the partitions of the parent RDD that the child partition depends upon
*/
def getParents(partitionId: Int): Seq[Int]
override def rdd: RDD[T] = _rdd
}
即:一个父RDD的partition至多被子RDD的partition使用一次
窄依赖也是个接口,有如下实现
窄依赖有OneToOneDependency,都是在一个stage中完成的
例如map
常见的窄依赖有
map、union
窄依赖后续数据出问题,只需要从前面找出问题部分的partition即可
宽依赖
遇到宽依赖会产生shuffle,即会有新的stage产生
一个父RDD的分区会被子RDD的partition使用多次
例如reduceByKey、groupByKey,会相同key的数据到一个task中执行
例如图中的数据a会写到一起、b会写到一起、c会写到一起
宽依赖后续数据出问题,需要从前面所有partition找数据
因此宽依赖非常耗费性能。
因此大部分场景,能窄依赖实现就窄依赖实现,但是也有很少的场景需要宽依赖。
流程分析
val lines = sc.textFile("file:///data/wc.text")
val words = lines.flatMap(_.split("\t"))
val pair = lines.map((_,1))
val result = pair.reduceByKey(_ + _)
result.collect
1.使用hdfs接口把数据度过来,假设我们的数据分了三个分区
2.会按 \t分割,把数据同样partition的进行变动
3.执行map,map是窄依赖,就会按照map里的操作,会生成相同分区转换成相应的数据
4.reduceByKey里面会实现一个combine操作,想本地把同分区的数据先进行聚合(可减少后续的shuffle)
reduceByKey里面是 (_ + _),就会将上面数据进行shuffle并求和
执行过程草图
到reduceByKey的草图
整个过程由于遇到reduceByKey,所以整个任务会有两个任务,前面是stage0,到reduceByKey执行stage1
整个流程的DAG图:
reduceByKey 有map端预聚合功能
groupByKey全数据shuffle,没有预聚合
官网对于shuffle的讲解
http://spark.apache.org/docs/latest/rdd-programming-guide.html#shuffle-operations
在spark里有一些事件会触发shuffle。shuffle会触发重分区。所以整个的操作都是跨partition进行操作,这样就会涉及到在executor和机器之间拷贝数据,这样会使shuffle有非常大的消耗。
为了更好的理解shuffle过程中到底发生了什么事情,我们就举一个reduceByKey的操作。
reduceByKey会产生一个新的RDD,这个RDD会把key相同的值放在一个partition里,这个key和执行的结果可以使用reduce函数把
为了理解在shuffle期间会发生什么,我们就举一个reduceByKey操作的例子。reduceByKey操作生成一个新的RDD,其中key的所有值都被组合成一个元组,key和执行结果把key相同的关联到一期。不一定所有的值都在一个partition里,相同的一个key,不一定在一个paritition,设置都不在一台机器,所以需要对数据进行重新分发。
Operations which can cause a shuffle include repartition operations like repartition and coalesce, ‘ByKey operations (except for counting) like groupByKey and reduceByKey, and join operations like cogroup and join.
对于性能的影响
由于涉及到磁盘I/O、数据序列化和网络I/O,所以Shuffle是一项成本非常大的操作。为了阻止shuffle的数据,spark会产生 tasks 的集合,map的task处理数据,reduce的task来聚合数据。
缓存Cache 机制
http://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-persistence
例如:
val lines = sc.textFile("file:///data/wc.data")
val words = lines.flatMap(_.split("\t"))
val pair = words.map((_, 1))
val result1 = pair.reduceByKey(_ + _)
val result2 = pair.reduceByKey(_ - _)
result1.collect()
result2.collect()
当我们不使用缓存的时候,result1 和 result2会吧pair的生成执行两次
val lines = sc.textFile("file:///data/wc.data")
val words = lines.flatMap(_.split("\t"))
val pair = words.map((_, 1))
pair.cache()
val result1 = pair.reduceByKey(_ + _)
val result2 = pair.reduceByKey(_ - _)
result1.collect()
result2.collect()
spark一个最重要的特性质疑就是persisting会把数据缓存在内存上。当你去持久化RDD的时候,那么这个RDD上面的所有分区都会被持久化。他的计算在内存中可以服用到其它action中,这样就会是action变得更快。缓存是一个非常重要的一个算法。
在spark中有两个算子能实现缓存,一个是chche,一个是persit,但是他们没有区别。缓存的操作是懒执行的。
def cache(): this.type = persist()
可以使用unpersist来清空我们的缓存,清空缓存不是懒执行的,是action算子。
Storage的等级
cache调用的事persit,persit调用的是persist(StorageLevel.MEMORY_ONLY)
Storage的level等级基础类
class StorageLevel private(
private var _useDisk: Boolean,
private var _useMemory: Boolean,
private var _useOffHeap: Boolean,
private var _deserialized: Boolean,
private var _replication: Int = 1)
下面这些为设计的等级,我们可以通过StorageLevel来查看实际情况
val NONE = new StorageLevel(false, false, false, false)
val DISK_ONLY = new StorageLevel(true, false, false, false)
val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)
val MEMORY_ONLY = new StorageLevel(false, true, false, true)
val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)
val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)
val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)
val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)
val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)
val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)
val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)
val OFF_HEAP = new StorageLevel(true, true, true, false, 1)
Storage Level | Meaning |
MEMORY_ONLY | Store RDD as deserialized Java objects in the JVM. If the RDD does not fit in memory, some partitions will not be cached and will be recomputed on the fly each time they're needed. This is the default level. 以JAVA方式存到内存中。存不下的数据不会被缓存。没被缓存的会从头再计算一次。spark默认的就是这种方式 |
MEMORY_AND_DISK | Store RDD as deserialized Java objects in the JVM. If the RDD does not fit in memory, store the partitions that don't fit on disk, and read them from there when they're needed. 如果RDD内存存不下,就会把数据放在磁盘中,当你用的时候去磁盘读。 |
MEMORY_ONLY_SER (Java and Scala) | Store RDD as serialized Java objects (one byte array per partition). This is generally more space-efficient than deserialized objects, especially when using a fast serializer, but more CPU-intensive to read. 当你使用序列化的方式,通常情况下会节省磁盘空间。 |
MEMORY_AND_DISK_SER (Java and Scala) | Similar to MEMORY_ONLY_SER, but spill partitions that don't fit in memory to disk instead of recomputing them on the fly each time they're needed. 当你使用序列化的方式,通常情况下会节省磁盘空间。 |
DISK_ONLY | Store the RDD partitions only on disk. |
MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. | Same as the levels above, but replicate each partition on two cluster nodes. 2副本存储两份,策略跟上述一直。 |
OFF_HEAP (experimental) | Similar to MEMORY_ONLY_SER, but store the data in off-heap memory. This requires off-heap memory to be enabled. |
如何正确选择一个存储策略
spark的存储策略需要在内存的使用和cpu的使用来权衡。
spark底层默认使用MEMORY_ONLY,
1.如果你的RDD使用MEMORY_ONLY搞得,就用默认的
2.如果MEMORY_ONLY存储不了。使用MEMORY_ONLY_SER方式
3.不要把数据缓存到磁盘上,推荐重新计算一个分区可能会比缓存到磁盘上更好。
4.使用副本的方式
spark能够监控每个节点的缓存情况,对于不怎用的,spark会判断出了清空缓存。
repartition和coalesce
data.repartition(4) //底层走的是shuffle
data.coalesce(1) //结果默认是个窄依赖,coleasce指定的分区数大于原分区数,参数就会失效,还会走原分区。coalesce
data.coalesce(1, true) //如果添加第二个参数为true,coleasce指定的分区数大于原分区数,参数也会生效。
repartition调用的是coalesce算子,shuffle默认为true stage
coalesce shuffle默认为false 传shuffle为true,就和repartition