Análisis de código fuente del proceso aleatorio de Spark

prefacio

Para comprender mejor el proceso de reproducción aleatoria de Spark, al leer el código fuente, podemos comprender a fondo el proceso de ejecución del proceso de reproducción aleatoria y el contenido relacionado con la clasificación.

La versión de chispa utilizada en este artículo es: 2.4.4

1、shuffle之BypassMergeSortShuffleWriter

Fundamental:

1. Tantas particiones como haya en la reducción descendente, tantos fileWriter[reduceNumer] se establecen en el mapa ascendente, y los datos de cada partición descendente se escriben en un archivo independiente. Después de escribir todos los archivos de partición, combine los datos de varias particiones en un solo archivo, el código es el siguiente:

while (records.hasNext()) {
      final Product2<K, V> record = records.next();
      final K key = record._1();
      //作者注:将数据写到对应分区的文件中去。
      partitionWriters[partitioner.getPartition(key)].write(key, record._2());
    }

    for (int i = 0; i < numPartitions; i++) {
      final DiskBlockObjectWriter writer = partitionWriters[i];
      partitionWriterSegments[i] = writer.commitAndGet();
      writer.close();
    }

    File output = shuffleBlockResolver.getDataFile(shuffleId, mapId);
    File tmp = Utils.tempFileWith(output);
    try {
      //作者注:合并所有分区的小文件为一个大文件,保证同一个分区的数据连续存在
      partitionLengths = writePartitionedFile(tmp);
      //作者注:构建索引文件
      shuffleBlockResolver.writeIndexFileAndCommit(shuffleId, mapId, partitionLengths, tmp);
    } finally {
      if (tmp.exists() && !tmp.delete()) {
        logger.error("Error while deleting temp file {}", tmp.getAbsolutePath());
      }
    }

2. Dado que los datos de cada archivo de partición independiente pertenecen a la misma reducción, al fusionar archivos, no es necesario ordenarlos, simplemente combínelos en un archivo de acuerdo con el orden de los archivos y cree el archivo de índice de datos de partición correspondiente .

3. Las condiciones para usar BypassMergeSortShuffleWriter son:

        (1), el número de particiones aguas abajo no puede exceder el valor del parámetro spark.shuffle.sort.bypassMergeThreshold (el valor predeterminado es 200)

        (2), operador de preagregación del lado del mapa (reduceByKey)

        El código de juicio específico es el siguiente:

def shouldBypassMergeSort(conf: SparkConf, dep: ShuffleDependency[_, _, _]): Boolean = {
    // We cannot bypass sorting if we need to do map-side aggregation.
    if (dep.mapSideCombine) {
      false
    } else {
      val bypassMergeThreshold: Int = conf.getInt("spark.shuffle.sort.bypassMergeThreshold", 200)
      dep.partitioner.numPartitions <= bypassMergeThreshold
    }
  }

2, reproducción aleatoria, ordenación aleatoria, escritura

Condiciones para ejecutar al escritor:

(1) El número de particiones descendentes excede el valor establecido por el parámetro spark.shuffle.sort.bypassMergeThreshold (predeterminado 200)

(2), omita un escritor llamado UnsafeShuffleWriter (vea 3 para más detalles)

Descripción del proceso de ejecución:

1. Si el final del mapa es un operador de agregación previa (como reduceByKey)

(1), use un mapa: objeto PartitionedAppendOnlyMap para almacenamiento de datos y agregación previa, el código es el siguiente:

        Nota: se puede ver que cuando map.changeValue, la clave actualizada no es la clave de los datos, pero la identificación de partición de la clave se agrega en función de la clave de datos ((getPartition(kv._1), kv. _1)), el propósito de esto es clasificar por ID de partición cuando los datos se desbordan en el disco, para garantizar que los datos de la misma partición se puedan almacenar juntos continuamente.

//作者注:判断是否是一个预聚合算子
if (shouldCombine) {
      // Combine values in-memory first using our AppendOnlyMap
      //作者注:获取预聚合算子的执行函数
      val mergeValue = aggregator.get.mergeValue
      val createCombiner = aggregator.get.createCombiner
      var kv: Product2[K, V] = null
      val update = (hadValue: Boolean, oldValue: C) => {
        if (hadValue) mergeValue(oldValue, kv._2) else createCombiner(kv._2)
      }
      while (records.hasNext) {
        addElementsRead()
        kv = records.next()
        //作者注:使用一个map:PartitionedAppendOnlyMap类型进行数据的存储和预聚合更新
        map.changeValue((getPartition(kv._1), kv._1), update)
        //作者注:执行溢出到磁盘操作
        maybeSpillCollection(usingMap = true)
      }
    }

