从源码看Spark读取Hive表数据小文件和分块的问题

大数据 Spark
本文涉及源码基于Spark2.0.0和Hadoop2.6.0,不同版本代码可能不一致,需自己对应。此外针对TextInputFormat格式的Hive表,其他格式的比如Parquet有Spark自己的高效实现,不在讨论范围之内

前言

有同事问到,Spark读取一张Hive表的数据Task有一万多个,看了Hive表分区下都是3MB~4MB的小文件,每个Task只处理这么小的文件,实在浪费资源浪费时间。而我们都知道Spark的Task数由partitions决定,所以他想通过repartition(num)的方式来改变分区数,结果发现读取文件的时候Task数并没有改变。遂问我有什么参数可以设置,从而改变读取Hive表时的Task数,将小文件合并大文件读上来

本文涉及源码基于Spark2.0.0和Hadoop2.6.0,不同版本代码可能不一致,需自己对应。此外针对TextInputFormat格式的Hive表,其他格式的比如Parquet有Spark自己的高效实现,不在讨论范围之内

分析

Spark读取Hive表是通过HadoopRDD扫描上来的,具体可见 org.apache.spark.sql.hive.TableReader类,构建HadoopRDD的代码如下 

  1. val rdd = new HadoopRDD( 
  2. sparkSession.sparkContext, 
  3. _broadcastedHadoopConf.asInstanceOf[Broadcast[SerializableConfiguration]], 
  4. Some(initializeJobConfFunc), 
  5. inputFormatClass, 
  6. classOf[Writable], 
  7. classOf[Writable], 
  8. _minSplitsPerRDD) 

这里inputFormatClass是Hive创建时指定的,默认不指定为 org.apache.hadoop.mapred.TextInputFormat,由它就涉及到了HDFS文件的FileSplit数,从而决定了上层Spark的partition数。在进入HadoopRDD类查看之前,还有一个参数需要我们注意,就是 _minSplitsPerRDD,它在后面SplitSize的计算中是起了作用的。

