Spark调优(1)--算子map,flatMap与mapValues,flatMapValues的区别

本文详细探讨了Spark中RDD算子map、flatMap、mapValues及flatMapValues的工作原理,并通过源码解析了这些算子如何影响shuffle操作次数。特别关注了mapValues算子如何通过保留分区特性减少shuffle操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关于 map,flatMap,mapValues,flatMapValues 算子 大家都应该很熟悉,底层都是调用了MapPartitionsRDD 算子,它 的父类是 RDD;

我们看下简单的例子,以下两个任务发生了几次shuffle

// reduceByKey 之后使用 map 对值进行操作,再 groupByKey
data.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).map(x=>(x._1,x._2*10)).groupByKey().foreach(println)
// reduceByKey 之后使用 mapValues 对值进行操作,再 groupByKey 
data.flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_).mapValues(_*10).groupByKey().foreach(println)

如图所示:

  • 使用了map算子 DAG图
    在这里插入图片描述
  • 使用了mapValues之后:
    在这里插入图片描述
    从以上两张图可以看出来,为什么使用了 mapValues 之后 shuffle 仅发生了一次,而不是想想中的2次,
    这其实因为 MapPartitionsRDD 参数 preservesPartitioning 的原因,以及 groupByKey底层函数 combineByKeyWithClassTag的原因 :

解析

  • MapPartitionsRDD 源码:
    从源码可以看出 preservesPartitioning的含义,即是否要使用到父Rdd的分区器
/**
 * An RDD that applies the provided function to every partition of the parent RDD.
 *
 * @param prev the parent RDD.
 * @param f The function used to map a tuple of (TaskContext, partition index, input iterator) to
 *          an output iterator.
 * @param preservesPartitioning Whether the input function preserves the partitioner, which should
 *                              be `false` unless `prev` is a pair RDD and the input function
 *                              doesn't modify the keys.
 * @param isOrderSensitive whether or not the function is order-sensitive. If it's order
 *                         sensitive, it may return totally different result when the input order
 *                         is changed. Mostly stateful functions are order-sensitive.
 */
private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag](
    var prev: RDD[T],
    f: (TaskContext, Int, Iterator[T]) => Iterator[U],  // (TaskContext, partition index, iterator)
    preservesPartitioning: Boolean = false,
    isOrderSensitive: Boolean = false)
  extends RDD[U](prev) {
  
  //判断是否使用 firstParent 的分区器
  
  override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None
  ......
  • map 源码:
    调用了 MapPartitionsRDD 参数preservesPartitioning 为false ,则 无分区器
  /**
   * Return a new RDD by applying a function to all elements of this RDD.
   */
  def map[U: ClassTag](f: T => U): RDD[U] = withScope {
    val cleanF = sc.clean(f)
    new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))
  }
  • mapValues 源码:
    底层 调用 MapPartitionsRDD 传入参数 preservesPartitioning = true
/**
   * Pass each value in the key-value pair RDD through a map function without changing the keys;
   * this also retains the original RDD's partitioning.
   */
  def mapValues[U](f: V => U): RDD[(K, U)] = self.withScope {
    val cleanF = self.context.clean(f)
    new MapPartitionsRDD[(K, U), (K, V)](self,
      (context, pid, iter) => iter.map { case (k, v) => (k, cleanF(v)) },
      preservesPartitioning = true)
  }

好了, 从以上源码我们可以看到在使用 groupByKey 这个算子之前 它的父Rdd的分区器是从哪儿来的,现在我们来看看 groupByKey的源码:

  • groupByKey 源码:
    groupByKey 底层是调用了 combineByKeyWithClassTag 函数,从源码中我们可以看到 如果 self.partitioner == Some(partitioner),那么 将不会调用 ShuffledRDD 发生shuffle ,看到这里就应该明白了 为什么;由于算子使用的分区器都是一样的,那么相同的key值 一定会与之前的 reduceByKey 算子shuffle 时移动, 分区一致,即 1 这个值 不管如何 一定会在1 这个分区,(使用了相同的分区器),既然如此,那么之间简单的进行io操作 ,进行移动即可;
 def combineByKeyWithClassTag[C](
      createCombiner: V => C,
      mergeValue: (C, V) => C,
      mergeCombiners: (C, C) => C,
      partitioner: Partitioner,
      mapSideCombine: Boolean = true,
      serializer: Serializer = null)(implicit ct: ClassTag[C]): RDD[(K, C)] = self.withScope {
    require(mergeCombiners != null, "mergeCombiners must be defined") // required as of Spark 0.9.0
    if (keyClass.isArray) {
      if (mapSideCombine) {
        throw new SparkException("Cannot use map-side combining with array keys.")
      }
      if (partitioner.isInstanceOf[HashPartitioner]) {
        throw new SparkException("HashPartitioner cannot partition array keys.")
      }
    }
    val aggregator = new Aggregator[K, V, C](
      self.context.clean(createCombiner),
      self.context.clean(mergeValue),
      self.context.clean(mergeCombiners))
      //判断是否要调用  ShuffledRDD 还是直接 进行io数据移动
    if (self.partitioner == Some(partitioner)) {
      self.mapPartitions(iter => {
        val context = TaskContext.get()
        new InterruptibleIterator(context, aggregator.combineValuesByKey(iter, context))
      }, preservesPartitioning = true)
    } else {
      new ShuffledRDD[K, V, C](self, partitioner)
        .setSerializer(serializer)
        .setAggregator(aggregator)
        .setMapSideCombine(mapSideCombine)
    }
  }

