spark常见的转换和动作

来源:互联网 发布:心理学实验软件 编辑:程序博客网 时间:2024/05/29 12:10

RDD支持两种操作:

  • 转换:从现有的数据集创建一个新的数据集;
  • 动作:在数据集上运行计算后,返回一个值给驱动程序。

例如,map 是一种转换,它将数据集每一个元素都传递给函数,并返回一个新的分布数据集表示结果,而 reduce 是一种动作,通过一些函数将所有的元素叠加起来,并将最终结果返回给运行程序。

Spark 中的 所有转换都是惰性的,也就是说,他们并不会直接计算结果。相反的,它们只是记住应用到基础数据集上的这些转换动作。只有当发生一个要求返回结果给运行程序的动作时,这些转换才会真正运行。

默认情况下,每一个转换过的 RDD 都会在你运行一个动作时被重新计算。不过,你也可以使用 persist 或者 cache 方法,持久化一个 RDD 在内存中。在这种情况下,Spark 将会在集群中,保存相关元素,下次你查询这个 RDD 时,它将能更快速访问。除了持久化到内存,Spark 也支持在磁盘上持久化数据集,或在节点之间复制数据集。

Scala 示例:

   val lines = sc.textFile("data.txt")val lineLengths = lines.map(s => s.length)val totalLength = lineLengths.reduce((a, b) => a + b)

Java 示例:

   JavaRDD<String> lines = sc.textFile("data.txt");JavaRDD<Integer> lineLengths = lines.map(s -> s.length());int totalLength = lineLengths.reduce((a, b) -> a + b);

Python 示例:

   lines = sc.textFile("data.txt")lineLengths = lines.map(lambda s: len(s))totalLength = lineLengths.reduce(lambda a, b: a + b)

代码说明:

  • 第一行定义了一个基础 RDD,但并没有开始载入内存,仅仅将 lines 指向了这个file
  • 第二行也仅仅定义了 linelengths 是作为 map 的结果,但也没有开始运行 map 这个过程
  • 直到第三句话才开始运行,各个 worker 节点开始运行自己的 map、reduce 过程

你也可以调用 lineLengths.persist() 来持久化 RDD。

除了使用 lambda 表达式,也可以通过函数来运行转换或者动作,使用函数需要注意局部变量的作用域问题。

例如下面的 Python 代码中的 field 变量:

   class MyClass(object):    def __init__(self):        self.field = "Hello"    def doStuff(self, rdd):        field = self.field        return rdd.map(lambda s: field + x)    

如果使用 Java 语言,则需要用到匿名内部类:

   class GetLength implements Function<String, Integer> {  public Integer call(String s) { return s.length(); }}class Sum implements Function2<Integer, Integer, Integer> {  public Integer call(Integer a, Integer b) { return a + b; }}JavaRDD<String> lines = sc.textFile("data.txt");JavaRDD<Integer> lineLengths = lines.map(new GetLength());int totalLength = lineLengths.reduce(new Sum());

Spark 也支持键值对的操作,这在分组和聚合操作时候用得到。定义一个键值对对象时,需要自定义该对象的 equals() 和 hashCode() 方法。

在 Scala 中有一个 Tuple2 对象表示键值对,这是一个内置的对象,通过 (a,b) 就可以创建一个 Tuple2 对象。在你的程序中,通过导入 org.apache.spark.SparkContext._ 就可以对 Tuple2 进行操作。对键值对的操作方法,可以查看 PairRDDFunctions

下面是一个用 scala 统计单词出现次数的例子:

   val lines = sc.textFile("data.txt")val pairs = lines.map(s => (s, 1))val counts = pairs.reduceByKey((a, b) => a + b)

接下来,你还可以执行 counts.sortByKey()、 counts.collect() 等操作。

如果用 Java 统计,则代码如下:

   JavaRDD<String> lines = sc.textFile("data.txt");JavaPairRDD<String, Integer> pairs = lines.mapToPair(s -> new Tuple2(s, 1));JavaPairRDD<String, Integer> counts = pairs.reduceByKey((a, b) -> a + b);

用 Python 统计,代码如下:

   lines = sc.textFile("data.txt")pairs = lines.map(lambda s: (s, 1))counts = pairs.reduceByKey(lambda a, b: a + b)

测试

现在来结合上面的例子实现一个完整的例子。下面,我们来 分析 Nginx 日志中状态码出现次数,并且将结果按照状态码从小到大排序。

