为了账号安全,请及时绑定邮箱和手机立即绑定

翻译:Hadoop权威指南之Spark-3

标签:
Hadoop

本文原始地址

Resilient Distributed Datasets

RDD是每个spark程序的核心,本节我们来看看更多细节。

Creation

创建RDD有三种方式:从一个内存中的对象集合,被称为并行化(parallelizing) 一个集合;使用一个外部存储(比如HDFS)的数据集;转变(transform)已存在的RDD。在对少量的输入数据并行地进行CPU密集型运算时,第一种方式非常有用。例如,下面执行从1到10的独立运算:

val params = sc.parallelize(1 to 10)
val result = params.map(performExpensiveComputation)

函数performExpensiveComputation并行处理输入数据。并行性的级别由属性spark.default.parallelism决定,该属性的默认值取决于Spark的运行方式。本地运行时,是本地机器的核心数量,集群运行时,是集群中所有执行(executor)节点的核心总数量。

可以为某特定运算设置并行性级别,指定parallelize()方法的第二个参数即可:

sc.parallelize(1 to 10, 10)

创建RDD的第二种方式,是创建一个指向外部数据集的引用。我们已经见过怎样为一个文本文件创建String对象的RDD:

val text:RDD[String] = sc.textFile(inputPath)

路径inputPath可以是任意的Hadoop文件系统路径,比如本地文件系统或HDFS上的一个文件。内部来看,Spark使用旧的MapReduce API中的TextInputFormat来读取这个文件。这就意味着文件切分行为与Hadoop是一样的,因此在HDFS的情况下,一个Spark分区对应一个HDFS块(block)。这个默认行为可以改变,传入第二个参数来请求一个特殊的切分数量:

sc.textFile(inputPath, 10)

另外一个方法允许把多个文本文件作为一个整体来处理,返回的RDD中,是成对的string,第一个string是文件的路径,第二个string是文件的内容。因为每个文件都会加载进内存,所以这种方式仅仅适合于小文件:

val files:RDD[(String, String)] = sc.wholeTextFiles(inputPath)

Spark能够处理文本文件以外的其他文件格式,比如,序列文件可以这样读入:

sc.sequenceFile[IntWritable, Text](inputPath)

注意这里指定序列文件的键和值的Writable类型的方式。对于常用的Writable类型,Spark能够映射到Java中的等价物,因此我们可以使用等价的方式:

sc.sequenceFile[Int, String](inputPath)

从任意的Hadoop InputFormat来创建RDD,有两种方式:基于文件的格式,使用hadoopFile(),接收一个路径;其他格式,比如HBase的TableInputFormat,使用hadoopRDD()。这些方法使用旧的MapReduce API。如果要用新的MapReduce API,使用newAPIHadoopFile()和newAPIHadoopRDD()。下面是读取Avro数据文件的示例,使用特定的API和一个WeatherRecord类:

val job = new Job()
AvroJob.setInputKeySchema(job, WeatherRecord.getClassSchema)
val data = sc.newAPIHadoopFile(inputPath,
    classOf[AvroKeyInputFormat[WeatherRecord]],
    classOf[AvroKey[WeatherRecord]], classOf[NullWritable],
    job.getConfiguration)

除了路径之外,newAPIHadoopFile()方法还需要InputFormat的类型、键的类型、值的类型,再加上Hadoop配置,该配置中带有Avro模式,在第二行我们使用AvroJob帮助类做的设置。

创建RDD的第三种方式,是转变(transform)已存在的RDD。

Transformations and Actions

Spark提供两种类型的操作:transformationsactions。transformations从已存在的RDD生成新的RDD,而actions会触发运算并输出结果——返回给用户,或者保存到外部存储。

Actions会立刻产生影响,而transformations不会——它们是懒惰的,它们不做任何工作,直到action被触发。下面的例子,把文本文件中的每一行转为小写:

val text = sc.textFile(inputPath)
val lower: RDD[String] = text.map(_.toLowerCase())
lower.foreach(println(_))

map()方法是个transformation,Spark内部这样处理:稍晚的时候,一个函数(这里是toLowerCase())会被调用,来处理RDD中的每一个元素。这个函数实际上没有执行,直到foreach()方法(这是个action)被调用,然后Spark会运行一个job,读取输入的文件,对文件中的每一行调用toLowerCase(),然后把结果写到控制台。

怎样分辨一个操作究竟是transformation还是action呢?一个方法是看它的返回类型:如果返回类型是RDD,这是个transformation;否则就是action。当你查阅RDD的文档时,这种方法是很有用的。对RDD执行的大多数操作,可以在RDD的文档(org.apache.spark.rdd包)中找到,更多的操作在PairRDDFunctions里,这里包含了处理键值对RDD的transformations和actions。

Spark的库中包含了丰富的操作,有transformations者诸如映射(mapping)、分组(grouping)、聚合(aggregating)、再分配(repartitioning)、取样(sampling)、连接(joining)多个RDD、把RDDs作为集合(sets)对待。还有actions者诸如把RDDs物化(materializing)为集合(collections)、对RDD进行计算统计、从RDD中取样出固定数目的元素,把RDD保存到外部存储。细节内容,查看文档。

MapReduce in Spark

尽管名字很有暗示性,Spark中的map()和reduce()操作,与Hadoop MapReduce中相同名字的函数,不是直接对应的。Hadoop MapReduce中的map和reduce的通常形式是:

