弹性分布式数据集:一种对内存集群计算的容错抽象(二)

说明

本文是翻译自讲述Spark核心设计思想的经典论文“Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing”。在翻译的过程中,更系统的理解了Spark中的RDD设计起源、优点与实际应用场景。阅读这样的经典文章,读者也能了解到一个大型的数据处理系统,是怎样被设计出来的。我在博客中计划用3篇博文来完成这个翻译,本篇为第2/3篇,如下既是正文。

  • 第1篇在这里:http://blog.csdn.net/ybdesire/article/details/78078764

4. RDDs的表示

用RDD作为抽象的挑战之一,是为他们选择一种,适应性很强的谱系表示。理想情况下,RDD系统应尽可能提供一丰富的转换运算符(例如,表2中的转换运算符),并让用户以任意方式组合它们。我们为RDD提出了一个简单的基于图形的表示,以实现这些目标。我们在Spark中使用了这种表示形式来支持广泛的转换,而不会为每个转换器添加特殊的逻辑,从而大大简化了系统设计。

简而言之,我们提出了一种通用的接口,用5个信息,来表示每一个RDD:(1)一组分区,它们是数据集的原子块;(2)一组依赖于父级RDD的方法;(3)基于其父母计算数据集的功能;(4)关于其分区方案的元数据;(5)关于其数据放置的元数据。例如,表示HDFS文件的RDD具有文件的每个块的分区,并且知道每个块所在的机器。同时,该RDD上的map结果具有相同的分区,但在计算其元素时将映射函数应用于父数据。 我们在表3中总结了这个接口

这里写图片描述

在这几这些接口时,最有趣的问题是,如何表示RDDs之间的依赖。我们发现,将依赖关系分成两种类型,是最有效的。这两种类型是:narrow依赖,父RDD的每个分区,至多由子RDD的一个分区使用;wide依赖,多个子分区都依赖于此。例如,map导致narrow依赖,而join导致wide依赖(除非父RDD是hash分区)。图4显示了其他例子。

这里写图片描述

用这种方式区分依赖关系,有两个原因。首先,narrow依赖允许在一个集群节点上按流水线执行,可以计算所有父分区。例如,我们可以在map操作后,直接执行filter操作。与此相反,wide依赖要求所有父分区的数据可用,并能使用MapReducelike操作,在整个节点上进行洗牌。其次,节点故障后的恢复对于narrow依赖而言更有效,因为只需要重新计算丢失的父分区,并且可以在不同节点上并行重新计算它们。与此相反,在具有wide赖性的谱系图中,单个故障节点可能会导致RDD的所有祖先丢失某些分区,从而需要完全重新执行。

这种RDD的通用接口,可以使Spark中的大多数转换,在少于20行代码中实现。实际上,即使是Spark的新用户,也不需要知道调度器的细节,就能实现新的转换(例如,采样和各种连接)。 我们在下面描绘一些RDD实现。

**HDFS文件:**RDDs的输入,来自于HDFS文件。对这些RDDs,partitions操作对文件中的每个块,返回一个分区。preferredLocations操作给出块所在的节点,iterator用于读取块。

**map:**在任何一个RDD上调用map操作,都会返回一个MappedRDD对象。这个对象具有和他父对象相同的分区,但使用这个操作,会向其父对象的iterator传递map操作。

**union:**在两个RDDs上调用union操作,会返回一个RDD。他的分区是这两个RDD的父分区的结合。他的每个子分区,通过narrow依赖,在响应的父分区上计算所得。

**sample:**sample操作与map操作类似。除了这一点:sample操作中,RDD存储每个分区的随机数生成器种子,以确定性地对父记录进行抽样。

**join:**对两个RDDs进行join操作,可能生成两个narrow依赖(如果他们都由相同的分区器进行hash/range分区),两个wide依赖,或混合依赖(如果其中一个的父RDD有分区器,另一个没有)。在这两种情况下,输出RDD都有一个分区器(从父节点继承的一个或默认散列分区器)。

5 实现

我们用14000行的Scala代码,实现了Spark。系统跑早Mesos集权管理器上,系统可以和Hadoop,MPI和其它应用共享资源。每一个Spark程序都作为一个单独的Mesos应用被执行,每个Spark程序都有他的driver(master)和workers。应用间的资源共享通过Mesos来处理。

Spark能从Hadoop的任何输入资源(HDFS or HBase)中,通过Hadoop输入插件APIs,读入数据。

我们现在讲解几个系统设计中有趣的部分:job调度器(5.1),交互式Spark解释器(5.2),内存管理(5.3),checkpointing机制(5.4)。

5.1 job调度

Spark的调度器使用我们的RDD表示,如第4节所述。

总的来说,我们的调度器类似于Dryad。但是,它另外考虑了内存中可用的持久性RDDs分区。一旦用户在RDD上执行操作(例如count或save),调度器就检查RDD的谱系图,以构建执行的阶段的DAG,如图5所示。每个阶段都包含尽可能多的流水线变换,具有较窄的依赖性。这些阶段的边界,必须是广泛依赖关系所需的洗牌操作,或任何已经计算出的可以使父RDD的计算短路的分区。然后,调度程序启动任务,以计算每个阶段的丢失分区,直到计算出目标RDD为止。