先将测试数据上传到 hdfs:

   $ hadoop fs -put access.log

然后,编写一个 python 文件,保存为 SimpleApp.py:

   from pyspark import SparkContextlogFile = "access.log"sc = SparkContext("local", "Simple App")logData = sc.textFile(logFile).cache()counts = logData.map(lambda line: line.split()[8]).map(lambda word: (word, 1)).reduceByKey(lambda a, b: a + b).sortByKey(lambda x: x) # This is just a demo on how to bring all the sorted data back to a single node.  # In reality, we wouldn't want to collect all the data to the driver node.output = counts.collect()  for (word, count) in output:      print "%s: %i" % (word, count)  counts.saveAsTextFile("spark_results")sc.stop()

接下来,运行下面代码:

   $ spark-submit  --master local[4]   SimpleApp.py

运行成功之后,你会在终端看到以下输出:

   200: 6827206: 120301: 7304: 10403: 38404: 125416: 1

并且,在hdfs 上 /user/spark/spark_results/part-00000 内容如下:

   (u'200', 6827)(u'206', 120)(u'301', 7)(u'304', 10)(u'403', 38)(u'404', 125)(u'416', 1)

其实,这个例子和官方提供的例子很相像,具体请看 wordcount.py。

常见的转换

转换含义map(func)返回一个新分布式数据集,由每一个输入元素经过func函数转换后组成filter(func)返回一个新数据集,由经过func函数计算后返回值为 true 的输入元素组成flatMap(func)类似于 map,但是每一个输入元素可以被映射为0或多个输出元素,因此 func 应该返回一个序列mapPartitions(func)类似于 map,但独立地在 RDD 的每一个分块上运行,因此在类型为 T 的 RDD 上运行时,func 的函数类型必须是 Iterator[T] ⇒ Iterator[U]mapPartitionsWithSplit(func)类似于 mapPartitions, 但 func 带有一个整数参数表示分块的索引值。因此在类型为 T的RDD上运行时,func 的函数类型必须是 (Int, Iterator[T]) ⇒ Iterator[U]sample(withReplacement,fraction, seed)根据 fraction 指定的比例,对数据进行采样,可以选择是否用随机数进行替换,seed 用于指定随机数生成器种子union(otherDataset)返回一个新的数据集,新数据集是由源数据集和参数数据集联合而成distinct([numTasks]))返回一个包含源数据集中所有不重复元素的新数据集groupByKey([numTasks])在一个键值对的数据集上调用,返回一个 (K,Seq[V])对的数据集 。注意:默认情况下,只有8个并行任务来做操作,但是你可以传入一个可选的 numTasks 参数来改变它reduceByKey(func, [numTasks])在一个键值对的数据集上调用时,返回一个键值对的数据集,使用指定的 reduce 函数,将相同 key 的值聚合到一起。类似 groupByKey,reduce 任务个数是可以通过第二个可选参数来配置的sortByKey([ascending], [numTasks])在一个键值对的数据集上调用,K 必须实现 Ordered 接口,返回一个按照 Key 进行排序的键值对数据集。升序或降序由 ascending 布尔参数决定join(otherDataset, [numTasks])在类型为(K,V)和(K,W) 类型的数据集上调用时,返回一个相同key对应的所有元素对在一起的 (K, (V, W)) 数据集cogroup(otherDataset, [numTasks])在类型为(K,V)和(K,W) 的数据集上调用,返回一个 (K, Seq[V], Seq[W]) 元组的数据集。这个操作也可以称之为 groupwithcartesian(otherDataset)笛卡尔积,在类型为 T 和 U 类型的数据集上调用时,返回一个 (T, U) 对数据集(两两的元素对)pipe(command, [envVars])对 RDD 进行管道操作coalesce(numPartitions)减少 RDD 的分区数到指定值。在过滤大量数据之后,可以执行此操作repartition(numPartitions)重新给 RDD 分区repartitionAndSortWithinPartitions(partitioner)重新给 RDD 分区,并且每个分区内以记录的 key 排序

常用的动作

常用的动作列表