在这里插入图片描述

在 IDEA 中实现 Spark 相关任务(包括 RDD 基本算子实验 WordCount 综合实验),需要先设置好开发环境,再基于具体的业务需求编写代码。下面详细介绍如何实现实验内容: ### 第一部分:掌握 RDD 算子的使用 #### 实现步骤 1. **准备环境** - 创建一个 Maven 工程。 - 修改 `pom.xml` 添加 Spark 依赖: ```xml <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.12</artifactId> <version>3.0.1</version> </dependency> ``` 2. **初始化 SparkSession SparkContext** 这是所有 Spark 操作的基础。 ```java import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; public class RddOperatorDemo { private static final String SPARK_MASTER_URL = "local"; public static void main(String[] args) { // 初始化 Spark Context SparkConf conf = new SparkConf().setAppName("RddOperator").setMaster(SPARK_MASTER_URL); JavaSparkContext sc = new JavaSparkContext(conf); rddOperators(sc); // 算子练习函数 sc.close(); // 关闭上下文 } /** * 测试各种 RDD 转换算子行动算子的功能 */ private static void rddOperators(JavaSparkContext sc) { List<String> wordsList = Arrays.asList("hello", "world", "hello spark", "hadoop"); // 生成初始 RDD JavaRDD<String> initialRDD = sc.parallelize(wordsList); // map 示例:将每个单词转成大写形式 System.out.println("--- Map Operator ---"); JavaRDD<String> mappedRDD = initialRDD.map(s -> s.toUpperCase()); mappedRDD.collect().forEach(System.out::println); // distinct 示例:去重操作 System.out.println("\n--- Distinct Operator ---"); JavaRDD<String> distinctRDD = initialRDD.distinct(); distinctRDD.collect().forEach(System.out::println); // flatMap 示例:对字符串按空格拆分出单个词语集合 System.out.println("\n--- FlatMap Operator ---"); JavaRDD<String> flattenWordsRDD = initialRDD.flatMap(s -> Arrays.asList(s.split(" ")).iterator()); flattenWordsRDD.collect().forEach(System.out::println); // filter 示例:过滤掉长度小于等于4的元素 System.out.println("\n--- Filter Operator ---"); JavaRDD<String> filteredRDD = flattenWordsRDD.filter(word -> word.length() > 4); filteredRDD.collect().forEach(System.out::println); // reduceByKey & mapValues 示例:词频统计前处理阶段 System.out.println("\n--- ReduceByKey and MapValues Operators ---"); JavaPairRDD<String, Integer> pairRDD = flattenWordsRDD.mapToPair(word -> new Tuple2<>(word, 1)); JavaPairRDD<String, Integer> reducedRDD = pairRDD.reduceByKey((v1, v2) -> v1 + v2); reducedRDD.collect().forEach(tuple -> System.out.println(tuple._1 + ": " + tuple._2)); // groupByKey 示例:按照 key 分组值列表 System.out.println("\n--- GroupByKey Operator ---"); JavaPairRDD<String, Iterable<Integer>> groupedRDD = pairRDD.groupByKey(); groupedRDD.collect().forEach(entry -> System.out.printf("%s => %s\n", entry._1(), Lists.newArrayList(entry._2()))); // sortByKey 示例:按键排序键值对 System.out.println("\n--- SortByKey Operator ---"); JavaPairRDD<Integer, String> invertedPairsRDD = flattenedWordsRDD.mapToPair(word -> new Tuple2<>(word.hashCode(), word)); invertedPairsRDD.sortByKey(true).collect().forEach(pair -> System.out.println(pair._2())); } } ``` --- ### 第二部分:Spark 算子综合实验 —— WordCount (词频统计) #### 实现步骤 1. 同样在上面基础上继续新增一段针对 WordCount 的完整代码逻辑。 ```java public static void runWordCountExample(JavaSparkContext sc){ String inputFilePath = "/path/to/input/text/file"; // 替换为实际输入路径 JavaRDD<String> linesRDD = sc.textFile(inputFilePath); // 加载文本数据作为行级 RDD // 单词分割、映射到(key,value) JavaRDD<String> allWords = linesRDD.flatMap(line -> Arrays.asList(line.toLowerCase().split("\\W+")).iterator()); JavaPairRDD<String,Integer> pairs = allWords.mapToPair(word->new Tuple2<>(word,1)); // 计算每种词汇的数量总计 JavaPairRDD<String,Integer> counts = pairs.reduceByKey(Integer::sum); // 输出最终结果集 counts.foreach(tup->{System.out.println(tup._1()+" : "+tup._2());}); } ``` 2. 最后记得修改主方法用新加入的任务入口: ``` public static void main(String[] args) { ... runWordCountExample(sc); ...} ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值