(2) Ejecute la operación de derrame de datos en disco: maySpillCollection, el código es el siguiente:

 private def maybeSpillCollection(usingMap: Boolean): Unit = {
    var estimatedSize = 0L
    //作者注:判断是否是预聚合算子
    if (usingMap) {
      //作者注:预聚合算子,则把map对象里面的数据写入到磁盘
      estimatedSize = map.estimateSize()
      if (maybeSpill(map, estimatedSize)) {
        map = new PartitionedAppendOnlyMap[K, C]
      }
    } else {
      //作者注:不是预聚合算子,则把buffer对象里面的数据写入到磁盘
      estimatedSize = buffer.estimateSize()
      if (maybeSpill(buffer, estimatedSize)) {
        buffer = new PartitionedPairBuffer[K, C]
      }
    }

       Luego ejecute la función maySpill para juzgar si se debe desbordar al disco de acuerdo con la condición de desbordamiento. El código es el siguiente:

protected def maybeSpill(collection: C, currentMemory: Long): Boolean = {
    var shouldSpill = false
    if (elementsRead % 32 == 0 && currentMemory >= myMemoryThreshold) {
      // Claim up to double our current memory from the shuffle memory pool
      val amountToRequest = 2 * currentMemory - myMemoryThreshold
      val granted = acquireMemory(amountToRequest)
      myMemoryThreshold += granted
      // If we were granted too little memory to grow further (either tryToAcquire returned 0,
      // or we already had more memory than myMemoryThreshold), spill the current collection
      shouldSpill = currentMemory >= myMemoryThreshold
    }

    //作者注:是否溢出磁盘,有两个判断条件
    //1、shouldSplill:判断内存空间的是否充足
    //2、_elementsRead > numElementsForceSpillThreshold:判断当前的写的数据条数是否超过阈值numElementsForceSpillThreshold(默认Integer.MAX_VALUE)
    shouldSpill = shouldSpill || _elementsRead > numElementsForceSpillThreshold
    // Actually spill
    if (shouldSpill) {
      _spillCount += 1
      logSpillage(currentMemory)
      spill(collection)
      _elementsRead = 0
      _memoryBytesSpilled += currentMemory
      releaseMemory()
    }
    shouldSpill
  }

        Si se cumplen las condiciones, esto ejecuta el derrame (recolección) para el desbordamiento de datos, el código es el siguiente:

override protected[this] def spill(collection: WritablePartitionedPairCollection[K, C]): Unit = {
    val inMemoryIterator = collection.destructiveSortedWritablePartitionedIterator(comparator)
    val spillFile = spillMemoryIteratorToDisk(inMemoryIterator)
    spills += spillFile
  }

        Echa un vistazo a esta línea de código:

val inMemoryIterator = collection.destructiveSortedWritablePartitionedIterator(comparador)

        La función de esta línea de código es ordenar los datos, ¿cómo ordenarlos? Después del proceso de depuración del autor, se encuentra que este proceso de clasificación en realidad no está clasificando las claves de los datos, sino clasificando las identificaciones de partición, para garantizar que los datos de la misma partición puedan estar juntos de forma consecutiva y brindar soporte para el clasificación de combinación posterior de archivos de desbordamiento.

        En este punto, se completa la operación de desbordamiento del disco de datos. El siguiente paso es cómo fusionar los datos de desbordamiento.

(3), los archivos de datos del disco de desbordamiento se fusionan en un archivo grande y se establece un archivo de índice de una partición. El proceso de ejecución de código específico es el siguiente (SortShuffleWriter): el proceso de ejecución específico en el interior no se repetirá nuevamente.

try {
      val blockId = ShuffleBlockId(dep.shuffleId, mapId, IndexShuffleBlockResolver.NOOP_REDUCE_ID)
      //作者注:将溢出的磁盘文件和当前缓存的文件进行归并合并,保证同一分区的数据连续存在
      val partitionLengths = sorter.writePartitionedFile(blockId, tmp)
      //作者注:构建索引文件
      shuffleBlockResolver.writeIndexFileAndCommit(dep.shuffleId, mapId, partitionLengths, tmp)
      mapStatus = MapStatus(blockManager.shuffleServerId, partitionLengths)
    } finally {
      if (tmp.exists() && !tmp.delete()) {
        logError(s"Error while deleting temp file ${tmp.getAbsolutePath}")
      }
    }

2. Si el lado del mapa no es un operador de agregación previa (como groupByKey)

El proceso de ejecución del barajador del operador de agregación previa se presentó anteriormente, y el proceso de ejecución del barajador del operador que no es de agregación previa es básicamente el mismo que el del operador de agregación previa. La única diferencia es que el la estructura de almacenamiento de datos no es mapa: PartitionedAppendOnlyMap, pero es búfer: PartitionedPairBuffer, el código es el siguiente:

if (shouldCombine) {
      // Combine values in-memory first using our AppendOnlyMap
      val mergeValue = aggregator.get.mergeValue
      val createCombiner = aggregator.get.createCombiner
      var kv: Product2[K, V] = null
      val update = (hadValue: Boolean, oldValue: C) => {
        if (hadValue) mergeValue(oldValue, kv._2) else createCombiner(kv._2)
      }
      while (records.hasNext) {
        addElementsRead()
        kv = records.next()
        map.changeValue((getPartition(kv._1), kv._1), update)
        maybeSpillCollection(usingMap = true)
      }
    } else {
      // Stick values into our buffer
      //作者注:非预聚合算子的数据存储到buffer中
      while (records.hasNext) {
        addElementsRead()
        val kv = records.next()
        buffer.insert(getPartition(kv._1), kv._1, kv._2.asInstanceOf[C])
        maybeSpillCollection(usingMap = false)
      }
    }

En cuanto a los procesos posteriores de desbordamiento de datos, clasificación de datos y combinación de archivos de datos de desbordamiento, son exactamente iguales que el proceso de ejecución del operador de agregación previa, y se llama al mismo proceso de ejecución, por lo que no entraré en detalles aquí.

3、shuffle之UnsafeShuffleWriter

El autor no persiguió el proceso de ejecución específico de este UnsafeShuffleWriter, porque también se puede ver en el nombre que Unsafe usa memoria fuera del montón para el almacenamiento de datos y operaciones relacionadas. El principio básico es serializar objetos de datos y almacenarlos en el montón. Memoria externa, y luego use el método binario para ordenar los datos, lo que puede mejorar el rendimiento informático.

En el proceso de ejecución real, este método es el preferido para escribir en el proceso aleatorio.Las condiciones específicas de ejecución son las siguientes:

def canUseSerializedShuffle(dependency: ShuffleDependency[_, _, _]): Boolean = {
    val shufId = dependency.shuffleId
    val numPartitions = dependency.partitioner.numPartitions
    //作者注:序列化器支持relocation.
    //作者注:目前spark提供的有两个序列化器:JavaSerializer和KryoSerializer
    //其中KryoSerializer支持relocation,而JavaSerializer不支持relocation
    if (!dependency.serializer.supportsRelocationOfSerializedObjects) {
      log.debug(s"Can't use serialized shuffle for shuffle $shufId because the serializer, " +
        s"${dependency.serializer.getClass.getName}, does not support object relocation")
      false
    } else if (dependency.mapSideCombine) { //作者注:非map端预聚合算子
      log.debug(s"Can't use serialized shuffle for shuffle $shufId because we need to do " +
        s"map-side aggregation")
      false
    } else if (numPartitions > MAX_SHUFFLE_OUTPUT_PARTITIONS_FOR_SERIALIZED_MODE) {
      //作者注:下游分区个数小于MAXIMUM_PARTITION_ID = (1 << 24) - 1
      log.debug(s"Can't use serialized shuffle for shuffle $shufId because it has more than " +
        s"$MAX_SHUFFLE_OUTPUT_PARTITIONS_FOR_SERIALIZED_MODE partitions")
      false
    } else {
      log.debug(s"Can use serialized shuffle for shuffle $shufId")
      true
    }
  }

El autor no rastreó la lógica de ejecución específica en profundidad, porque pueden ser todos los datos binarios después del rastreo, y es imposible ver la información de los datos de manera intuitiva.Si los lectores están interesados, pueden depurar y rastrear por sí mismos.

Supongo que te gusta

Origin blog.csdn.net/chenzhiang1/article/details/126834574
Recomendado
Clasificación