动作含义reduce(func)通过函数 func 聚集数据集中的所有元素。这个功能必须可交换且可关联的,从而可以正确的被并行执行。collect()在驱动程序中,以数组的形式,返回数据集的所有元素。这通常会在使用 filter 或者其它操作并返回一个足够小的数据子集后再使用会比较有用。count()返回数据集的元素的个数。first()返回数据集的第一个元素,类似于 take(1)take(n)返回一个由数据集的前 n 个元素组成的数组。注意,这个操作目前并非并行执行,而是由驱动程序计算所有的元素takeSample(withReplacement,num, seed)返回一个数组,在数据集中随机采样 num 个元素组成,可以选择是否用随机数替换不足的部分,seed 用于指定的随机数生成器种子takeOrdered(n, [ordering])返回自然顺序或者自定义顺序的前 n 个元素saveAsTextFile(path)将数据集的元素,以 textfile 的形式,保存到本地文件系统,HDFS或者任何其它 hadoop 支持的文件系统。对于每个元素,Spark 将会调用 toString 方法,将它转换为文件中的文本行saveAsSequenceFile(path) (Java and Scala)将数据集的元素,以 Hadoop sequencefile 的格式保存到指定的目录下saveAsObjectFile(path) (Java and Scala)将数据集的元素,以 Java 序列化的方式保存到指定的目录下countByKey()对(K,V)类型的 RDD 有效,返回一个 (K,Int) 对的 Map,表示每一个key对应的元素个数foreach(func)在数据集的每一个元素上,运行函数 func 进行更新。这通常用于边缘效果,例如更新一个累加器,或者和外部存储系统进行交互,例如HBase

3.4 RDD持久化

Spark 最重要的一个功能,就是在不同操作间,持久化(或缓存)一个数据集在内存中,这将使得后续的动作变得更加迅速。缓存是用 Spark 构建迭代算法的关键。 使用以下两种方法可以标记要缓存的 RDD:

   lineLengths.persist()  lineLengths.cache() 

取消缓存则用:

   lineLengths.unpersist()

每一个RDD都可以用不同的保存级别进行保存,通过将一个 org.apache.spark.storage.StorageLevel 对象传递给 persist(self, storageLevel) 可以控制 RDD 持久化到磁盘、内存或者是跨节点复制等等。 cache() 方法是使用默认存储级别的快捷方法,也就是 StorageLevel.MEMORY_ONLY。 完整的可选存储级别如下:

存储级别意义MEMORY_ONLY默认的级别, 将 RDD 作为反序列化的的对象存储在 JVM 中。如果不能被内存装下,一些分区将不会被缓存,并且在需要的时候被重新计算MEMORY_AND_DISK将 RDD 作为反序列化的的对象存储在 JVM 中。如果不能被与内存装下,超出的分区将被保存在硬盘上,并且在需要时被读取MEMORY_ONLY_SER将 RDD 作为序列化的的对象进行存储(每一分区占用一个字节数组)。通常来说,这比将对象反序列化的空间利用率更高,尤其当使用fast serializer,但在读取时会比较占用CPUMEMORY_AND_DISK_SER与 MEMORY_ONLY_SER 相似,但是把超出内存的分区将存储在硬盘上而不是在每次需要的时候重新计算DISK_ONLY只将 RDD 分区存储在硬盘上MEMORY_ONLY_2MEMORY_AND_DISK_2等与上述的存储级别一样,但是将每一个分区都复制到两个集群结点上OFF_HEAP开发中

Spark 的不同存储级别,旨在满足内存使用和 CPU 效率权衡上的不同需求。我们建议通过以下的步骤来进行选择:

  • 如果你的 RDD 可以很好的与默认的存储级别契合,就不需要做任何修改了。这已经是 CPU 使用效率最高的选项,它使得 RDD的操作尽可能的快。
  • 如果不行,试着使用 MEMORY_ONLY_SER 并且选择一个快速序列化的库使得对象在有比较高的空间使用率的情况下,依然可以较快被访问。
  • 尽可能不要存储到硬盘上,除非计算数据集的函数,计算量特别大,或者它们过滤了大量的数据。否则,重新计算一个分区的速度,和与从硬盘中读取基本差不多快。
  • 如果你想有快速故障恢复能力,使用复制存储级别。例如:用 Spark 来响应web应用的请求。所有的存储级别都有通过重新计算丢失数据恢复错误的容错机制,但是复制存储级别可以让你在 RDD 上持续的运行任务,而不需要等待丢失的分区被重新计算。
  • 如果你想要定义你自己的存储级别,比如复制因子为3而不是2,可以使用 StorageLevel 单例对象的 apply()方法。
0 0
原创粉丝点击