【spark2】【源码学习】【分区数】spark读取 本地/可分割/单个 的文件时是如何划分分区

大数据计算中很关键的一个概念就是分布式并行计算,意思就是将一份原始数据切分成若干份,然后分发到多个机器或者单个机器多个虚拟出来的内存容器中同时执行相同的逻辑,先分发(map),然后聚合(reduce)的一个过程。
那么问题是原始文件是怎么切分的呢,在spark读取不同的数据源,切分的逻辑也是不同的。
首先spark是有改变分区的函数的,分别是Coalesce()方法和rePartition()方法,但是这两个方法只对shuffle过程生效,包括参数spark.default.parallelism也只是对shuffle有效,所以spark读取数据的时候分区数是没有固定的一个参数来设定的,会经过比较复杂的逻辑计算得到的。

这次先看下spark读取 【本地】【可分割】【单个】 的文件,注意括起来的词,因为不同的数据源spark的处理是不一样的。

一、spark读取代码

这里直接复用spark源码中的example【JavaWordCount】

public final class JavaWordCount {
   
   
  private static final Pattern SPACE = Pattern.compile(" ");

  public static void main(String[] args) throws Exception {
   
   

    if (args.length < 1) {
   
   
      System.err.println("Usage: JavaWordCount <file>");
      System.exit(1);
    }

    SparkSession spark = SparkSession
      .builder()
      .appName("JavaWordCount")
      .getOrCreate();

    spark.sparkContext().setLogLevel("WARN");

//    JavaRDD<String> lines = spark.read().textFile(args[0]).javaRDD();
    JavaRDD<String> lines = spark.sparkContext().textFile(args[0], 2).toJavaRDD();
    System.out.println("lines.getNumPartitions(): " + lines.getNumPartitions());
    JavaRDD<String> words = lines.flatMap(s -> Arrays.asList(SPACE.split(s)).iterator());

    JavaPairRDD<String, Integer> ones = words.mapToPair(s -> new Tuple2<>(s, 1));

    JavaPairRDD<String, Integer> counts = ones.reduceByKey((i1, i2) -> i1 + i2);

    List<Tuple2<String, Integer>> output = counts.collect();
//    for (Tuple2<?,?> tuple : output) {
   
   
//      System.out.println(tuple._1() + ": " + tuple._2());
//    }
    spark.stop();
  }
}

二、textFile()方法

spark 提供的读取文件api,发现textFile最后是new一个HadoopRDD返回的。

testFile的两个参数:
path:就是读取文件的路径,可以是精确文件名,也可以是一个目录名
minPartitions: 最小分区数,这个不是确定的最后分区数,这个参数在后面计算分区数时用到。

# SparkContext.scala
/**
   * Read a text file from HDFS, a local file system (available on all nodes), or any
   * Hadoop-supported file system URI, and return it as an RDD of Strings.
   * @param path path to the text file on a supported file system
   * @param minPartitions suggested minimum number of partitions for the resulting RDD
   * @return RDD of lines of the text file
   */
def textFile(
      path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
   
   
    print("\n>>>>>> pgx code <<<<<<\n")
    print("defaultParallelism: " + defaultMinPartitions)
    print("\n")
    print("minPartitions: " + minPartitions)
    print("\n>>>>>> pgx code <<<<<<\n")
    assertNotStopped()
    hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
      minPartitions).map(pair => pair._2.toString).setName(path)
  }

/** Get an RDD for a Hadoop file with an arbitrary InputFormat
   *
   * @note Because Hadoop's RecordReader class re-uses the same Writable object for each
   * record, directly caching the returned RDD or directly passing it to an aggregation or shuffle
   * operation will create many references to the same object.
   * If you plan to directly cache, sort, or aggregate Hadoop writable objects, you should first
   * copy them using a `map` function.
   * @param path directory to the input data files, the path can be comma separated paths
   * as a list of inputs
   * @param inputFormatClass storage format of the data to be read
   * @param keyClass `Class` of the key associated with the `inputFormatClass` parameter
   * @param valueClass `Class` of the value associated with the `inputFormatClass` parameter
   * @param minPartitions suggested minimum number of partitions for the resulting RDD
   * @return RDD of tuples of key and corresponding value
   */
