BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

理解Spark的核心RDD

| 作者 张逸 关注 9 他的粉丝 发布于 2014年9月1日. 估计阅读时间: 10 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

与许多专有的大数据处理平台不同,Spark建立在统一抽象的RDD之上,使得它可以以基本一致的方式应对不同的大数据处理场景,包括MapReduce,Streaming,SQL,Machine Learning以及Graph等。这即Matei Zaharia所谓的“设计一个通用的编程抽象(Unified Programming Abstraction)。这正是Spark这朵小火花让人着迷的地方。

要理解Spark,就需得理解RDD。

RDD是什么?

RDD,全称为Resilient Distributed Datasets,是一个容错的、并行的数据结构,可以让用户显式地将数据存储到磁盘和内存中,并能控制数据的分区。同时,RDD还提供了一组丰富的操作来操作这些数据。在这些操作中,诸如map、flatMap、filter等转换操作实现了monad模式,很好地契合了Scala的集合操作。除此之外,RDD还提供了诸如join、groupBy、reduceByKey等更为方便的操作(注意,reduceByKey是action,而非transformation),以支持常见的数据运算。

通常来讲,针对数据处理有几种常见模型,包括:Iterative Algorithms,Relational Queries,MapReduce,Stream Processing。例如Hadoop MapReduce采用了MapReduces模型,Storm则采用了Stream Processing模型。RDD混合了这四种模型,使得Spark可以应用于各种大数据处理场景。

RDD作为数据结构,本质上是一个只读的分区记录集合。一个RDD可以包含多个分区,每个分区就是一个dataset片段。RDD可以相互依赖。如果RDD的每个分区最多只能被一个Child RDD的一个分区使用,则称之为narrow dependency;若多个Child RDD分区都可以依赖,则称之为wide dependency。不同的操作依据其特性,可能会产生不同的依赖。例如map操作会产生narrow dependency,而join操作则产生wide dependency。

Spark之所以将依赖分为narrow与wide,基于两点原因。

首先,narrow dependencies可以支持在同一个cluster node上以管道形式执行多条命令,例如在执行了map后,紧接着执行filter。相反,wide dependencies需要所有的父分区都是可用的,可能还需要调用类似MapReduce之类的操作进行跨节点传递。

其次,则是从失败恢复的角度考虑。narrow dependencies的失败恢复更有效,因为它只需要重新计算丢失的parent partition即可,而且可以并行地在不同节点进行重计算。而wide dependencies牵涉到RDD各级的多个Parent Partitions。下图说明了narrow dependencies与wide dependencies之间的区别:

本图来自Matei Zaharia撰写的论文An Architecture for Fast and General Data Processing on Large Clusters。图中,一个box代表一个RDD,一个带阴影的矩形框代表一个partition。

RDD如何保障数据处理效率?

RDD提供了两方面的特性persistence和patitioning,用户可以通过persist与patitionBy函数来控制RDD的这两个方面。RDD的分区特性与并行计算能力(RDD定义了parallerize函数),使得Spark可以更好地利用可伸缩的硬件资源。若将分区与持久化二者结合起来,就能更加高效地处理海量数据。例如:

input.map(parseArticle _).partitionBy(partitioner).cache()

partitionBy函数需要接受一个Partitioner对象,如:

val partitioner = new HashPartitioner(sc.defaultParallelism)

RDD本质上是一个内存数据集,在访问RDD时,指针只会指向与操作相关的部分。例如存在一个面向列的数据结构,其中一个实现为Int的数组,另一个实现为Float的数组。如果只需要访问Int字段,RDD的指针可以只访问Int数组,避免了对整个数据结构的扫描。

RDD将操作分为两类:transformation与action。无论执行了多少次transformation操作,RDD都不会真正执行运算,只有当action操作被执行时,运算才会触发。而在RDD的内部实现机制中,底层接口则是基于迭代器的,从而使得数据访问变得更高效,也避免了大量中间结果对内存的消耗。

在实现时,RDD针对transformation操作,都提供了对应的继承自RDD的类型,例如map操作会返回MappedRDD,而flatMap则返回FlatMappedRDD。当我们执行map或flatMap操作时,不过是将当前RDD对象传递给对应的RDD对象而已。例如:

def map[U: ClassTag](f: T => U): RDD[U] = new MappedRDD(this, sc.clean(f))

这些继承自RDD的类都定义了compute函数。该函数会在action操作被调用时触发,在函数内部是通过迭代器进行对应的转换操作:

