¿Cuántas tareas habrá después de que Spark lea la tabla de Hive?

Como un marco importante para los almacenes de datos en big data, Hive ha ido mejorando en velocidad desde el lento motor MR, hasta Tez y el Spark de hoy. Aunque un SQL de Hive se convertirá en varios trabajos de Spark y cuántas Etapas se generarán, aún no podemos juzgar, "¿Pero cuántas Tareas habrá después de que Spark lea la tabla de Hive?"

Sabemos que "la cantidad de tareas en Spark está determinada por particiones" , entonces, ¿cómo decidir?

  1. Cuando Hive lee archivos no segmentables, todos los datos solo pueden ser leídos por un solo nodo, incluso si usted mismo configura manualmente las particiones.

  2. Si los archivos en cada partición de la tabla de Hive son archivos pequeños de varios megabytes que se pueden dividir, entonces cuando Spark lee, cada tarea solo procesa archivos tan pequeños, lo que no solo desperdicia recursos sino que también desperdicia tiempo.¿Cómo optimizarlo?

Luego comenzamos el análisis desde el código fuente del archivo leído por chispa:

//简单写个读取文件的语句
val words: RDD[String] = sc.textFile("xxxx",3)

Entramos desde el método textfile(),

  def textFile(
      path: String,
      //注意看这里的最小分区,如果textfile()方法中传了分区参数的话就会以传入的为准,否则就会使用默认值
      //def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
      //我们通过源码发现,defaultMinPartitions 是默认并行度和2的最小值,而默认并行度=自己的cpu核数,所以分区最小值一般等于2
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
    assertNotStopped()
    hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
    //把上面的minPartitions传到这个hadoopFile()方法中。
      minPartitions).map(pair => pair._2.toString).setName(path)
  }

Entonces ingresamos desde hadoopFile()

Se encuentra que se llamará a HadoopRDD para leer el archivo, y el de aquí inputFormatClassse especifica cuando se crea Hive. No se especifica de forma predeterminada  org.apache.hadoop.mapred.TextInputFormat. Al mismo tiempo, preste atención al parámetro aquí minPartitions, que es el valor pasado por el método que acabamos de mencionar. Esta vez, siga adelante e ingrese desde HadoopRDD y recupere minPartitions para ver qué método está usando este parámetro. Después de buscar, encontré el siguiente método

//getSplits()获取切片数
val allInputSplits = getInputFormat(jobConf).getSplits(jobConf, minPartitions)

Luego continúe ingresando desde el método getSplits, y luego busque la clase de implementación a través de Ctrl+h, aquí seleccionamos FileInputFormat y continuamos recuperando getSplits, y luego busque el siguiente método, echemos un vistazo a su código fuente:

  public InputSplit[] getSplits(JobConf job, int numSplits)
    throws IOException {
    Stopwatch sw = new Stopwatch().start();
    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();
    }
//注意看这里,文件的总大小,直接除以之前获取到切片数,为2
    long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
    long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
      FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
      //而上面的minSplitSize通过看源码发现private long minSplitSize = 1;
//从这里得到的minSize 也等于1
    // generate splits
    ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
    NetworkTopology clusterMap = new NetworkTopology();
     files是上面扫描的分区目录下的part-*****文件
    for (FileStatus file: files) {
      Path path = file.getPath();
      long length = file.getLen();
      if (length != 0) {
        FileSystem fs = path.getFileSystem(job);
        BlockLocation[] blkLocations;
        if (file instanceof LocatedFileStatus) {
          blkLocations = ((LocatedFileStatus) file).getBlockLocations();
        } else {
          blkLocations = fs.getFileBlockLocations(file, 0, length);
        }
        //判断文件是否可切割
        if (isSplitable(fs, path)) {
        // 这里获取的不是文件本身的大小,它的大小从上面的length就可以知道,这里获取的是HDFS文件块(跟文件本身没有关系)的大小
      // HDFS文件块的大小由两个参数决定,分别是 dfs.block.size 和 fs.local.block.size
      // 在HDFS集群模式下,由 dfs.block.size 决定,对于Hadoop2.0来说,默认值是128MB
      // 在HDFS的local模式下,由 fs.local.block.size 决定,默认值是32MB
          long blockSize = file.getBlockSize();// 128MB
          // 这里计算splitSize,goalSize是textfile()方法中指定路径下的文件总大小,minSize为1
          long splitSize = computeSplitSize(goalSize, minSize, blockSize);
//而这里computeSplitSize = Math.max(minSize, Math.min(goalSize, blockSize))
//所以如果文件大小>128M,那么splitSize 就等于128M,否则就等于文件大小
          long bytesRemaining = length;
          // 如果文件大小大于splitSize,就按照splitSize对它进行分块
      // 由此可以看出,这里是为了并行化更好,所以按照splitSize会对文件分的更细,因而split会更多
      //SPLIT_SLOP 为1.1,也就是说,如果文件大小是切片大小的1.1倍以下时,也会分到一个切片,而不会分为2个
          while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
            String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,
                length-bytesRemaining, splitSize, clusterMap);
            splits.add(makeSplit(path, length-bytesRemaining, splitSize,
                splitHosts[0], splitHosts[1]));
            bytesRemaining -= splitSize;
          }

//而当切到最后一个切片<1.1倍时,就会再追加一个切片。
//举个例子,假如文件大小为160M,因为160/128>1.1,所以切了一个之后,还剩32M
//32M/128<1.1,但是32M != 0 ,所以就会为这32M生成一个切片。
          if (bytesRemaining != 0) {
            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);
          //在这里就makeSplit = new FileSplit(file, start, length, hosts);
          //所以就是1个分区直接读取,所以假如这个文件的大小是500G不可分割的文件,
          //那么只能是一个节点去读,只能用Spark的一个Task,容易数据倾斜。
          splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1]));
        }
      } else { 
        //Create empty hosts array for zero length files
        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.elapsedMillis());
    }
    return splits.toArray(new FileSplit[splits.size()]);
  }

Entonces, si hay 200 archivos pequeños debajo de la partición de la tabla de Hive y el tamaño es de 5M, entonces cada archivo pequeño es una división, lo que corresponde a una partición de Spark, por lo que cuando hay varias particiones de archivos pequeños, la cantidad de tareas de Spark también será aumenta en línea recta, y el número de particiones autoespecificado no resuelve el problema de los archivos pequeños, por lo que es imposible cambiar la situación de leer una gran cantidad de tareas en la tabla de Hive.

Si se trata de un archivo indivisible con un tamaño de 500 G (un archivo indivisible mucho más grande que un tamaño de bloque de 128 M), solo puede ser leído por un nodo y solo se puede usar una tarea de Spark, que es fácil de sesgar los datos.

Entonces, ¿volviendo a la pregunta del principio? Cómo optimizar estos dos escenarios:

  1. De acuerdo con el escenario comercial real, el volumen de datos de la empresa es relativamente grande y habrá varios gigabytes de datos todos los días, por lo tanto, no use métodos de compresión indivisibles cuando vuelva a almacenar, puede usar Lzo o bzip2

  2. Cuando se generan varios archivos pequeños, es necesario pensar en el origen de los archivos pequeños, es decir, si el método de partición de la tabla Hive actual es razonable. ¿Es razonable la configuración progresiva de los archivos de registro en Hive? Lo último es fusionar archivos pequeños.

Finalmente, puse el proceso de trazabilidad del código fuente en una imagen para que todos lo vean.

Supongo que te gusta

Origin blog.csdn.net/ytp552200ytp/article/details/126155510
Recomendado
Clasificación