我们看一下它的定义 

  1. private val _minSplitsPerRDD = if (sparkSession.sparkContext.isLocal) { 
  2. 0 // will splitted based on block by default
  3. else { 
  4. math.max(hadoopConf.getInt("mapred.map.tasks", 1), 
  5. sparkSession.sparkContext.defaultMinPartitions) 

在我们指定以--master local模式跑的时候,它为0,而在其他模式下,则是求的一个***值。这里重点看 defaultMinPartitions,如下 

  1. def defaultMinPartitions: Int = math.min(defaultParallelism, 2) 
  2.  
  3. // defaultParallelism 在yarn和standalone模式下的计算 
  4. def defaultParallelism(): Int = { 
  5. conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2)) 

从这里可以看到,defaultMinPartitions的值一般为2,而 mapred.map.tasks 或者 mapreduce.job.maps( 新版参数)是Hadoop的内建参数,其默认值也为2,一般很少去改变它。所以这里_minSplitsPerRDD的值基本就是2了。

下面我们跟到HadoopRDD类里,去看看它的partitions是如何来的 

  1. def getPartitions: Array[Partition] = { 
  2. val jobConf = getJobConf() 
  3. // add the credentials here as this can be called before SparkContext initialized 
  4. SparkHadoopUtil.get.addCredentials(jobConf) 
  5. // inputFormat就是上面参数inputFormatClass所配置的类的实例 
  6. val inputFormat = getInputFormat(jobConf) 
  7. // 此处获取FileSplit数,minPartitions就是上面的_minSplitsPerRDD 
  8. val inputSplits = inputFormat.getSplits(jobConf, minPartitions) 
  9. val array = new Array[Partition](inputSplits.size
  10. // 从这里可以看出FileSplit数决定了Spark扫描Hive表的partition数 
  11. for (i <- 0 until inputSplits.size) { 
  12. array(i) = new HadoopPartition(id, i, inputSplits(i)) 
  13. array 

在 getPartitions 方法里我们可以看到 FileSplit数***决定了Spark读取Hive表的Task数,下面我们再来看看 mapred.TextInputFormat 类里 getSplits 的实现

分两步来看,首先是扫描文件,计算文件大小的部分 

  1. FileStatus[] files = listStatus(job); 
  2.  
  3. ..... 
  4.  
  5. long totalSize = 0; // compute total size 
  6. for (FileStatus file: files) { // check we have valid files 
  7. if (file.isDirectory()) { 
  8. throw new IOException("Not a file: "+ file.getPath()); 
  9. totalSize += file.getLen(); 
  10.  
  11. // numSplits就是上面传入的minPartitions,为2 
  12. long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits); 
  13. long minSize = Math.max(job.getLong("mapreduce.input.fileinputformat.split.minsize", 1), minSplitSize); 
  14.  
  15. // minSplitSize 默认为1,唯一可通过 setMinSplitSize 方法设置 
  16. private long minSplitSize = 1; 

针对Hive表的分区,Spark对每个分区都构建了一个HadoopRDD,每个分区目录下就是实际的数据文件,例如我们集群的某一张表按天分区,每天下面有200个数据文件,每个文件大概3MB~4MB之间,这些实际上是reduce设置不合理导致的小文件产生,如下图 

HiveFile

此处 listStatus 方法就是扫描的分区目录,它返回的就是图中显示的具体 part-*****文件的FileStatus对象,一共200个。从 totalSize 的计算可以看出,它是这200个文件的总大小,为838MB,因此 goalSize 就为419MB。

参数 mapreduce.input.fileinputformat.split.minsize 在Spark程序没有配的情况下,获取的值为0,而 minSplitSize在Spark获取FileSplits的时候并没有被设置,所以为默认值1,那么 minSize 就为1

其次,我们再来看从文件划分Split,部分代码如下(部分解释见注释) 

  1. ArrayList splits = new ArrayList(numSplits); 
  2. NetworkTopology clusterMap = new NetworkTopology(); 
  3.  
  4. // files是上面扫描的分区目录下的part-*****文件 
  5. for (FileStatus file: files) { 
  6. Path path = file.getPath(); 
  7. long length = file.getLen(); 
  8. if (length != 0) { 
  9. FileSystem fs = path.getFileSystem(job); 
  10. BlockLocation[] blkLocations; 
  11. if (file instanceof LocatedFileStatus) { 
  12. blkLocations = ((LocatedFileStatus) file).getBlockLocations(); 
  13. else { 
  14. blkLocations = fs.getFileBlockLocations(file, 0, length); 
  15. // 判断文件是否可切割 
  16. if (isSplitable(fs, path)) { 
  17. // 这里获取的不是文件本身的大小,它的大小从上面的length就可以知道,这里获取的是HDFS文件块(跟文件本身没有关系)的大小 
  18. // HDFS文件块的大小由两个参数决定,分别是 dfs.block.size 和 fs.local.block.size 
  19. // 在HDFS集群模式下,由 dfs.block.size 决定,对于Hadoop2.0来说,默认值是128MB 
  20. // 在HDFS的local模式下,由 fs.local.block.size 决定,默认值是32MB 
  21. long blockSize = file.getBlockSize(); // 128MB 
  22.  
  23. // 这里计算splitSize,根据前面计算的goalSize=419MB,minSize为1 
  24. long splitSize = computeSplitSize(goalSize, minSize, blockSize); 
  25.  
  26. long bytesRemaining = length; 
  27. // 如果文件大小大于splitSize,就按照splitSize对它进行分块 
  28. // 由此可以看出,这里是为了并行化更好,所以按照splitSize会对文件分的更细,因而split会更多 
  29. while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) { 
  30. String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, 
  31. length-bytesRemaining, splitSize, clusterMap); 
  32. splits.add(makeSplit(path, length-bytesRemaining, splitSize, 
  33. splitHosts[0], splitHosts[1])); 
  34. bytesRemaining -= splitSize; 
  35.  
  36. if (bytesRemaining != 0) { 
  37. String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length 
  38. - bytesRemaining, bytesRemaining, clusterMap); 
  39. splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining, 
  40. splitHosts[0], splitHosts[1])); 
  41. else { 
  42. String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap); 
  43. splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1])); 
  44. else { 
  45. //Create empty hosts array for zero length files 
  46. splits.add(makeSplit(path, 0, length, new String[0])); 

从上面可以看到,splitSize是从 computeSplitSize(goalSize, minSize, blockSize) 计算来的,这三个参数我们都知道大小,那么计算规则是怎么样的呢

规则:Math.max(minSize, Math.min(goalSize, blockSize)),从而我们可以知道 splitSize = 128MB,对于3MB~4MB的小文件来说,就 决定了一个小文件就是一个split了,从而对应了一个Spark的partition,所以我们一个分区下就有200个partition,当取两个月的数据时,就是 200 * 30 * 2 = 12000,从而是12000个Task,跟同事所说的吻合!

> 而从TextInputFormat里分Split的逻辑来看,它只会把一个文件分得越来越小,而不会对小文件采取合并,所以无论调整哪个参数,都没法改变这种情况!而通过repartition强行分区,也是在拿到HDFS文件之后对这12000个partition进行重分区,改变不了小文件的问题,也无法改变读取Hive表Task数多的情况

总结

1、Block是物理概念,而Split是逻辑概念,***数据的分片是根据Split来的。一个文件可能大于BlockSize也可能小于BlockSize,大于它就会被分成多个Block存储到不同的机器上,SplitSize可能大于BlockSize也可能小于BlockSize,SplitSize如果大于BlockSize,那么一个Split就可能要跨多个Block。对于数据分隔符而言,不用担心一个完整的句子分在两个Block里,因为在Split构建RecordReader时,它会被补充完整

2、对于采用 org.apache.hadoop.mapred.TextInputFormat 作为InputFormat的Hive表,如果存在小文件,Spark在读取的时候单凭调参数和repartition是改变不了分区数的!对于小文件的合并,目前除了Hadoop提供的Archive方式之外,也只能通过写MR来手动合了,***的方式还是写数据的时候自己控制reduce的个数,把握文件数

3、对于Spark直接通过SparkContext的 textFile(inputPath, numPartitions) 方法读取HDFS文件的,它底层也是通过HadoopRDD构建的,它的参数numPartitions就是上面计算goalSize的numSplits参数,这篇 文章 对原理描述的非常详细,非常值得一读

4、对于小文件合并的InputFormat有 org.apache.hadoop.mapred.lib.CombineFileInputFormat,跟它相关的参数是 mapreduce.input.fileinputformat.split.maxsize,它用于设置一个Split的***值

5、跟mapred.TextInputFormat 里的Split划分相关的参数

  • mapreduce.input.fileinputformat.split.minsize : 决定了计算Split划分时的minSize
  • mapreduce.job.maps 或 mapred.map.tasks : 决定了getSplits(JobConf job, int numSplits)方法里的numSplits,从而可以影响goalSize的大小
  • dfs.block.size 或 fs.local.block.size : 决定了HDFS的BlockSize

6、MapReduce新版API里的 org.apache.hadoop.mapreduce.lib.input.TextInputFormat,它的SplitSize与上面说到的计算方式不一样,getSplits方法的签名为 getSplits(JobContext job),不再有numSplilts这个参数,splitSize的计算规则改为 Math.max(minSize, Math.min(maxSize, blockSize)),minSize和blockSize跟之前一样,新的maxSize为conf.getLong("mapreduce.input.fileinputformat.split.maxsize", Long.MAX_VALUE)

7、在Spark2.0.0里,设置Hadoop相关的参数(比如mapreduce开头的)要通过 spark.sparkContext.hadoopConfiguration 来设置

责任编辑:未丽燕 来源: 网络大数据
相关推荐

2023-05-11 00:17:44

分区HiveReduce

2017-09-25 16:21:30

Spark on yacluster模式

2020-08-13 14:58:06

Spark小文件存储

2021-07-14 09:48:15

Linux源码Epoll

2021-07-15 14:27:47

LinuxSocketClose

2019-04-17 14:44:42

Spark内存源码

2019-10-10 16:20:23

spark内存管理

2017-06-26 15:00:17

2021-03-10 08:20:54

设计模式OkHttp

2021-06-10 09:52:33

LinuxTCPAccept

2017-04-05 20:00:32

ChromeObjectJS代码

2020-10-10 07:00:16

LinuxSocketTCP

2023-01-31 10:22:00

HiveMapReduce文件合并

2018-02-02 15:48:47

ChromeDNS解析

2010-12-10 08:51:13

Web 2.0Cache集群

2023-03-30 09:06:20

HiveSpark大数据

2021-04-12 06:08:16

HiveSpark大数据

2017-02-09 15:15:54

Chrome浏览器

2020-09-23 12:32:18

网络IOMySQL

2013-10-23 10:48:30

HadoopHDFS文件处理
点赞
收藏

51CTO技术栈公众号