Explicación detallada de la gestión de la memoria Spark (Parte 2): gestión de la memoria
Cuando Task lee una partición al comienzo del inicio, primero determinará si la partición persiste. De lo contrario, debe verificar el punto de control o volver a calcular según el linaje. Por lo tanto, si desea realizar varias operaciones en un RDD, puede utilizar el método de persistencia o caché en la primera operación para conservar o almacenar en caché el RDD en la memoria o el disco, aumentando así la velocidad de cálculo de las operaciones posteriores. De hecho, el método de caché conserva el RDD en la memoria utilizando el nivel de almacenamiento predeterminado MEMORY_ONLY, por lo que el caché es un tipo especial de persistencia. El diseño de la memoria de almacenamiento dentro y fuera del montón permite una planificación y gestión unificadas de la memoria utilizada al almacenar en caché RDD (otros escenarios de aplicación de la memoria de almacenamiento, como el almacenamiento en caché de datos de transmisión, están temporalmente fuera del alcance de este artículo).
La persistencia de RDD está a cargo del módulo de almacenamiento de Spark [1], que separa el RDD del almacenamiento físico. El módulo de almacenamiento es responsable de administrar los datos generados por Spark durante el proceso de cálculo y encapsula la función de acceso local o remoto a los datos en la memoria o el disco. En la implementación específica, los módulos de almacenamiento del controlador y el ejecutor forman una arquitectura maestro-esclavo, es decir, el BlockManager del controlador es el maestro y el BlockManager del ejecutor es el esclavo. El módulo de almacenamiento utiliza lógicamente bloques como unidad de almacenamiento básica. Cada partición del RDD solo corresponde a un bloque (formato Bloque (BlockId es rdd_RDD-ID_PARTITION-ID) después del procesamiento. El Maestro es responsable de administrar y mantener los metadatos de los bloques. de toda la información de la aplicación Spark, el esclavo debe informar el estado de actualización del bloque al maestro y recibir comandos del maestro, como agregar o eliminar RDD.
Cuando persiste el RDD, Spark especifica siete. diferentes niveles de almacenamiento como MEMORY_ONLY y MEMORY_AND_DISK, el nivel de almacenamiento es una combinación de las siguientes cinco variables [2]:
A través del análisis de la estructura de datos, podemos ver que el nivel de almacenamiento define el almacenamiento. de particiones RDD (es decir, bloques) desde tres dimensiones Modo:
Antes de que el RDD se almacene en caché en la memoria de almacenamiento, generalmente se accede a los datos de la partición en la estructura de datos del iterador. en el lenguaje Scala para atravesar cada partición en el conjunto de datos. Cada elemento de datos (registro) serializado o no serializado se puede obtener a través de un iterador. Las instancias de objetos de estos registros ocupan lógicamente otra parte del espacio de memoria en el montón de JVM. y el espacio de diferentes registros en la misma partición es discontinuo. p>
Después de que el RDD se almacena en caché en la memoria de almacenamiento, las particiones se convierten en bloques, que se registran en el montón o fuera del montón y ocupan un espacio continuo. espacio en la memoria de almacenamiento El proceso por el cual Spark convierte las particiones de un espacio de almacenamiento no contiguo a un espacio de almacenamiento continuo se llama " "Expandir". Hay dos formatos de almacenamiento para bloques: serializado y no serializado, según el nivel de almacenamiento de. los bloques no serializados se definen mediante una estructura de datos de DeserializedMemoryEntry, una matriz utilizada para almacenar todos los objetos Java, y los bloques serializados se definen mediante serializedMemoryEntry Una definición de estructura de datos, ByteBuffer se utiliza para almacenar datos binarios El módulo de almacenamiento de cada uno. El ejecutor utiliza una estructura de mapa vinculada para administrar todas las instancias de objetos de bloque en la memoria de almacenamiento del montón y fuera del montón [6]. La adición de este LinkedHashMap y Deletion registra indirectamente la aplicación y la liberación de memoria. no hay garantía de que el espacio de almacenamiento pueda acomodar todos los datos en el iterador a la vez, la tarea informática actual debe solicitar suficiente espacio de expansión del MemoryManager para ocupar temporalmente el espacio. Si no hay suficiente espacio, la expansión fallará. puede continuar cuando haya suficiente espacio.
Para particiones serializadas, el espacio de expansión requerido se puede calcular directamente de forma acumulativa. La partición no serializada debe aplicarse secuencialmente durante el proceso de recorrido de registros, es decir, cada vez que se lee un registro, se muestrea y estima el espacio de expansión requerido. Cuando el espacio es insuficiente, el espacio de expansión ocupado se puede interrumpir y liberar. Si la expansión final es exitosa, el espacio de expansión ocupado por la partición actual se convertirá en el espacio de almacenamiento de un RDD en caché normal, como se muestra en la Figura 2 a continuación.
Se puede ver en la Figura 3 y la Figura 5 de Gestión de memoria de Spark (Parte 1): explicación detallada de la asignación de memoria que durante la gestión de memoria estática, Spark divide especialmente un espacio de expansión en la memoria de almacenamiento y su tamaño. es fijo de. Cuando la gestión de la memoria está unificada, el espacio de expansión no se distingue específicamente. Cuando el espacio de almacenamiento es insuficiente, se procesará de acuerdo con el mecanismo de ocupación dinámica.
Debido al espacio limitado de memoria de almacenamiento de todas las tareas informáticas del mismo ejecutor * * *, cuando hay nuevos bloques que deben almacenarse en caché pero no hay suficiente espacio restante para ocuparlos dinámicamente, los bloques antiguos en LinkedHashMap quedará en desuso. Si el nivel de almacenamiento del bloque eliminado también contiene un requisito para ser almacenado en el disco, se descartará; de lo contrario, el bloque se eliminará directamente.
Las reglas de eliminación de la memoria de almacenamiento son:
El proceso de desinstalación del disco es relativamente sencillo. Si su nivel de almacenamiento cumple con la condición de que _useDisk sea verdadero, determine si está en un formato no serializado según su _deserializado y, de ser así, serialícelo. Finalmente, los datos se almacenan en el disco y su información se actualiza en el módulo de almacenamiento.
Las tareas que se ejecutan en Executor también disfrutan de memoria de ejecución. Spark utiliza la estructura HashMap para guardar la asignación de tareas al consumo de memoria. El rango de tamaño de la memoria de ejecución que puede ocupar cada tarea es 1/2N ~ 1/N, donde N es el número de tareas que se ejecutan en el ejecutor actual. Cuando se inicia cada tarea, debe solicitar al menos 1/2N de memoria de ejecución de MemoryManager. Si no se pueden cumplir los requisitos, la tarea se bloqueará hasta que otras tareas liberen suficiente memoria de ejecución.
La memoria de ejecución se utiliza principalmente para almacenar la memoria ocupada por las tareas al ejecutar Shuffle. La mezcla aleatoria es el proceso de reparticionar datos RDD de acuerdo con ciertas reglas. Echemos un vistazo al uso de memoria durante las fases de escritura y lectura de Shuffle:
En ExternalSorter y Aggregator, Spark usará una tabla hash llamada AppendOnlyMap para almacenar datos en el montón, pero en No todos los datos durante El proceso aleatorio se puede guardar en la tabla hash. Cuando la memoria ocupada por esta tabla hash se muestrea y estima periódicamente, cuando es lo suficientemente grande y ya no es posible solicitar nueva memoria de ejecución al MemoryManager, Spark almacenará todo su contenido en un archivo de disco. Este proceso se denomina derrame y los archivos derramados en el disco finalmente se fusionan.
El tungsteno utilizado en la etapa Shuffle Write es una solución propuesta por Databricks para optimizar la memoria y el uso de CPU de Spark [4], lo que soluciona algunas limitaciones y deficiencias en el rendimiento de JVM. Spark elegirá automáticamente si desea utilizar la clasificación de tungsteno según la situación de Shuffle. El mecanismo de administración de memoria basado en páginas utilizado por Tungsten se basa en MemoryManager, es decir, Tungsten abstrae aún más el uso de la memoria de ejecución, de modo que durante el proceso Shuffle, no hay necesidad de preocuparse si los datos se almacenan en el montón o fuera. el montón. Cada página de memoria está definida por un MemoryBlock, y la dirección de la página de memoria en la memoria del sistema se identifica uniformemente mediante dos variables, Object obj y long offset. MemoryBlock en el montón es memoria asignada en forma de una matriz larga. El valor de obj es la referencia del objeto de la matriz y el desplazamiento es la dirección de desplazamiento inicial de la matriz larga en la JVM. En resumen, se puede localizar la dirección absoluta de una matriz en el montón. El MemoryBlock fuera del montón es un bloque de memoria aplicado directamente, su obj es nulo y el desplazamiento es la dirección absoluta de 64 bits de este bloque de memoria en la memoria del sistema. Spark usa inteligentemente MemoryBlock para encapsular páginas de memoria internas y externas, y usa pageTable para administrar las páginas de memoria de cada aplicación de tareas.
Toda la memoria bajo la administración de páginas de Tungsten está representada por una dirección lógica de 64 bits, que consta de un número de página y un desplazamiento dentro de la página:
A través de un método de direccionamiento unificado, Spark Un puntero a una dirección lógica de 64 bits se puede colocar en la memoria dentro o fuera del montón. Todo el proceso de clasificación Shuffle Write solo requiere punteros de clasificación y no requiere deserialización. Todo el proceso es muy eficiente y mejora significativamente la eficiencia del acceso a la memoria y la eficiencia del uso de la CPU [5].
La memoria de almacenamiento y la memoria de ejecución de Spark tienen métodos de administración completamente diferentes: para la memoria de almacenamiento, Spark usa un LinkedHashMap para administrar centralmente todos los bloques, que se convierten a partir de las particiones de RDD que deben almacenarse en caché. memoria, Spark usa AppendOnlyMap para almacenar datos durante el proceso Shuffle, e incluso los abstrae en la gestión de la memoria de la página en la clasificación de tungsteno, abriendo un nuevo mecanismo de gestión de memoria JVM.
La administración de memoria de Spark es un mecanismo complejo, la versión de Spark se actualiza rápidamente y el nivel del autor es limitado, por lo que es inevitable que haya algunas descripciones incorrectas y poco claras. Si los lectores tienen buenas sugerencias y una comprensión más profunda, envíenme sus comentarios.