1.概述
两个抽象:RDD和共享变量
- RDD
RDD是跨集群节点分区的元素集合.
- 可以并行操作
- 可以持久化到内存中
- 会自动从节点故障中恢复
- 共享变量
广播变量,可用于在所有节点的内存中缓存值,
累加器,它们是仅“添加”到的变量,例如计数器和总和。
2.RDD
2.1.创建
创建 RDD 有两种方法:并行化 驱动程序中的现有集合,或引用外部存储系统中的数据集。
- 并行集合
List<Integer> data = Arrays.asList(1, 2, 3, 4, 5);
JavaRDD<Integer> distData = sc.parallelize(data, 2);
- 外部数据集
JavaRDD<String> distFile = sc.textFile("data.txt");
2.2.操作
分为两类:转换(从现有数据集创建新数据集)和行动(在对数据集运行计算后将值返回给驱动程序)。
Spark 中的所有转换都是惰性的,
默认情况下,每个转换后的 RDD 可能会在您每次对其运行操作时重新计算。但是,可以使用persist(或者 cache) 方法将 RDD持久化。
- 简单示例
JavaRDD<String> lines = sc.textFile("data.txt");
JavaRDD<Integer> lineLengths = lines.map(s -> s.length());
int totalLength = lineLengths.reduce((a, b) -> a + b);
lineLengths.persist(StorageLevel.MEMORY_ONLY());
- 第一行定义了来自外部文件的基本 RDD。此数据集未加载到内存中或以其他方式执行:lines仅是指向文件的指针。
- 第二行定义lineLengths为map转换的结果。同样,由于懒惰,不会立即lineLengths 计算。
- 最后,我们运行reduce,这是一个动作。
- 这将导致lineLengths在第一次计算后保存在内存中。
- 将函数传递给 Spark
Spark 的 API 在很大程度上依赖于在驱动程序中传递函数来在集群上运行。
有两种方法可以创建此类函数:
- 在您自己的类中实现 Function 接口,可以作为匿名内部类或命名的内部类,并将其实例传递给 Spark。
- 使用lambda 表达式 来简洁地定义一个实现。
- 了解闭包
关于 Spark 的难点之一是在跨集群执行代码时了解变量和方法的范围和生命周期。
修改其范围之外的变量的 RDD 操作可能是一个常见的混淆源。
int counter = 0;
JavaRDD<Integer> rdd = sc.parallelize(data);
// Wrong: Don't do this!!
rdd.foreach(x -> counter += x);
println("Counter value: " + counter);
- 使用键值对
虽然大多数 Spark 操作适用于包含任何类型对象的 RDD,但少数特殊操作仅适用于键值对的 RDD。最常见的是分布式“shuffle”操作,例如通过键对元素进行分组或聚合。
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);
- 常见转换
转型 | 意义 |
map(func) | 通过函数func传递源的每个元素,返回一个新的分布式数据集。 |
filter(func) | 返回通过选择func返回 true的源元素形成的新数据集。 |
flatMap(func) | 类似于 map,但每个输入项可以映射到 0 个或更多输出项(因此func应该返回一个 Seq 而不是单个项)。 |
mapPartitions(func) | 与map类似,但在RDD的每个分区(块)上单独运行,所以func在T类型的RDD上运行时必须是Iterator => Iterator类型。 |
mapPartitionsWithIndex(func) | 与 mapPartitions 类似,但也为func提供了一个表示分区索引的整数值,因此在 T 类型的 RDD 上运行时, func必须是 (Int, Iterator) => Iterator 类型。 |
sample(withReplacement, fraction, seed) | 使用给定的随机数生成器种子对数据的一小部分进行采样,无论是否替换。 |
union(otherDataset) | 返回一个新数据集,其中包含源数据集中元素和参数的并集。 |
intersection(otherDataset) | 返回一个新的 RDD,其中包含源数据集中元素和参数的交集。 |
distinct([numPartitions]) | 返回一个包含源数据集不同元素的新数据集。 |
groupByKey([numPartitions]) | 在 (K, V) 对的数据集上调用时,返回 (K, Iterable) 对的数据集。注意:如果您正在分组以便对每个键执行聚合(例如求和或平均),则使用 reduceByKey 或 aggregateByKey 将产生更好的性能。注意:默认情况下,输出中的并行度取决于父 RDD 的分区数。您可以传递一个可选numPartitions参数来设置不同数量的任务。 |
reduceByKey(func, [numPartitions]) | 在 (K, V) 对的数据集上调用时,返回 (K, V) 对的数据集,其中每个键的值使用给定的 reduce 函数func聚合,该函数必须是 (V,V) => V. 与中一样groupByKey,reduce 任务的数量可以通过可选的第二个参数进行配置。 |
aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions]) | 当在 (K, V) 对的数据集上调用时,返回 (K, U) 对的数据集,其中每个键的值使用给定的组合函数和中性“零”值聚合。允许与输入值类型不同的聚合值类型,同时避免不必要的分配。与 groupByKey 一样,任务的数量可以通过可选的第二个参数进行配置。 |
sortByKey([ascending], [numPartitions]) | 当在 K 实现 Ordered 的 (K, V) 对数据集上调用时,返回按布尔ascending参数中指定的键按升序或降序排序的 (K, V) 对数据集。 |
join(otherDataset, [numPartitions]) | 当在 (K, V) 和 (K, W) 类型的数据集上调用时,返回 (K, (V, W)) 对的数据集,其中每个键的所有元素对。leftOuterJoin通过、rightOuterJoin和支持外连接fullOuterJoin。 |
cogroup(otherDataset, [numPartitions]) | 当在 (K, V) 和 (K, W) 类型的数据集上调用时,返回 (K, (Iterable, Iterable)) 元组的数据集。此操作也称为groupWith。 |
cartesian(otherDataset) | 在 T 和 U 类型的数据集上调用时,返回 (T, U) 对(所有元素对)的数据集。 |
pipe(command, [envVars]) | 通过 shell 命令(例如 Perl 或 bash 脚本)对 RDD 的每个分区进行管道传输。RDD 元素被写入进程的标准输入,输出到标准输出的行作为字符串的 RDD 返回。 |
coalesce(numPartitions) | 将 RDD 中的分区数减少到 numPartitions。对于过滤大型数据集后更有效地运行操作很有用。 |
repartition(numPartitions) | 随机重新排列 RDD 中的数据以创建更多或更少的分区并在它们之间进行平衡。这总是对网络上的所有数据进行洗牌。 |
repartitionAndSortWithinPartitions(partitioner) | 根据给定的分区器对 RDD 进行重新分区,并在每个生成的分区中,按记录的键对记录进行排序。这比repartition在每个分区中调用然后排序更有效,因为它可以将排序下推到 shuffle 机器中。 |
- 常见行动
转换 | 意义 |
reduce(func) | 使用函数func聚合数据集的元素(它接受两个参数并返回一个)。该函数应该是可交换的和关联的,以便可以并行正确计算。 |
collect() | 在驱动程序中将数据集的所有元素作为数组返回。这通常在过滤器或其他返回足够小的数据子集的操作之后很有用。 |
count() | 返回数据集中元素的数量。 |
first() | 返回数据集的第一个元素(类似于 take(1))。 |
take(n) | 返回包含数据集前n 个元素的数组。 |
takeSample(withReplacement, num, [seed]) | 返回一个数组,其中包含数据集的num个元素的随机样本,有或没有替换,可选地预先指定一个随机数生成器种子。 |
takeOrdered(n, [ordering]) | 使用自然顺序或自定义比较器返回 RDD 的前n 个元素。 |
saveAsTextFile(path) | 将数据集的元素作为文本文件(或文本文件集)写入本地文件系统、HDFS 或任何其他 Hadoop 支持的文件系统的给定目录中。Spark 将对每个元素调用 toString 以将其转换为文件中的一行文本。 |
saveAsSequenceFile(path)(Java and Scala) | 将数据集的元素作为 Hadoop SequenceFile 写入本地文件系统、HDFS 或任何其他 Hadoop 支持的文件系统中的给定路径中。这在实现 Hadoop 的 Writable 接口的键值对的 RDD 上可用。在 Scala 中,它也可用于可隐式转换为 Writable 的类型(Spark 包括基本类型的转换,如 Int、Double、String 等)。 |
saveAsObjectFile(path)(Java and Scala) | 使用 Java 序列化以简单格式编写数据集的元素,然后可以使用 SparkContext.objectFile()。 |
countByKey() | 仅适用于 (K, V) 类型的 RDD。返回 (K, Int) 对的哈希图以及每个键的计数。 |
foreach(func) | 对数据集的每个元素运行函数func。这通常是针对副作用进行的,例如更新累加器或与外部存储系统交互。注意:修改除了累加器以外的变量foreach()可能会导致未定义的行为。有关更多详细信息,请参阅了解闭包。 |
- 混洗
是 Spark 用于重新分配数据的机制,以便跨分区以不同方式分组。这通常涉及跨执行器和机器复制数据,使洗牌成为一项复杂且成本高昂的操作。
可能导致洗牌的操作包括重新分区操作,如 repartition 和 coalesce,ByKey操作(计数除外),如groupByKey 和 reduceByKey,以及 连接操作,如cogroup 和 join。
Shuffle是一项昂贵的操作,因为它涉及磁盘 I/O、数据序列化和网络 I/O 。 - RDD 持久性
Spark 中最重要的功能之一是跨操作将数据集持久化(或缓存)在内存中。缓存是迭代算法和快速交互使用的关键工具。
每个持久化的 RDD 可以使用不同的存储级别进行存储:
存储级别 | 意义 |
MEMORY_ONLY | 将 RDD 作为反序列化的 Java 对象存储在 JVM 中。如果 RDD 不适合内存,则某些分区将不会被缓存,并且会在每次需要时重新计算。这是默认级别。 |
MEMORY_AND_DISK | 将 RDD 作为反序列化的 Java 对象存储在 JVM 中。如果 RDD 不适合内存,请将不适合的分区存储在磁盘上,并在需要时从那里读取它们。 |
MEMORY_ONLY_SER(Java 和 Scala) | 将RDD 存储为序列化的Java 对象(每个分区一个字节数组)。这通常比反序列化对象更节省空间,尤其是在使用 快速序列化程序时,但读取时更占用 CPU。 |
MEMORY_AND_DISK_SER(Java 和 Scala) | 类似于 MEMORY_ONLY_SER,但将不适合内存的分区溢出到磁盘,而不是在每次需要时动态地重新计算它们。 |
DISK_ONLY | 仅将 RDD 分区存储在磁盘上。 |
MEMORY_ONLY_2、MEMORY_AND_DISK_2 等。 | 与上述级别相同,但在两个集群节点上复制每个分区。 |
OFF_HEAP(实验性) | 类似于 MEMORY_ONLY_SER,但将数据存储在 堆外内存中。这需要启用堆外内存。 |
3.共享变量
两种有限类型的共享变量:广播变量和累加器。
3.1.广播变量
Broadcast<int[]> broadcastVar = sc.broadcast(new int[] {1, 2, 3});
broadcastVar.value();
3.2.累加器
LongAccumulator accum = jsc.sc().longAccumulator();
sc.parallelize(Arrays.asList(1, 2, 3, 4)).foreach(x -> accum.add(x));
accum.value();
可以通过继承AccumulatorV2来创建自己的类型。