这里写图片描述

我们的调度员使用延迟调度,根据数据位置将任务分配给机器。如果任务需要处理节点上的内存中可用的分区,我们会将这个任务其发送到该节点。否则,如果任务处理的分区RDD中包含其它位置(比如HDFS文件),我们会将其发送到这些位置。

对于wide依赖(即shuffle依赖),我们目前在保存父分区的节点上实现中间记录,以简化故障恢复,这很像MapReduce实现映射输出。

如果任务失败,我们会在另一个节点上重新运行它,只要该阶段(stage)的父节点仍然可用。如果某些阶段变得不可用(例如,由于shuffle导致“map”的输出丢失),我们重新提交任务并行计算丢失的分区。我们还不能容忍调度程序的故障,尽管复制RDD谱系图很简单。

最后,虽然Spark中的所有计算都是针对driver程序中调用的动作而运行的,但我们也在尝试让集群上的任务(例如,map)调用lookup操作,该操作提供hash分区的RDDs元素的随机访问。 在这种情况下,任务将需要告诉调度程序计算所需的分区(如果缺失)。

5.2 解释器的集成

Scala包括与Ruby和Python类似的交互式shell。 考虑到内存中数据的低延迟,我们希望让用户从解释器交互运行Spark来查询大数据集。

Scala解释器通常通过为用户输入的每一行编译一个类,将其加载到JVM中,并调用其上的函数。此类包含一个单例对象,该对象包含该行上的变量或函数,并在初始化方法中运行该行的代码。比如,用户在解释器中,先输入var x = 5,再输入println(x)。解释器就会定义一个叫Line1的类,该类中含有x。因此,第二行代码会被编译为 println(Line1.getInstance().x)

我们在Spark中,对解释器做了两个修改:

    1. 类迁移:为了让worker节点也能同步拿到解释器为每一行输入定义的类信息(字节码),我们使解释器通过HTTP同步传输这些类。
    1. 代码生成的修改:通常,解释器通过对应类的静态方法来访问为每一行代码创建的单例对象。这意味着当我们序列化一个引用前一行上定义的变量的闭包(如上面的例子中的Line1.x)时,Java不会跟踪这个对象图,即不会将围绕x生成的Line实例迁移到worker。因此,worker节点就无法收到x。我们修改了代码生成逻辑,直接引用每个行对象的实例。

图6显示了解释器如何将由用户输入的一组行转换为Java对象。

这里写图片描述

我们发现Spark解释器可用于处理作为我们研究和探索存储在HDFS中的大数据。 我们还计划用来交互运行更高级别的查询语言,例如SQL。

5.3 内存管理

Spark对RDDs数据持久化存储,提供了三种选择:内存存储作为反序列化的Java对象;内存存储作为序列化数据;磁盘存储。第一个选项提供最快的性能,因为Java VM可以本地访问每个RDD元素。第二个选项允许用户在空间有限的情况下选择比Java对象图更有效的内存效率表示,代价是降低性能。第三个选项对于太大的RDD非常有用,太大的RDD不能保留在RAM中,而是在每次使用时重新计算成本高昂。

为了管理可用的有限内存,我们使用RDD级别的LRU驱逐策略。当我们计算新的RDD分区,但没有足够的空间来存储时,我们从最近最少访问的RDD中删除一个分区,除非与具有新分区的RDD相同。在这种情况下,我们将旧分区保留在内存中,以防止从同一RDD进入和退出循环分区。这很重要,因为大多数操作将在整个RDD上运行任务,所以很有可能将来需要用到已经存在于内存中的分区。我们发现这个默认策略到目前为止在我们所有的应用程序中都能很好的工作,但是我们还通过的持久性优先级给用户进一步的控制权。

最后,Spark上的每个实例当前都有自己独立的内存空间。 在未来的工作中,我们计划通过统一的内存管理器调查,Spark实例之间的共享RDD。

5.4 检查点的支持

虽然有了谱系图,就可以在故障后用于恢复RDD,但对于长谱系链的RDD,这种恢复可能是耗时的。 因此,检查某些RDD可以有助于稳定存储。

通常,检查点对于包含wide依赖关系的长谱系图RDD很有用,例如PageRank示例(3.2.2)中的rank数据集。在这些情况下,集群中的节点故障可能导致每个父RDD丢失一些数据片段,这就需要完全重新计算整个RDD。相比之下,对于稳定存储中数据依赖性较小的RDD(narrow依赖),例如LogLogic回归示例(3.2.1)中的点和PageRank中的链接列表,就永远不需要检查点。如果节点出现故障,则可以在其他节点上并行重新计算来自这些RDD的丢失分区,这样的代价就是,只需复制整个RDD。

Spark目前提供了一个用于检查点的API(对persist方法设置标志REPLICATE),但是确定哪个数据到检查点的决策留给了用户。不过,我们也在调查如何执行自动检查点。因为我们的调度程序知道每个数据集的大小,以及首次计算它的时间,所以它应该能够选择一组最优的RDD检查点,以最小化系统恢复时间。最后,请注意,RDD的只读属性使检查点比普通共享内存更简单。因为一致性不是一个问题,RDD可以在后台写出,而不需要程序暂停或分布式快照方案。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值