def hadoopFile[K, V](
      path: String,
      inputFormatClass: Class[_ <: InputFormat[K, V]],
      keyClass: Class[K],
      valueClass: Class[V],
      minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
   
   
    assertNotStopped()

    // This is a hack to enforce loading hdfs-site.xml.
    // See SPARK-11227 for details.
    FileSystem.getLocal(hadoopConfiguration)

    // A Hadoop configuration can be about 10 KB, which is pretty big, so broadcast it.
    val confBroadcast = broadcast(new SerializableConfiguration(hadoopConfiguration))
    val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
    new HadoopRDD(
      this,
      confBroadcast,
      Some(setInputPathsFunc),
      inputFormatClass,
      keyClass,
      valueClass,
      minPartitions).setName(path)
  }

三、从action追踪代码

因为spark的transform算子不会引发计算,所以我们直接tracked down 最后引发计算的action 算子 collect()
# JavaRDDlike.scala
/**
   * Return an array that contains all of the elements in this RDD.
   *
   * @note this method should only be used if the resulting array is expected to be small, as
   * all the data is loaded into the driver's memory.
   */
  def collect(): JList[T] =
    rdd.collect().toSeq.asJava
collect()方法是调用了SparkContext对象的runJob方法
# RDD.scala
/**
   * Return an array that contains all of the elements in this RDD.
   *
   * @note This method should only be used if the resulting array is expected to be small, as
   * all the data is loaded into the driver's memory.
   */
  def collect(): Array[T] = withScope {
   
   
    val results = sc.runJob(this, (iter: Iterator[T]) => iter.toArray)
    Array.concat(results: _*)
  }
SparkContext对象中的runJob的方法调用自身另外一个runJob方法,开始执行任务

我们主要关注分区数,所以继续追踪runJonb的第三个参数 【0 until rdd.partitions.length】

# SparkContext.scala
/**
   * Run a job on all partitions in an RDD and return the results in an array.
   *
   * @param rdd target RDD to run tasks on
   * @param func a function to run on each partition of the RDD
   * @return in-memory collection with a result of the job (each collection element will contain
   * a result from one partition)
   */
  def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {
   
   
    runJob(rdd, func, 0 until rdd.partitions.length)
  }
SparkContext对象执行runJob时,需要从rdd对象中拿partitions (0 until rdd.partitions.length)

rdd对象中的partitions方法调用了 getPartitions来获取分区数

// RDD.scala
/**
   * Get the array of partitions of this RDD, taking into account whether the
   * RDD is checkpointed or not.
   */
  final def partitions: Array[Partition] = {
   
   
    checkpointRDD.map(_.partitions).getOrElse {
   
   
      if (partitions_ == null) {
   
   
        stateLock.synchronized {
   
   
          if (partitions_ == null) {
   
   
            partitions_ = getPartitions
            partitions_.zipWithIndex.foreach {
   
    case (partition, index) =>
              require(partition.index == index,
                s"partitions($index).partition == ${partition.index}, but it should equal $index")
            }
          }
        }
      }
      partitions_
    }
  }
这个 getPartitions方法有多个实现类, 因为前面看到textFile最终创建的是一个HadoopRDD,所以这里看的是HadoopRDD.scala中的getPartitions()

我们可以看到getPartitions()方法中,返回的数组是有inputSplits决定
inputSplits 又是来自allInputSplits
所以val allInputSplits = getInputFormat(jobConf).getSplits(jobConf, minPartitions)这行代码是计算分区数的关键代码
minPartitions就是我们在调用textFile()方法时传入的第二个参数。