private[spark]
class MappedRDD[U: ClassTag, T: ClassTag](prev: RDD[T], f: T => U)
  extends RDD[U](prev) {

  override def getPartitions: Array[Partition] = firstParent[T].partitions

  override def compute(split: Partition, context: TaskContext) =
    firstParent[T].iterator(split, context).map(f)
}

RDD对容错的支持

支持容错通常采用两种方式:数据复制或日志记录。对于以数据为中心的系统而言,这两种方式都非常昂贵,因为它需要跨集群网络拷贝大量数据,毕竟带宽的数据远远低于内存。

RDD天生是支持容错的。首先,它自身是一个不变的(immutable)数据集,其次,它能够记住构建它的操作图(Graph of Operation),因此当执行任务的Worker失败时,完全可以通过操作图获得之前执行的操作,进行重新计算。由于无需采用replication方式支持容错,很好地降低了跨网络的数据传输成本。

不过,在某些场景下,Spark也需要利用记录日志的方式来支持容错。例如,在Spark Streaming中,针对数据进行update操作,或者调用Streaming提供的window操作时,就需要恢复执行过程的中间状态。此时,需要通过Spark提供的checkpoint机制,以支持操作能够从checkpoint得到恢复。

针对RDD的wide dependency,最有效的容错方式同样还是采用checkpoint机制。不过,似乎Spark的最新版本仍然没有引入auto checkpointing机制。

总结

RDD是Spark的核心,也是整个Spark的架构基础。它的特性可以总结如下:

  • 它是不变的数据结构存储
  • 它是支持跨集群的分布式数据结构
  • 可以根据数据记录的key对结构进行分区
  • 提供了粗粒度的操作,且这些操作都支持分区
  • 它将数据存储在内存中,从而提供了低延迟性

作者简介

张逸,现就职于ThoughtWorks中国。作为一名咨询师,主要为客户提供组织的敏捷转型、过程改进、企业系统架构、领域驱动设计、大数据、代码质量提升、测试驱动开发等咨询与培训工作。

 

 


关注IT趋势,承载前沿、深入、有温度的内容。感兴趣的读者可以搜索ID:laocuixiabian,或者扫描下方二维码加关注。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

Very Good! by 孙 奇辉

加油,继续分析源码!

Re: Very Good! by 张 逸

后期会有相关的源码分析,呵呵:)

容易让人误解的描述 by 田 毅

说RDD将数据储存在内存中, 这是一个比较容易让人误解的描述

要是加一点自己的分析就更好了 by zhao xianwei

好像直接翻译的作者的博士论文啊,要是加一点自己的分析就更好了

学习资料 by M ourecho

如果要学习spark,请问有什么的推荐的资料吗?

Re: 学习资料 by qingping zhang

《Learning Spark完整版下载》 Learning Spark电子书格式是mobi格式的
www.iteblog.com/archives/1267

关于 ReduceByKey 是action还是 transfermation by 袁 丙泽

Hi,
其中在讲述RDD是什么的片段中。最后一句特加了一行注释(注意,reduceByKey是action,而非transformation)

我想在你的理解中,reduceByKey函数应该是一个action操作,而非transformation操作。

在你所写的 RDD如何保障数据处理效率?中 有一段这样的描述
RDD将操作分为两类:transformation与action。无论执行了多少次transformation操作,RDD都不会真正执行运算,只有当action操作被执行时,运算才会触发。而在RDD的内部实现机制中,底层接口则是基于迭代器的,从而使得数据访问变得更高效,也避免了大量中间结果对内存的消耗。

我在spark-shell中做了一个测试。
测试代码如下


val rawFile=sc.textFile("READEME.md")
val words=rawFile.flatMap(l=>l.split(" "))
val wordNumber= words.map(w=>(w,1))
val wordCounts=wordNumber.reduceByKey(_ + _)
wordCounts.foreach(println)

其中,发现执行到 reduceByKey 时并未触发 job,得到如下结果,由此我判断,reduceByKey仍然是一个 transfermation操作。而不是action。

请问你是如何理解 reduceByKey函数?

Re: 关于 ReduceByKey 是action还是 transfermation by 张 逸

抱歉。内容写错了。reduceByKey是一个transformation,reduce才是action。

Re: 关于 ReduceByKey 是action还是 transfermation by 杨 浩

能改下文档么

这篇文章要么修改,要么删除,错误太多,误导的人也不少 by Lee Levin

这篇文章要么修改,要么删除,错误太多,误导的人也不少

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

10 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT