Spark SQL on Hive,为什么小文件多task也多?

本文深入探讨了Spark处理小文件时任务多的原因,主要围绕HadoopTableReader和HadoopRDD的实现。通过分析代码,发现任务数量与RDD分区直接相关,而RDD分区由Hadoop的splitSize计算得出,这取决于文件大小和配置参数。在处理大量小文件时,由于每个文件可能对应一个任务,导致task数量增多,影响性能。解决方案包括合并小文件和调整配置参数。
摘要由CSDN通过智能技术生成

为什么小文件多,task也多?

Spark SQL通过这个类读取Hive表 org.apache.spark.sql.hive.TableReader$HadoopTableReader
HadoopTableReader继承自TableReader类,而该基类仅有两个方法:

  • makeRDDForTable(hiveTable: HiveTable): RDD[InternalRow]
  • makeRDDForPartitionedTable(partitions: Seq[HivePartition]): RDD[InternalRow]

因此关注HadoopTableReader类对这两个方法的实现即可。

makeRDDForTable实现更简单清晰些,因此从它开始看:

  def makeRDDForTable(
      hiveTable: HiveTable,
      deserializerClass: Class[_ <: Deserializer],
      filterOpt: Option[PathFilter]): RDD[InternalRow] = {
    // ...校验、配置、表的路径等...
 
    // 这里创建出RDD,分区是由该方法定义的
    // makeRDDForPartitionedTable中创建RDD也是通过这个方法
    val hadoopRDD = createHadoopRdd(localTableDesc, inputPathStr, ifc)
    // ...
  }

因此跟进createHadoopRdd:

private def createHadoopRdd(
  tableDesc: TableDesc,
  path: String,
  inputFormatClass: Class[InputFormat[Writable, Writable]]): RDD[Writable] = {

  val initializeJobConfFunc = HadoopTableReader.initializeLocalJobConfFunc(path, tableDesc) _

  val rdd = new HadoopRDD(
    sparkSession.sparkContext,
    _broadcastedHadoopConf.asInstanceOf[Broadcast[SerializableConfiguration]],
    Some(initializeJobConfFunc),
    inputFormatClass,
    classOf[Writable],
    classOf[Writable],
    // 单从变量名即可识别出,对RDD切分的最小切片数
    _minSplitsPerRDD)

  // Only take the value (skip the key) because Hive works only with values.
  rdd.map(_._2)
}

查看_minSplitsPerRDD是如何定义的:

// Hadoop honors "mapreduce.job.maps" as hint,
// but will ignore when mapreduce.jobtracker.address is "local".
// https://hadoop.apache.org/docs/r2.6.5/hadoop-mapreduce-client/hadoop-mapreduce-client-core/
// mapred-default.xml
//
// In order keep consistency with Hive, we will let it be 0 in local mode also.
private val _minSplitsPerRDD = if (sparkSession.sparkContext.isLocal) {
  0 // will splitted based on block by default.
} else {
  // 比对配置和defaultMinPartitions,取其中最大值
  math.max(hadoopConf.getInt("mapreduce.job.maps", 1),
    sparkSession.sparkContext.defaultMinPartitions)
}

我们查看默认的最小分区数是如何定义的:

// 可以看到,Spark是与最小并行度进行比对,取最小值
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)

当我们进一步跟踪defaultParallelism,可以发现它是由Spark调度器定义的,代码中有4个相关的实现类,这些实现类中对于该值的定义也有不同的实现类,为了限定范围,我们按照企业级常用的集群模式的调度来查看:

// org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend
  override def defaultParallelism(): Int = {
    // totoalCoreCount是集群内core的总数,对该变量有兴趣可以跟进到receiveAndReply方法下的case RegisterExecutor
    conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
  }

在企业级生产环境下,defaultParallelism一般由Executor的资源数定义,一般都大于2,但defaultMinPartitions的值是min(defaultParallelism, 2),因此一般情况下,defaultMinPartitions为2,也由此得到_minSplitsPerRDD一般为2。
费了老大功夫,只确认了一个参数的大致取值为2

接着查看HadoopRDD类中是如何分片的:

override def getPartitions: Array[Partition] = {
  val jobConf = getJobConf()
  // add the credentials here as this can be called before SparkContext initialized
  SparkHadoopUtil.get.addCredentials(jobConf)
  // 先看完整个方法定义,可以看到该方法返回了分区数组,而分区数组元素的数量实质是由下面这个变量决定的
  // 因此,需要跟进这里的getSplits方法
  val allInputSplits = getInputFormat(jobConf).getSplits(jobConf, minPartitions) // minPartitions即上述确认的2
  val inputSplits = if (ignoreEmptySplits) {
    allInputSplits.filter(_.getLength > 0)
  } else {
    allInputSplits
  }
  val array = new Array[Partition](inputSplits.size)
  for (i <- 0 until inputSplits.size) {
    array(i) = new HadoopPartition(id, i, inputSplits(i))
  }
  array
}

getSplits实现方法众多,一个个查看确认是org.apache.hadoop.mapred.FileInputFormat实现的:

public InputSplit[] getSplits(JobConf job, int numSplits)
  throws IOException {
  // 获取目录下所有文件
  FileStatus[] files = listStatus(job);
  
  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);
  // 分片的最小大小,最小1
  long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
    FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);

  // generate splits
  ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
  for (FileStatus file: files) {
    Path path = file.getPath();
    long length = file.getLen();
    if (length != 0) {
      // 文件可分片
      if (isSplitable(fs, path)) {
        // 文件系统的块大小
        long blockSize = file.getBlockSize();
        // 该方法的实现即Math.max(minSize, Math.min(goalSize, blockSize))
        // 计算出应当分割的阈值
        long splitSize = computeSplitSize(goalSize, minSize, blockSize);

        long bytesRemaining = length;
        // SPLIT_SLOP为1.1
        while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
          // ...分片并将分片添加到splits
          bytesRemaining -= splitSize;
        }

        // ...分片剩余部分

      } else {
        // 不可分片的文件,就只创建一个分片
      }
    } else { 
      // 空文件就分一个为空的分片
    }
   }
  return splits.toArray(new FileSplit[splits.size()]);
}

可以看到,当输入的都是小文件时,splitSize = max(minSize, Math.min(goalSize, blockSize)),

  • 假设goalSize比每个文件都大,取到的splitSize也都会比每个文件大,基本一个文件对应一个切片;
  • 假设goalSize比部分文件小,取到splitSize有可能比每个文件小,一个文件可能对应多个切片。

此时我们再回到HadoopRDD#getPartitions方法,上述的切片数量对应着RDD的分区数,在Spark中,RDD分区和task一一对应,因此小文件多,task也就多。即使通过repartition,也无法改变读取时RDD分区多的事实!因此一定要避免用Spark处理过多的小文件问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
对于Hive on Spark的调优,有几个关键的配置可以考虑。首先,可以通过调整hive.auto.convert.join.noconditionaltask.size来控制将基础join转化为map join的阈值,从而影响性能。这个配置在Hive on MapReduce和Hive on Spark中都可以使用,但是两者的解释略有不同。 其次,可以通过设置一些Spark相关的配置来进行调优。例如,可以设置hive.execution.engine为spark来指定使用Spark作为执行引擎。还可以设置spark.executor.memory、spark.executor.cores、spark.executor.instances来调整Spark执行器的内存和核心资源分配。另外,设置spark.serializer为org.apache.spark.serializer.KryoSerializer可以改进序列化的性能。 此外,Hive on Spark相比于Hive on MapReduce有更好的性能,并且提供了相同的功能。用户的SQL不需要进行修改,就可以直接在Hive on Spark上运行。同时,UDF函数也被全部支持。 总结起来,在运行于YARN模式下的Hive on Spark的调优,可以关注以下几点: 1. 调整hive.auto.convert.join.noconditionaltask.size来控制基础join转化为map join的阈值。 2. 设置相关Spark配置,如hive.execution.engine、spark.executor.memory、spark.executor.cores、spark.executor.instances等,来优化资源分配和序列化性能。 3. 充分利用Hive on Spark的性能优势,无需修改SQL即可运行,并支持全部UDF函数。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [hive on spark 调优](https://blog.csdn.net/weixin_45489441/article/details/123252215)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Hive on Spark调优](https://blog.csdn.net/javastart/article/details/126041883)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值