Modelo de asignación de memoria de Spark y análisis de código fuente

1. Antecedentes

    En el trabajo de producción real, la chispa en el hilo se usa generalmente para ejecutar y administrar las tareas de chispa. En este momento, es inevitable que encuentre cómo escribir parámetros de configuración al enviar tareas. Por ejemplo, la empresa le asigna una cola de hilos de memoria de 10 núcleos y 200 G. ¿Cómo envía tareas por debajo de este límite general de recursos? Para responder a esta pregunta, este artículo explica el mecanismo de administración de memoria de Spark para su referencia.

2. Mecanismo de asignación de memoria estática de Spark

    El mecanismo de administración de memoria Spark se basa en la clase MemoryManager. Primero, analice el código fuente de la clase Memorymanager:

@GuardedBy("this")
protected val onHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.ON_HEAP)
@GuardedBy("this")
protected val offHeapStorageMemoryPool = new StorageMemoryPool(this, MemoryMode.OFF_HEAP)
@GuardedBy("this")
protected val onHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.ON_HEAP)
@GuardedBy("this")
protected val offHeapExecutionMemoryPool = new ExecutionMemoryPool(this, MemoryMode.OFF_HEAP)

    La memoria asignada por el Ejecutor se asigna como un todo en dos partes, que apuntan al almacenamiento (utilizado para los datos de la caché) y la ejecución (utilizado para calcular los datos), y la memoria se asigna mediante la construcción de un MemoryPool (grupo de memoria). La agrupación de memoria puede elegir dos modos durante el proceso de construcción: ON_HEAP (en el montón) y OFF_HEAP, el valor predeterminado es la asignación del montón, es decir, todos los objetos se crean en la JVM, pero la creación de demasiados objetos en el montón provocará realmente Bringing La presión del GC, el tiempo de respuesta del GC demasiado largo también conducirán a situaciones de OOM. Al mismo tiempo, cuando se escriben datos en el disco, realmente pasarán a través de la memoria fuera del montón, por lo que esta parte de la operación también tendrá pérdida de tiempo. Si lo implementa OFF_HEAP, esta parte de la memoria es administrada directamente por el kernel, lo que reduce la presión sobre la propia JVM. Spark ha introducido el proyecto Tungsten desde 1.6. Si está en modo On-heap, buscará el objeto y luego usará el desplazamiento para ubicar la dirección en el objeto, y si está Off-heap, lo ubicará directamente los datos del objeto. Mejore aún más la ventaja de velocidad de OFF_HEAP.

    MemoryManager es una clase abstracta de nivel superior. La implementación del modelo de administración de memoria antes de spark1.6 se basa en la clase StaticMemoryManager. De manera similar, echemos un vistazo a parte del código fuente:

private def getMaxStorageMemory(conf: SparkConf): Long = {
    val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    val memoryFraction = conf.getDouble("spark.storage.memoryFraction", 0.6)
    val safetyFraction = conf.getDouble("spark.storage.safetyFraction", 0.9)
    (systemMaxMemory * memoryFraction * safetyFraction).toLong
  }

  /**
   * Return the total amount of memory available for the execution region, in bytes.
   */
  private def getMaxExecutionMemory(conf: SparkConf): Long = {
    val systemMaxMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)

    if (systemMaxMemory < MIN_MEMORY_BYTES) {
      throw new IllegalArgumentException(s"System memory $systemMaxMemory must " +
        s"be at least $MIN_MEMORY_BYTES. Please increase heap size using the --driver-memory " +
        s"option or spark.driver.memory in Spark configuration.")
    }
    if (conf.contains("spark.executor.memory")) {
      val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
      if (executorMemory < MIN_MEMORY_BYTES) {
        throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
          s"$MIN_MEMORY_BYTES. Please increase executor memory using the " +
          s"--executor-memory option or spark.executor.memory in Spark configuration.")
      }
    }
    val memoryFraction = conf.getDouble("spark.shuffle.memoryFraction", 0.2)
    val safetyFraction = conf.getDouble("spark.shuffle.safetyFraction", 0.8)
    (systemMaxMemory * memoryFraction * safetyFraction).toLong
  }

    Como se muestra en el código, se implementan dos métodos, getMaxStorageMemory y getMaxExecutionMemory, que corresponden a la asignación de memoria de las secciones de Almacenamiento y Ejecución antes mencionadas, respectivamente. Runtime.getRuntime.maxMemory obtiene la memoria máxima que la máquina virtual java puede obtener del sistema operativo en tiempo de ejecución, es decir, el valor total de memoria que podemos obtener. De forma predeterminada, el 60% de la memoria total se asigna a la sección StorageMemory, StorageMemory reserva el 10% de forma predeterminada como umbral de seguridad y el 90% restante es la memoria caché del usuario. Del 90% de los recursos de memoria, el 20% se asigna a unRollMemory por defecto, que se utiliza principalmente para almacenar en caché los datos de bloque del tipo Iterator y la expansión de datos serializados. El 80% restante se utiliza para datos de caché específicos. De forma predeterminada, el 20% de la memoria total se asigna a la parte ExecutionMemory, y ExecutionMemory retiene el 20% de forma predeterminada como umbral de seguridad, y el 80% restante se utiliza específicamente para el cálculo.

    La siguiente es una imagen para mostrar la asignación de memoria real en detalle. Suponemos que la memoria total obtenida cuando el ejecutor llama a Runtime.getRuntime.maxMemory es 10G:

              

    Como se muestra en la figura, la memoria 10G del ejecutor se divide en tres bloques, 6G (StorageMemoryFraction), 2G (ExecutionMemoryFraction) y 2G están reservados para los objetos de creación de JVM. Excluyendo el umbral de seguridad establecido y unRollMemory, la memoria real asignada a la caché es 4.32G, y la memoria real asignada al cálculo es 1.6G. También se puede ver desde aquí que el mecanismo de control y administración de recursos de memoria estática usado antes de Spark 1.6 tiene mucho margen de mejora.

