4. RDD的操作
4.1 基本操作
RDD有2种类型的操作,一种是转换transformations,它基于一个存在的数据集创建出一个新的数据集;另一种是行动actions,它通过对一个存在的数据集进行运算得出结果。例如,map方法是转换操作,它将数据集的每一个元素按指定的函数转换为一个新的RDD;reduce方法是行动操作,它将数据集的所有元素按指定的函数进行聚合运算得出结果给驱动节点。Spark的所有转换操作都是延时加载执行的,当需要执行行动操作返回结果时才会加载执行,这种设计让Spark的运算更加高效。例如:
运行上述代码后控制台是没有输出的,因为map方法实际是没有执行的。如果结合reduce使用的话就会被执行,例如以下例子,控制台输出1 2 3 4 5。
默认地,每个转换的RDD在执行action操作时都会重新计算,不过可以使用persist或cache方法把RDD持久化到内存以便下次使用而不需要重新运算创建;Spark也支持将RDD持久化到磁盘,或在多个节点上复制。例如下面代码把map的结果持久化到内存:
4.2 传递方法
Spark的API在很大程度上依赖于把驱动节点中的方法传递到集群上运行,那些方法都是通过实现了定义在org.apache.spark.api.java.function包里面接口的类来表示,有2种创建该方法的方式:
- 使用匿名内部类或命名内部类实现该方法接口
- 使用Java 8的lambda表达式定义实现
在上述的3.1代码里面也有相关的例子,又例如下面的2个例子,分别使用了匿名内部类和实体类:
- 匿名内部类方式:
- 命名内部类方式:
5. 理解闭包
理解程序在集群中执行时变量和方法的使用范围和生命周期是Spark的其中一个难点。在变量使用范围之外修改变量的RDD操作是一个经常引起困惑的原因。例如下面的例子,使用了foreach()方法实现一个计数器,根据程序是否在同一个JVM中执行会有不同的结果(类似的问题在其它的RDD操作中也会出现),例如当程序运行在Spark本地模式(--master = local[n])和集群模式(例如通过spark-submit提交任务到YARN):
5.1 本地模式 VS 集群模式
在集群模式下,Spark执行jobs的时候会把处理的RDD操作分割为多个任务,每个任务被一个执行器executor处理,在执行之前,Spark首先会计算任务的闭包。闭包是必须对executor可见的变量和方法,用于对RDD执行运算(上例的foreach())。闭包会被序列化和发送给每个executor,被发送给每个executor的闭包内的变量是原来的副本。因此,当在foreach方法内引用的计数器已经不是原来驱动节点的计数器了。在驱动节点的内存里面虽然仍有一个计数器,但它对其它的executors都是不可见的!executors只能访问对应序列化闭包中的计数器副本。因此,最终的计数器值仍然是0,因为所有对计数器的操作都是对对应序列化闭包中的计数器副本执行的。
在本地模式下,一般情况foreach方法和驱动节点是在同一个JVM执行,因此操作的是同一个计数器,会得出正确的运算结果。
在类似的场景下,为了确保得出正确的结果应该使用累加器Accumulator。Spark中的累加器专门为在集群中的多个节点间更新同一变量提供了一种安全的机制。在后续的指南里面会对累加器做进一步的详细介绍。一般来说,像循环或本地定义方法这样的闭包结构,不应该用于更改全局状态。
5.2 打印RDD的元素
另外一种常用的操作就是使用rdd.foreach(x -> System.out.println(x))或rdd.map(x -> System.out.println(x))打印一个RDD的所有元素。在单机模式下,该方法会输出期待的打印对应RDD的所有元素结果。然而在集群模式下,输出的结果是在对应executor的stdout,而不是在驱动节点,所以在驱动节点的stdout是看不见输出的。如果想把RDD的所有元素打印到驱动节点上,可以使用collect()方法:
但这样会导致驱动节点的内存不足,因为collect()方法会把整个RDD的数据都传送到驱动节点上;如果只需要打印RDD的少量元素,可以使用较安全的take()方法获取前X个元素,例如下面例子获取RDD的前100个元素到驱动节点并打印出来:
6. 键值对的操作
大部分Spark操作的RDD都是包含任意类型的对象,也有少量特殊的仅支持含有键值对的RDD操作。最常用的一个操作是分布式“移动(shuffle)”操作,例如按照key将RDD的元素进行分组或聚合操作。
在Java中,键值对使用的是Scala标准库里面的scala.Tuple2类,可以通过调用new Tuple2(a, b)创建,然后通过tuple._1()和tuple._2()方法访问它的属性。键值对RDD使用的是JavaPairRDD类,可以使用特定的map操作将JavaRDDs转换为JavaPairRDDs,例如mapToPair和flatMapToPair。JavaPairRDD拥有标准RDD和特殊键值对的方法,例如,在下面的代码中使用了reduceByKey对键值对操作,计算每行的文本出现的次数:
我们还可以使用counts.sortByKey()按照字母顺序将键值对排序,然后使用counts.collect()将结果以一个数组的形式发送给驱动节点。需要注意的是,当在键值对操作中使用自定义对象作为key时,必须确保自定义的equals()方法有对应的hashCode()方法。
TO BE CONTINUED...O(∩_∩)O
4.1 基本操作
RDD有2种类型的操作,一种是转换transformations,它基于一个存在的数据集创建出一个新的数据集;另一种是行动actions,它通过对一个存在的数据集进行运算得出结果。例如,map方法是转换操作,它将数据集的每一个元素按指定的函数转换为一个新的RDD;reduce方法是行动操作,它将数据集的所有元素按指定的函数进行聚合运算得出结果给驱动节点。Spark的所有转换操作都是延时加载执行的,当需要执行行动操作返回结果时才会加载执行,这种设计让Spark的运算更加高效。例如:
运行上述代码后控制台是没有输出的,因为map方法实际是没有执行的。如果结合reduce使用的话就会被执行,例如以下例子,控制台输出1 2 3 4 5。
默认地,每个转换的RDD在执行action操作时都会重新计算,不过可以使用persist或cache方法把RDD持久化到内存以便下次使用而不需要重新运算创建;Spark也支持将RDD持久化到磁盘,或在多个节点上复制。例如下面代码把map的结果持久化到内存:
4.2 传递方法
Spark的API在很大程度上依赖于把驱动节点中的方法传递到集群上运行,那些方法都是通过实现了定义在org.apache.spark.api.java.function包里面接口的类来表示,有2种创建该方法的方式:
- 使用匿名内部类或命名内部类实现该方法接口
- 使用Java 8的lambda表达式定义实现
在上述的3.1代码里面也有相关的例子,又例如下面的2个例子,分别使用了匿名内部类和实体类:
- 匿名内部类方式:
- 命名内部类方式:
5. 理解闭包
理解程序在集群中执行时变量和方法的使用范围和生命周期是Spark的其中一个难点。在变量使用范围之外修改变量的RDD操作是一个经常引起困惑的原因。例如下面的例子,使用了foreach()方法实现一个计数器,根据程序是否在同一个JVM中执行会有不同的结果(类似的问题在其它的RDD操作中也会出现),例如当程序运行在Spark本地模式(--master = local[n])和集群模式(例如通过spark-submit提交任务到YARN):
5.1 本地模式 VS 集群模式
在集群模式下,Spark执行jobs的时候会把处理的RDD操作分割为多个任务,每个任务被一个执行器executor处理,在执行之前,Spark首先会计算任务的闭包。闭包是必须对executor可见的变量和方法,用于对RDD执行运算(上例的foreach())。闭包会被序列化和发送给每个executor,被发送给每个executor的闭包内的变量是原来的副本。因此,当在foreach方法内引用的计数器已经不是原来驱动节点的计数器了。在驱动节点的内存里面虽然仍有一个计数器,但它对其它的executors都是不可见的!executors只能访问对应序列化闭包中的计数器副本。因此,最终的计数器值仍然是0,因为所有对计数器的操作都是对对应序列化闭包中的计数器副本执行的。
在本地模式下,一般情况foreach方法和驱动节点是在同一个JVM执行,因此操作的是同一个计数器,会得出正确的运算结果。
在类似的场景下,为了确保得出正确的结果应该使用累加器Accumulator。Spark中的累加器专门为在集群中的多个节点间更新同一变量提供了一种安全的机制。在后续的指南里面会对累加器做进一步的详细介绍。一般来说,像循环或本地定义方法这样的闭包结构,不应该用于更改全局状态。
5.2 打印RDD的元素
另外一种常用的操作就是使用rdd.foreach(x -> System.out.println(x))或rdd.map(x -> System.out.println(x))打印一个RDD的所有元素。在单机模式下,该方法会输出期待的打印对应RDD的所有元素结果。然而在集群模式下,输出的结果是在对应executor的stdout,而不是在驱动节点,所以在驱动节点的stdout是看不见输出的。如果想把RDD的所有元素打印到驱动节点上,可以使用collect()方法:
但这样会导致驱动节点的内存不足,因为collect()方法会把整个RDD的数据都传送到驱动节点上;如果只需要打印RDD的少量元素,可以使用较安全的take()方法获取前X个元素,例如下面例子获取RDD的前100个元素到驱动节点并打印出来:
6. 键值对的操作
大部分Spark操作的RDD都是包含任意类型的对象,也有少量特殊的仅支持含有键值对的RDD操作。最常用的一个操作是分布式“移动(shuffle)”操作,例如按照key将RDD的元素进行分组或聚合操作。
在Java中,键值对使用的是Scala标准库里面的scala.Tuple2类,可以通过调用new Tuple2(a, b)创建,然后通过tuple._1()和tuple._2()方法访问它的属性。键值对RDD使用的是JavaPairRDD类,可以使用特定的map操作将JavaRDDs转换为JavaPairRDDs,例如mapToPair和flatMapToPair。JavaPairRDD拥有标准RDD和特殊键值对的方法,例如,在下面的代码中使用了reduceByKey对键值对操作,计算每行的文本出现的次数:
我们还可以使用counts.sortByKey()按照字母顺序将键值对排序,然后使用counts.collect()将结果以一个数组的形式发送给驱动节点。需要注意的是,当在键值对操作中使用自定义对象作为key时,必须确保自定义的equals()方法有对应的hashCode()方法。
TO BE CONTINUED...O(∩_∩)O