map: (K1, V1) -> list(K2, V2)
reduce: (K2, list(V2)) -> list(K3, V3)

从list标记可以看出,这两个函数都可以返回多个输出对。这种操作在Spark(Scala)中被实现为flatMap(),与map()很像,但是移除了一层嵌套:

scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)

scala> l.map(a => List(a))
res0: List[List[Int]] = List(List(1), List(2), List(3))

scala> l.flatMap(a => List(a))
res1: List[Int] = List(1, 2, 3)

有一种朴素的方式,可以在Spark中模拟Hadoop MapReduce。用两个flatMap()操作,中间用groupByKey()和sortByKey()来执行MapReduce的混洗(shuffle)和排序:

val input: RDD[(K1, V1)] = ...
val mapOutput: RDD[(K2, V2)] = input.flatMap(mapFn)
val shuffled: RDD[(K2, Iterable[V2])] = mapOutput.groupByKey().sortByKey()
val output: RDD[(K3, V3)] = shuffled.flatMap(reduceFn)

这里key的类型K2要继承自Scala的Ordering类型,以满足sortByKey()。

这个例子可以帮助我们理解MapReduce和Spark的关系,但是不能盲目应用。首先,这里的语义和Hadoop的MapReduce有微小的差别,sortByKey()执行的是全量排序。使用repartitionAndSortWithinPartitions()方法来执行部分排序,可以避免这个问题。然而,这样还是无效的,因为Spark有两次混洗的过程(一次groupByKey(),一次sort)。

与其重造MapReduce,不如仅仅使用那些你实际需要的操作。比如,如果不需要按key排序,你可以省略sortByKey(),这在Hadoop MapReduce中是不可能的。

同样的,大多数情况下groupByKey()太普遍了。通常只在聚合数据时需要混洗,因此应该使用reduceByKey(),foldByKey(),或者aggregateByKey(),这些函数比groupByKey()更有效率,因为它们可以在map任务中作为combiner运行。最后,flatMap()可能总是不需要的,如果总有一个返回值,map()是首选,如果有0或1个返回值,使用filter()。

Aggregation transformations

根据key来聚合键值对RDD的三个主要的transformations是reduceByKey(),foldByKey(),和aggregateByKey()。它们的工作方式稍有不同,但它们都是根据键来聚合值的,为每一个键生成一个单独的值。对应的actions是reduce(),fold()和aggregate(),它们以类似的方式运行,为整个RDD输出一个单独的值。

最简单的是reduceByKey(),它对成对儿的值反复执行一个函数,直到生成一个单独的值。例如:

val pairs: RDD[(String, Int)] =
    sc.parallelize(Array(("a", 3), ("a", 1), ("b", 7), ("a", 5)))
val sums: RDD[(String, Int)] = pairs.reduceByKey(_+_)
assert(sums.collect().toSet === Set(("a", 9), ("b", 7)))

键 a 对应的值,使用相加函数(+)聚合起来,(3 + 1)+ 5 = 9,而键 b 对应的值只有一个,因此不需要聚合。一般来说,这些操作是分布式的,在RDD的不同分区对应的任务中分别执行,因此这些函数要具有互换性和连接性。换句话说,操作的顺序和分组是不重要的。这种情况下,聚合函数可以这样执行 5 +(3 + 1),或者 3 + (1 + 5),都会返回相同的结果。

在assert语句中使用的三联相等操作符(===),来自ScalaTest,比通常的 == 操作符提供更多有用的失败信息。

下面是用foldByKey()来执行相同的操作:

val sums: RDD[(String, Int)] = pairs.foldByKey(0)(_+_)
assert(sums.collect().toSet === Set(("a", 9), ("b", 7)))

注意到这次我们需要提供一个零值,整数相加时是0,但如果是别的类型和操作,零值将是其他不同的东西。这一次,键 a 对应的值聚合的方式是((0 + 3)+ 1)+ 5)= 9(也可能是其他的顺序,不过加 0 总是第一个操作)。对于 b 是0 + 7 = 7。

使用foldByKey(),并不比reduceByKey()更强或更弱。特别地,也不能改变聚合结果的值类型。为此我们需要aggregateByKey(),例如,我们可以把那些整数值聚合到一个集合里:

val sets: RDD[(String, HashSet[Int])] =
    pairs.aggregateByKey(new HashSet[Int])(_+=_, _++=_)
assert(sets.collect.toSet === Set(("a", Set(1, 3, 5)), ("b", Set(7))))

集合相加时,零值是空集合,因此我们用new HashSet[Int]来创建一个新的可变集合。我们需要向aggregateByKey()提供两个函数作为参数。第一个函数用来控制怎样把一个Int和一个HashSet[Int]相加,本例中我们用加等函数 += 把整数加到集合里面(+ 会返回一个新集合,旧集合不会改变)。

第二个函数用来控制怎样把两个HashSet[Int]相加(这种情况发生在map任务的combiner执行之后,reduce任务把两个分区聚合之时),这里我们使用 ++= 把第二个集合的所有元素加到第一个集合里。

对于键 a,操作的顺序可能是:
(( ∅ + 3) + 1) + 5) = (1, 3, 5)
或者:
( ∅ + 3) + 1) ++ ( ∅ + 5) = (1, 3) ++ (5) = (1, 3, 5)
如果Spark使用了组合器(combiner)的话。

转变后的RDD可以持久化到内存中,因此后续的操作效率很高。



作者:qinm08
链接:https://www.jianshu.com/p/37d839b8947d


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消