3. Mecanismo de asignación de memoria dinámica Spark (después de la versión 1.6)

    Después del análisis real anterior, se puede encontrar claramente que el mecanismo de administración y asignación de la memoria estática Spark es inflexible y hay mucho margen de mejora. Entonces, Spark comenzó a introducir un mecanismo de administración de memoria dinámica después de la versión 1.6. El código es implementado por la clase UnifiedMemoryManager. Veamos el código fuente a continuación:

  override def maxOnHeapStorageMemory: Long = synchronized {
    maxHeapMemory - onHeapExecutionMemoryPool.memoryUsed
  }

  override def maxOffHeapStorageMemory: Long = synchronized {
    maxOffHeapMemory - offHeapExecutionMemoryPool.memoryUsed
  }

    Se puede ver en el código que ya sea ON_HEAP u OFF_HEAP, maxStorageMemory se obtendrá restando la memoria real ocupada por ExecutionMemoryPool de la memoria total. En otras palabras, la asignación de memoria de almacenamiento y ejecución es dinámica y se puede ajustar dinámicamente dentro del rango de la memoria total.

private def getMaxMemory(conf: SparkConf): Long = {
    val systemMemory = conf.getLong("spark.testing.memory", Runtime.getRuntime.maxMemory)
    val reservedMemory = conf.getLong("spark.testing.reservedMemory",
      if (conf.contains("spark.testing")) 0 else RESERVED_SYSTEM_MEMORY_BYTES)
    val minSystemMemory = (reservedMemory * 1.5).ceil.toLong
    if (systemMemory < minSystemMemory) {
      throw new IllegalArgumentException(s"System memory $systemMemory must " +
        s"be at least $minSystemMemory. Please increase heap size using the --driver-memory " +
        s"option or spark.driver.memory in Spark configuration.")
    }
    // SPARK-12759 Check executor memory to fail fast if memory is insufficient
    if (conf.contains("spark.executor.memory")) {
      val executorMemory = conf.getSizeAsBytes("spark.executor.memory")
      if (executorMemory < minSystemMemory) {
        throw new IllegalArgumentException(s"Executor memory $executorMemory must be at least " +
          s"$minSystemMemory. Please increase executor memory using the " +
          s"--executor-memory option or spark.executor.memory in Spark configuration.")
      }
    }
    val usableMemory = systemMemory - reservedMemory
    val memoryFraction = conf.getDouble("spark.memory.fraction", 0.6)
    (usableMemory * memoryFraction).toLong
  }

    La memoryFraction se obtiene a través del método getMaxMemory, que es el valor máximo de memoria utilizado por el ejecutor para Ejecución y Almacenamiento. Analicemos en detalle cómo se calcula este valor:

  1. systemMemory = Runtime.getRuntime.maxmemory es el valor máximo de memoria que puede obtener JVM
  2. ReservadoMemory = RESERVED_SYSTEM_MEMORY_BYTES, si se activa el modo "spark.testing", es 0 (correspondiente al modo local), de lo contrario es un valor fijo de 300M. Este valor se puede encontrar en el código fuente:
private val RESERVED_SYSTEM_MEMORY_BYTES = 300 * 1024 * 1024

     3.minSystemMemory = reservedMemory * 1.5 = 450M. Este valor se juzgará en el código fuente. Si systemMemory es menor que 450M, se informará un error directamente. Si se especifica que la memoria del ejecutor sea inferior a 450M al enviar una tarea de chispa, también se informará un error.

     4. usableMemory = systemMemory-reservedMemory (300M) es la cantidad total de memoria reservada para planificar

     5. El valor de retorno general de la función es usableMemory * memoryFraction predeterminado a usableMemory * 0.6. Tenga en cuenta que la versión de prueba real del autor es spark2.2.0. El valor de la fracción del código fuente es 0.6, pero el valor predeterminado de la fracción de la versión anterior puede ser 0,75. El 40% restante está ocupado por objetos de creación de JVM.

     Aquí hay una imagen en línea:

    Tomemos una castaña a continuación: Si la memoria del ejecutor es 10G, la cantidad total de memoria reservada para Ejecución y Almacenamiento por defecto es (10 * 1024-300) * 0.6 = 5964M. Estos 5 gigabytes de memoria son ocupados dinámicamente por Ejecución y Almacenamiento, si su propio espacio es insuficiente, pueden ocupar el uno al otro.

def apply(conf: SparkConf, numCores: Int): UnifiedMemoryManager = {
  val maxMemory = getMaxMemory(conf)
  new UnifiedMemoryManager(
    conf,
    maxHeapMemory = maxMemory,
    onHeapStorageRegionSize =
      (maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong,
    numCores = numCores)
}

    La ejecución y el almacenamiento representan cada uno el 50% durante la inicialización 

Agregue el mecanismo de ajuste dinámico de la memoria de almacenamiento y ejecución:

1. Cuando la memoria caché (almacenamiento) es insuficiente, si queda memoria de ejecución, se puede ocupar. Pero la memoria ocupada por la ejecución no se liberará a la caché.

2. Cuando la memoria de ejecución es insuficiente, si queda memoria caché, se puede ocupar, si la memoria caché está llena, se liberarán algunas particiones con niveles bajos de caché.

Después de comprender el mecanismo de asignación de memoria de Spark, nos centraremos en cómo configurar los parámetros de la tarea Spark en el próximo artículo.

 

Supongo que te gusta

Origin blog.csdn.net/weixin_36714575/article/details/81662718
Recomendado
Clasificación