目录

  • 1. 什么是Spark的并行度 、什么是Rdd的分区?
  • 2. 看示例
  • 3. 看源码
  • 4. 记结论
  • 5.  org.apache.hadoop.mapred.FileInputFormat 之 getSplits
  • 6. getSplits(JobContext job) Vs getSplits(JobConf job, int numSplits)
  • 7. org.apache.hadoop.mapreduce.lib.input.FileInputFormat 之 getSplits

1. 什么是Spark的并行度 、什么是Rdd的分区?

1. 什么是Spark的并行度 ?
    Driver 将任务进行切分成不同的Task, 再发送给 Executor 节点并行计算,并行计算的任务数量 我们称之为 并行度

2. 什么是Rdd的分区 ?
  1. 将要操作的数据分成 若干份,以便 分布式计算

2. 看示例

package pak_partition {

  import org.apache.spark.{SparkConf, SparkContext}
  import org.apache.spark.rdd.RDD

  //1. ParallelCollectionRDD 分区策略
  object MemoryBypartion extends App {
    val sparkconf: SparkConf = new SparkConf().setMaster("local").setAppName("MemoryBypartion")
    // 设置 分区个数
    sparkconf.set("spark.default.parallelism", "2")

    val sc: SparkContext = new SparkContext(sparkconf)

    /*
    *  定义 :
    *     def parallelize[T](seq: Seq[T], numSlices: Int)(evidence$1: ClassTag[T]): RDD[T]
    *  参数 :
    *     numSlices : 指定切片数量
    *                 不指定时, 使用默认设置值 defaultParallelism
    *                    override def defaultParallelism(): Int =
    *                          scheduler.conf.getInt("spark.default.parallelism", totalCores)
    * */
    val list_rdd: RDD[Int] = sc.parallelize(
      List(1, 2, 3, 4)
    )

    // 查看分区数
    println(s"当前分区数: ${list_rdd.getNumPartitions}")

    list_rdd.saveAsTextFile("Spark_319/src/output/01")

    //关闭资源
    sc.stop()
  }

  //2. File Rdd
  object FileBypartion extends App {
    val sparkconf: SparkConf = new SparkConf().setMaster("local").setAppName("initRddByLocalFile")
    //sparkconf.set("spark.default.parallelism", "13")
    val sc: SparkContext = new SparkContext(sparkconf)

    //    private val str: String = sparkconf.get("spark.default.parallelism")
    //    println(s"默认分区数:${str}")

    /*
    * 定义 :
    *     def textFile(path: String, minPartitions: Int): RDD[String]
    * 参数 :
    *     minPartitions : 不指定时,使用默认参数(使用默认参数时,切片个数最大为2)
    *     def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
    * */
    private val text_rdd: RDD[String] = sc.textFile("Spark_319/src/data", 5)

    // 查看分区数
    println(s"当前分区数: ${text_rdd.getNumPartitions}")

    text_rdd.saveAsTextFile("Spark_319/src/output/02")

    //关闭资源
    sc.stop()
  }


}

3. 看源码

1. ParallelCollectionRDD

创建 ParallelCollectionRDD

-- 创建rdd
def parallelize[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
assertNotStopped()
new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
}

-- 读取 分区个数的默认值
-- 通过 sparkconf.set("spark.default.parallelism","2") 来设置
override def defaultParallelism(): Int =
scheduler.conf.getInt("spark.default.parallelism", totalCores)

-- 切片规则
-- 根据 集合索引 和 切片个数 来进行切片
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
(0 until numSlices).iterator.map { i =>
val start = ((i * length) / numSlices).toInt
val end = (((i + 1) * length) / numSlices).toInt
(start, end)
}
}

2. file rdd 

-- 创建rdd
-- 不指定 minPartitions时,使用默认值
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}

-- 获取 切片个数默认值
override def defaultParallelism(): Int =
scheduler.conf.getInt("spark.default.parallelism", totalCores)
 
-- 使用默认参数时,切片个数 最大为2
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)

-- 切片规则
见下文 org.apache.hadoop.mapred.FileInputFormat
           public InputSplit[] getSplits(JobConf job, int numSplits)

4. 记结论