<think> 我们正在讨论Spark分区机制。根据用户的问题,他/她想了解Spark如何进行分区以及如何判定分区大小。结合之前的对话历史和提供的引用内容,我们可以总结如下: 1. **分区的目的**:分区Spark并行计算的基本单位,合理分区可以显著提升性能[^1]。 2. **分区方式**: - **默认分区**:Spark根据数据源和配置自动划分分区。例如,从HDFS读取文件,每个Block(默认128MB)对应一个分区[^4]。 - **手动分区**:通过`repartition()`或`coalesce()`调整分区数。 - **分区器(Partitioner)**:在Shuffle操作中,Spark使用分区器决定数据如何分布到下游分区。默认是哈希分区器(HashPartitioner)[^3][^5]。 3. **分区大小判定**: - **数据量**:Spark会根据数据量大小和集群配置自动调整分区数[^2]。 - **配置参数**:`spark.default.parallelism`设置默认并行度(即分区数)[^5]。 - **父RDD影响**:子RDD的分区数可能继承自父RDD或由操作类型决定(如join操作的分区数由`spark.default.parallelism`决定)[^5]。 现在,我们详细展开: ### 一、Spark分区方式 #### 1. 初始分区(数据加载) - **文件数据源**(如HDFS): - 每个文件块(Block)对应一个分区。 - 例如:HDFS块大小为128MB,则一个1GB文件会被分为8个分区(1024/128=8)[^4]。 - **集合数据源**(如`sc.parallelize()`): - 分区数由`spark.default.parallelism`决定(集群模式默认值为总核心数,本地模式为本地线程数)[^5]。 #### 2. 转换操作中的分区变化 | 操作类型 | 分区行为 | |-------------------|--------------------------------------------------------------------------| | **窄依赖操作** | 分区数不变(如`map`、`filter`) | | **宽依赖操作** | 触发Shuffle,生成新分区(如`groupByKey`、`join`) | | **重分区操作** | 显式调整分区:<br>- `repartition(numPartitions)`:增加或减少分区数<br>- `coalesce()`:仅减少分区数(无Shuffle) | #### 3. 分区器(Partitioner)的作用 - **哈希分区器(HashPartitioner)**(默认): - 分区规则:$ \text{partition} = \text{hash}(key) \\mod \\text{numPartitions} $。 - 问题:可能引起数据倾斜(某些分区数据量过大)[^3]。 - **范围分区器(RangePartitioner)**: - 按Key的范围分区(需抽样数据),用于`sortByKey`等操作。 - **自定义分区器**: - 继承`Partitioner`类,实现`numPartitions`和`getPartition`方法。 ### 二、分区大小的判定方法 #### 1. 自动判定逻辑 - **目标**:使每个分区数据量适中(通常建议128MB以内)。 - **影响因素**: - **数据源大小**:从HDFS读取分区数 ≈ 总数据量 / Block大小。 - **集群资源**:分区数不应超过总核心数的2~4倍(避免过度并行化)。 - **Shuffle操作**:新分区数由`spark.default.parallelism`控制(默认200)[^5]。 #### 2. 手动调优参数 | 参数 | 作用 | |-------------------------------|----------------------------------------------------------------------| | `spark.default.parallelism` | 设置Shuffle操作的默认分区数(如`reduceByKey`)[^5] | | `spark.sql.shuffle.partitions` | DataFrame API中Shuffle操作的分区数(默认200) | #### 3. 优化建议 - **避免倾斜**:使用`salting`技术(为Key添加随机前缀)分散热点数据[^3]。 - **合理分区数**: - 太大:任务调度开销增加。 - 太小:无法充分利用并行度。 - 公式参考:$ \text{理想分区数} = \frac{\text{总数据量}}{\text{目标分区大小}} $(目标分区大小通常为100-200MB)。 - **动态调整**: - Spark 3.0+支持自适应查询(AQE),可自动合并小分区(`spark.sql.adaptive.enabled=true`)。 ### 三、案例说明 ```scala // 示例:从HDFS加载数据并调整分区 val data = spark.read.text("hdfs://path/to/data.txt") // 原始分区数=文件块数 val words = data.flatMap(_.split(" ")) // 手动重分区(触发Shuffle) val repartitioned = words.repartition(100) // 使用分区器操作 val kvPairs = words.map(word => (word, 1)) val counts = kvPairs.reduceByKey(_ + _, 50) // 设置50个分区 ``` ### 四、总结 - **分区方式**:自动(数据源/Shuffle) + 手动(`repartition`/`coalesce`)。 - **分区大小判定**: - 初始加载:由数据源决定(如HDFS块大小)[^4]。 - Shuffle阶段:由`spark.default.parallelism`或操作参数决定[^5]。 - 优化目标:平衡数据分布,避免倾斜[^3]。 --- ### 相关问题 1. 如何检测Spark任务的数据倾斜? 2. Spark 3.0的自适应查询(AQE)如何优化分区? 3. 哈希分区器和范围分区器的适用场景分别是什么? 4. 如何自定义Spark分区器解决特定业务场景的数据分布问题? </think>### Spark分区机制详解:分区方式与分区大小判定 #### 一、Spark分区方式 Spark主要通过以下三种方式确定数据分区: 1. **基于数据源的分区**(初始分区) - 从HDFS读取:每个HDFS块(默认128MB)对应一个RDD分区[^4] - 集合数据:由`spark.default.parallelism`控制(默认=集群核心数) ```scala // 示例:集合数据分区 val rdd = sc.parallelize(1 to 1000) // 分区数=spark.default.parallelism ``` 2. **基于转换操作的分区** | 操作类型 | 分区行为 | |----------------|--------------------------------------------------------------------------| | **窄依赖操作** | 保留父RDD分区结构(如`map`, `filter`) | | **宽依赖操作** | 触发重新分区(如`groupByKey`, `join`),使用HashPartitioner(默认)[^3] | ```scala // 宽依赖示例:reduceByKey默认使用HashPartitioner val counts = rdd.reduceByKey(_ + _) // 分区数=spark.default.parallelism ``` 3. **显式重分区方法** ```scala // 增加分区(触发Shuffle) rdd.repartition(100) // 减少分区(无Shuffle) rdd.coalesce(10) ``` #### 二、分区大小判定机制 Spark通过以下策略决定分区大小和数量: 1. **分区数判定公式** $$ \text{分区数} = \max\left(\text{父RDD分区数}, \text{spark.default.parallelism}\right) $$ 特殊规则: - Join操作:取父RDD中最大分区数[^5] - 无父RDD:取`spark.default.parallelism` 2. **分区数据量控制** | 配置参数 | 默认值 | 作用说明 | |-------------------------------|----------|-----------------------------------| | `spark.sql.files.maxPartitionBytes` | 128MB | 单个分区最大数据量[^2] | | `spark.default.parallelism` | 集群核心数 | Shuffle默认分区数[^5] | | `spark.sql.shuffle.partitions` | 200 | DataFrame的Shuffle分区数 | 3. **动态分区优化(Spark 3.0+)** - 开启自适应查询(AQE): `spark.sql.adaptive.enabled=true` - 自动合并小分区(避免数据倾斜) ```sql -- 自动合并小于1MB的分区 SET spark.sql.adaptive.coalescePartitions.minPartitionSize=1MB ``` #### 三、分区策略对比 | 分区策略 | 优点 | 缺点 | 适用场景 | |------------------|---------------------------|---------------------------|-----------------------| | **HashPartitioner** (默认) | 计算简单,分布均匀 | 可能数据倾斜 | 常规聚合操作 | | **RangePartitioner** | 避免数据倾斜 | 需要数据抽样,开销较大 | 排序操作 | | **自定义分区器** | 解决业务特定分布问题 | 需开发实现 | 特殊数据分布场景 | #### 四、最佳实践建议 1. **监控分区状态** ```scala // 查看分区信息 rdd.partitions.length // 分区数 rdd.glom().collect() // 查看每个分区数据 ``` 2. **优化策略** - 分区大小控制在100-200MB(HDFS块大小对齐) - 倾斜数据处理: ```scala // 添加随机前缀解决倾斜 rdd.map(k => (s"${Random.nextInt(10)}_$k", v)) .reduceByKey(_ + _) .map(kv => (kv._1.split("_")(1), kv._2)) ``` - 读取文件合并小文件: ```scala spark.read.option("mergeSchema", "true").parquet("path") ``` #### 五、分区机制示例 ```mermaid flowchart TD A[HDFS文件] -->|128MB/块| B[初始分区=块数] B --> C[filter/map操作] C -->|窄依赖| D[保持分区数] D -->|reduceByKey| E[Shuffle Write] E -->|HashPartitioner| F[新分区数= spark.default.parallelism] F --> G[Shuffle Read] ``` > **关键结论**:Spark分区由数据源特性、操作类型和配置参数共同决定,最佳分区应平衡数据分布均匀性和任务调度开销[^1][^2][^4]。 --- ### 相关问题 1. 如何通过Spark UI监控分区数据倾斜? 2. Spark 3.0的自适应查询(AQE)如何动态调整分区? 3. DataFrame和RDD在分区策略上有哪些差异? 4. 如何处理Spark Shuffle过程中的数据倾斜问题?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值