容错 指的是一个系统在部分模块出现故障时还可否持续的对外提供服务,一个高可用的系统应该具备很高的容错性;对于一个大的集群系统来讲,机器故障、网络异常等都是很常见的,Spark这样的大型分布式计算集群提供了不少的容错机制来提升整个系统的可用性。
node
通常来讲,分布式数据集的容错性有两种方式:数据检查点和记录数据的更新。
面向大规模数据分析,数据检查点操做成本很高,须要经过数据中心的网络链接在机器之间复制庞大的数据集,而网络带宽每每比内存带宽低得多,同时还须要消耗更多的存储资源。
所以,Spark选择记录更新的方式。可是,若是更新粒度太细太多,那么记录更新成本也不低。所以,RDD只支持粗粒度转换,即只记录单个块上执行的单个操做,而后将建立RDD的一系列变换序列(每一个RDD都包含了他是如何由其余RDD变换过来的以及如何重建某一块数据的信息。所以RDD的容错机制又称“血统(Lineage)”容错)记录下来,以便恢复丢失的分区。
Lineage本质上很相似于数据库中的重作日志(Redo Log),只不过这个重作日志粒度很大,是对全局数据作一样的重作进而恢复数据。数据库
相比其余系统的细颗粒度的内存数据更新级别的备份或者LOG机制,RDD的Lineage记录的是粗颗粒度的特定数据Transformation操做(如filter、map、join等)行为。当这个RDD的部分分区数据丢失时,它能够经过Lineage获取足够的信息来从新运算和恢复丢失的数据分区。由于这种粗颗粒的数据模型,限制了Spark的运用场合,因此Spark并不适用于全部高性能要求的场景,但同时相比细颗粒度的数据模型,也带来了性能的提高。apache
RDD在Lineage依赖方面分为两种:窄依赖(Narrow Dependencies)与宽依赖(Wide Dependencies,源码中称为Shuffle
Dependencies),用来解决数据容错的高效性。 缓存
- 窄依赖是指父RDD的每个分区最多被一个子RDD的分区所用,表现为一个父RDD的分区对应于一个子RDD的分区
或多个父RDD的分区对应于一个子RDD的分区,也就是说一个父RDD的一个分区不可能对应一个子RDD的多个分区。
1个父RDD分区对应1个子RDD分区,这其中又分两种状况:1个子RDD分区对应1个父RDD分区(如map、filter等算子),1个子RDD分区对应N个父RDD分区(如co-paritioned(协同划分)过的Join)。- 宽依赖是指子RDD的分区依赖于父RDD的多个分区或全部分区,即存在一个父RDD的一个分区对应一个子RDD的多个分区。
1个父RDD分区对应多个子RDD分区,这其中又分两种状况:1个父RDD对应全部子RDD分区(未经协同划分的Join)或者1个父RDD对应非所有的多个RDD分区(如groupByKey)。
spark 依赖的实现:网络
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] { //返回子RDD的partitionId依赖的全部的parent RDD的Partition(s) def getParents(partitionId: Int): Seq[Int] override def rdd: RDD[T] = _rdd }(1)窄依赖是有两种具体实现,分别以下:
一种是一对一的依赖,即OneToOneDependency:框架
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) { override def getParents(partitionId: Int) = List(partitionId) }
override def getParents(partitionId: Int) = { if(partitionId >= outStart && partitionId < outStart + length) { List(partitionId - outStart + inStart) } else { Nil } }
宽依赖的实现只有一种:ShuffleDependency。子RDD依赖于parent RDD的全部Partition,所以须要Shuffle过程:分布式
class ShuffleDependency[K, V, C]( @transient _rdd: RDD[_ <: Product2[K, V]], val partitioner: Partitioner, val serializer: Option[Serializer] = None, val keyOrdering: Option[Ordering[K]] = None, val aggregator: Option[Aggregator[K, V, C]] = None, val mapSideCombine: Boolean = false) extends Dependency[Product2[K, V]] { override def rdd = _rdd.asInstanceOf[RDD[Product2[K, V]]] //获取新的shuffleId val shuffleId: Int = _rdd.context.newShuffleId() //向ShuffleManager注册Shuffle的信息 val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle( shuffleId, _rdd.partitions.size, this) _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this)) }注意:宽依赖支持两种Shuffle Manager。
本质理解:根据父RDD分区是对应1个仍是多个子RDD分区来区分窄依赖(父分区对应一个子分区)和宽依赖(父分区对应多个子分
区)。若是对应多个,则当容错重算分区时,由于父分区数据只有一部分是须要重算子分区的,其他数据重算就形成了冗余计算。ide
对于宽依赖,Stage计算的输入和输出在不一样的节点上,对于输入节点无缺,而输出节点死机的状况,经过从新计算恢复数据这种状况下,这种方法容错是有效的,不然无效,由于没法重试,须要向上追溯其祖先看是否能够重试(这就是lineage,血统的意思),窄依赖对于数据的重算开销要远小于宽依赖的数据重算开销。性能
窄依赖和宽依赖的概念主要用在两个地方:一个是容错中至关于Redo日志的功能;另外一个是在调度中构建DAG做为不一样Stage的划分点。this
第一,窄依赖能够在某个计算节点上直接经过计算父RDD的某块数据计算获得子RDD对应的某块数据;宽依赖则要等到父RDD全部数据都计算完成以后,而且父RDD的计算结果进行hash并传到对应节点上以后才能计算子RDD。
第二,数据丢失时,对于窄依赖只须要从新计算丢失的那一块数据来恢复;对于宽依赖则要将祖先RDD中的全部数据块所有从新计算来恢复。因此在长“血统”链特别是有宽依赖的时候,须要在适当的时机设置数据检查点。也是这两个特性要求对于不一样依赖关系要采起不一样的任务调度机制和容错恢复机制。
在容错机制中,若是一个节点死机了,并且运算窄依赖,则只要把丢失的父RDD分区重算便可,不依赖于其余节点。而宽依赖须要父RDD的全部分区都存在,重算就很昂贵了。能够这样理解开销的经济与否:在窄依赖中,在子RDD的分区丢失、重算父RDD分区时,父RDD相应分区的全部数据都是子RDD分区的数据,并不存在冗余计算。在宽依赖状况下,丢失一个子RDD分区重算的每一个父RDD的每一个分区的全部数据并非都给丢失的子RDD分区用的,会有一部分数据至关于对应的是未丢失的子RDD分区中须要的数据,这样就会产生冗余计算开销,这也是宽依赖开销更大的缘由。
咱们应该都很熟悉 checkpoint 这个概念, 就是把内存中的变化刷新到持久存储,斩断依赖链 在存储中 checkpoint 是一个很常见的概念, 举几个例子
val data = sc.textFile("/tmp/spark/1.data").cache() // 注意要cache sc.setCheckpointDir("/tmp/spark/checkpoint") data.checkpoint data.count
使用很简单, 就是设置一下 checkpoint 目录,而后再rdd上调用 checkpoint 方法, action 的时候就对数据进行了 checkpoint
RDD checkpoint 过程当中会通过如下几个状态,
[ Initialized –> marked for checkpointing –> checkpointing in progress –> checkpointed ]
咱们看下状态转换流程
rdd.context.runJob(rdd, CheckpointRDD.writeToFile(path.toString, broadcastedConf))
)。若是一个RDD 咱们已经 checkpoint了那么是何时用呢, checkpoint 将 RDD 持久化到 HDFS 或本地文件夹,若是不被手动 remove 掉,是一直存在的,也就是说能够被下一个 driver program 使用。 好比 spark streaming 挂掉了, 重启后就可使用以前 checkpoint 的数据进行 recover (这个流程咱们在下面一篇文章会讲到) , 固然在同一个 driver program 也可使用。 咱们讲下在同一个 driver program 中是怎么使用 checkpoint 数据的。
若是 一个 RDD 被checkpoint了, 若是这个 RDD 上有 action 操做时候,或者回溯的这个 RDD 的时候,这个 RDD 进行计算的时候,里面判断若是已经 checkpoint 过, 对分区和依赖的处理都是使用的 RDD 内部的 checkpointRDD 变量。
具体细节以下,
若是 一个 RDD 被checkpoint了, 那么这个 RDD 中对分区和依赖的处理都是使用的 RDD 内部的 checkpointRDD 变量, 具体实现是 ReliableCheckpointRDD 类型。 这个是在 checkpoint 写流程中建立的。依赖和获取分区方法中先判断是否已经checkpoint, 若是已经checkpoint了, 就斩断依赖, 使用ReliableCheckpointRDD, 来处理依赖和获取分区。
若是没有,才往前回溯依赖。 依赖就是没有依赖, 由于已经斩断了依赖, 获取分区数据就是读取 checkpoint 到 hdfs目录中不一样分区保存下来的文件。
整个 checkpoint 读流程就完了。
在如下两种状况下,RDD须要加检查点。
- DAG中的Lineage过长,若是重算,则开销太大(如在PageRank中)。
- 在宽依赖上作Checkpoint得到的收益更大。
因为RDD是只读的,因此Spark的RDD计算中一致性不是主要关心的内容,内存相对容易管理,这也是设计者颇有远见的地方,这样减小了框架的复杂性,提高了性能和可扩展性,为之后上层框架的丰富奠基了强有力的基础。
在RDD计算中,经过检查点机制进行容错,传统作检查点有两种方式:经过冗余数据和日志记录更新操做。在RDD中的doCheckPoint方法至关于经过冗余数据来缓存数据,而以前介绍的血统就是经过至关粗粒度的记录更新操做来实现容错的。
检查点(本质是经过将RDD写入Disk作检查点)是为了经过lineage作容错的辅助,lineage过长会形成容错成本太高,这样就不如在中间阶段作检查点容错,若是以后有节点出现问题而丢失分区,从作检查点的RDD开始重作Lineage,就会减小开销。