1. ParallelCollectionRDD
  1. 切片个数
      Rdd 创建时 可以指定切片个数,不指定时,将使用 默认值
      note : 默认切片个数 不会超过2
  2. 切片规则
      根据 集合索引 和 切片个数,将数据切片
      示例 : List(1,2,3,4) 分区数=2 
      切分成 1:(1,2) 2:(3,4)

2. File Rdd 
  1. 切片个数
      Rdd 创建时 可以指定切片个数,不指定时,将使用 默认值
      note : 默认切片个数 不会超过2
             这里指定的是 最小切片数(minPartitions)
                 也就是说  创建的Rdd真实切片个数必定 >= minPartitions
  2. 切片规则
      根据 文件大小 和 切片大小,将数据切片

5.  org.apache.hadoop.mapred.FileInputFormat 之 getSplits

// 将job输入生成一个文件列表
// 并将文件切分成 InputSplit对象,并将 切片对象 存储到list中 
public InputSplit[] getSplits(JobConf job, int numSplits)
  throws IOException {
  StopWatch sw = new StopWatch().start();
  //1. 通过job 读取input目录,将路径中 文件作为元素
  FileStatus[] files = listStatus(job);
  
  // Save the number of input files for metrics/loadgen
  job.setLong(NUM_INPUT_FILES, files.length);
  // 计算 所有文件的长度
  long totalSize = 0;                           // compute total size
  for (FileStatus file: files) {                // check we have valid files
    if (file.isDirectory()) {
      throw new IOException("Not a file: "+ file.getPath());
    }
    totalSize += file.getLen();
  }

  // 计算 目标切片大小 = 所有文件总和 / 指定切片数
  long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
  // 计算 最小切片大小
  // SPLIT_MINSIZE = mapreduce.input.fileinputformat.split.minsize
  // private long minSplitSize = 1;
  // 不设置 SPLIT_MINSIZE时,默认为1
  long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
    FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);

  // 创建 List<InputSplit>,用来存储 切片结果
  ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
  NetworkTopology clusterMap = new NetworkTopology();

  // 遍历文件列表,对每个文件进行切片
  for (FileStatus file: files) {
    // 1. 获取文件路径
    Path path = file.getPath();
    // 2. 获取文件长度
    long length = file.getLen();
    // 处理非空文件
    if (length != 0) {
      FileSystem fs = path.getFileSystem(job);
      // 3. 获取文件 block信息
      BlockLocation[] blkLocations;
      if (file instanceof LocatedFileStatus) {
        blkLocations = ((LocatedFileStatus) file).getBlockLocations();
      } else {
        blkLocations = fs.getFileBlockLocations(file, 0, length);
      }
      // 4. 判断文件 是否可以 切片
      if (isSplitable(fs, path)) {
        // 文件支持 切片
        // 5. 获取文件块的大小,本地运行环境 32M
        long blockSize = file.getBlockSize();
        // 6. 获取切片大小
        long splitSize = computeSplitSize(goalSize, minSize, blockSize);
                //计算规则
                protected long computeSplitSize(long goalSize, long minSize,long blockSize) {
                   return Math.max(minSize, Math.min(goalSize, blockSize));
                }

        // 7. 设置临时变量 存储 剩余文件长度
        long bytesRemaining = length;

        // 8. 对文件 循环切片
        // SPLIT_SLOP = 1.1;   // 10% slop 允许切片大小有 10%的溢出
        // 这样做的好处是 : 能保证 一个文件的某个切片 为切片大小的1.1倍,避免了 过小切片的出现
        while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
          //
          String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,
              length-bytesRemaining, splitSize, clusterMap);
            //10. 初始化切片对象,添加到  List<InputSplit>
                  // path :  文件路径
                  // length-bytesRemaining : 切片在文件中 起始位置
                  // splitSize : 切片大小
                  // splitHosts : 切片在集群上的位置信息
          splits.add(makeSplit(path, length-bytesRemaining, splitSize,
              splitHosts[0], splitHosts[1]));

          // 剩余文件长度 减去一个 切片长度
          bytesRemaining -= splitSize;
        }

        //11. 当文件剩余长度 小于切片长度*1.1 时,将文件剩余部分 作为一个切片
        if (bytesRemaining != 0) {
          // 获取切片对象属性,并往 List<InputSplit> 添加一个切片
          String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
              - bytesRemaining, bytesRemaining, clusterMap);
          splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
              splitHosts[0], splitHosts[1]));
        }
      } else {
        // 当文件不可切片时,将整个文件 作为一个切片进行存储
        String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap);
        splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1]));
      }
    } else { 
      //存储空文件
      splits.add(makeSplit(path, 0, length, new String[0]));
    }
  }


  sw.stop();
  if (LOG.isDebugEnabled()) {
    LOG.debug("Total # of splits generated by getSplits: " + splits.size()
        + ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
  }
  // 6. 返回 存储切片对象的 集合
  return splits.toArray(new FileSplit[splits.size()]);
}

总结说明 : 

总结说明 : 
org.apache.hadoop.mapred.FileInputFormat
    public InputSplit[] getSplits(JobConf job, int numSplits)

1. numSplits = 指定切片个数
      也就是 创建Rdd时,指定的最小切片数(minPartitions)
                     创建的Rdd真实切片个数必定 >= minPartitions

2. 计算 目标切片大小
      long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits)
      goalSize = sc.textFile() 读取的文件总和 / minPartitions
      目标切片大小 = 所有文件总和 / 指定切片数 

3. 计算 切片的最小值
      long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
        FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
      SPLIT_MINSIZE = mapreduce.input.fileinputformat.split.minsize
      不设置时,默认为1

4. 计算 切片的真实大小
      long splitSize = Math.max(minSize, Math.min(goalSize, blockSize));
      // 能保证切片大小 不大于 blockSize(数据本地化)
      // 设置 minSize, 可以将 切片大小调整 大于blockSize

5. 遍历文件列表,对列表内文件 循环切片(每个文件单独切片)
      for (FileStatus file: files) 
      note : while (((double) bytesRemaining)/splitSize > SPLIT_SLOP)  SPLIT_SLOP = 1.1
            // 允许实际切片大小 为 切片的1.1倍
            // 这样做的好处是 避免了 过小切片的出现
            // 示例 : 1.txt = 93M 切片大小为 30M
            //             split1 = 30M
            //             split2 = 30M
            //             split3 = 33M  (33M/30M=1.1)

6. 返回 切片对象的数组
      splits.toArray(new FileSplit[splits.size()])
      // 成功将 输入的文件,切分成了大小相等的切片

6. getSplits(JobContext job) Vs getSplits(JobConf job, int numSplits)

-- org.apache.hadoop.mapreduce.lib.input.FileInputFormat-getSplits(JobContext job)
1. 计算 切片最小值
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
       getFormatMinSplitSize() return 1;
       getMinSplitSize(job) return job.getConfiguration().getLong(SPLIT_MINSIZE, 1L);
            SPLIT_MINSIZE = mapreduce.input.fileinputformat.split.minsize
       note1 : minSize 取 mapreduce.input.fileinputformat.split.minsize 设置值
                          没设置时,取 1

2. 计算 切片最大值
long maxSize = getMaxSplitSize(job);
      getMaxSplitSize return context.getConfiguration().getLong(SPLIT_MAXSIZE, Long.MAX_VALUE);
      SPLIT_MAXSIZE = mapreduce.input.fileinputformat.split.maxsize
      note1 : maxSize 取 mapreduce.input.fileinputformat.split.maxsize 设置值
                          没设置时,取 Long.MAX_VALUE      

3. 获取切片大小
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
              computeSplitSize =  return Math.max(minSize, Math.min(maxSize, blockSize));
              note1 : 
                   不设置  mapreduce.input.fileinputformat.split.minsize 
                          mapreduce.input.fileinputformat.split.maxsize

                  computeSplitSize = blockSize
                            minsize = 1 、maxsize = Long.MAX_VALUE  

说明 :
    1. Spark 对文件的切片使用的是 org.apache.hadoop.mapred.FileInputFormat下的 getSplits(JobConf job, int numSplits) 方法
         而MapReduce 默认用的是  org.apache.hadoop.mapreduce.lib.input.FileInputFormat-getSplits(JobContext job)

    2. 两者的主要区别在于 计算真实切片大小的规则不同
          getSplits(JobConf job, int numSplits)
               根据 long splitSize = Math.max(minSize, Math.min(goalSize, blockSize))

          getSplits(JobContext job)  
               根据 long splitSize = computeSplitSize(blockSize, minSize, maxSize);

7. org.apache.hadoop.mapreduce.lib.input.FileInputFormat 之 getSplits