paint-brush
Diga adiós a los bloqueos de OOMpor@shirleyfromapachedoris
763 lecturas
763 lecturas

Diga adiós a los bloqueos de OOM

por Shirley H.11m2023/06/12
Read on Terminal Reader

Demasiado Largo; Para Leer

Una solución de administración de memoria más robusta y flexible con optimizaciones en la asignación de memoria, seguimiento de memoria y límite de memoria.
featured image - Diga adiós a los bloqueos de OOM
Shirley H. HackerNoon profile picture

¿Qué garantiza la estabilidad del sistema en tareas de consulta de datos grandes? Es un mecanismo efectivo de asignación y monitoreo de memoria. Es la forma de acelerar el cálculo, evitar puntos de acceso de memoria, responder rápidamente a la memoria insuficiente y minimizar los errores OOM.




Desde la perspectiva de un usuario de la base de datos, ¿cómo sufre una mala gestión de la memoria? Esta es una lista de cosas que solían molestar a nuestros usuarios:


  • Los errores de OOM hacen que los procesos de back-end se bloqueen. Para citar a uno de los miembros de nuestra comunidad: Hola, Apache Doris, está bien ralentizar las cosas o fallar en algunas tareas cuando te falta memoria, pero lanzar un tiempo de inactividad simplemente no es bueno.


  • Los procesos de back-end consumen demasiado espacio de memoria, pero no hay forma de encontrar la tarea exacta a la que culpar o limitar el uso de memoria para una sola consulta.


  • Es difícil establecer un tamaño de memoria adecuado para cada consulta, por lo que es probable que una consulta se cancele incluso cuando hay mucho espacio en la memoria.


  • Las consultas de alta simultaneidad son desproporcionadamente lentas y los puntos de acceso de memoria son difíciles de localizar.


  • Los datos intermedios durante la creación de HashTable no se pueden vaciar en los discos, por lo que las consultas de unión entre dos tablas grandes a menudo fallan debido a OOM.


Afortunadamente, esos días oscuros quedaron atrás porque hemos mejorado nuestro mecanismo de administración de memoria de abajo hacia arriba. Ahora prepárate; las cosas van a ser intensas.

Asignación de memoria

En Apache Doris, tenemos una única interfaz para la asignación de memoria: Allocator . Hará los ajustes que considere apropiados para mantener el uso de la memoria eficiente y bajo control.


Además, los MemTrackers están en su lugar para rastrear el tamaño de la memoria asignada o liberada, y tres estructuras de datos diferentes son responsables de la gran asignación de memoria en la ejecución del operador (las abordaremos de inmediato).




Estructuras de datos en la memoria

Como diferentes consultas tienen diferentes patrones de puntos de acceso de memoria en ejecución, Apache Doris proporciona tres estructuras de datos en memoria diferentes: Arena , HashTable y PODArray . Todos ellos están bajo el reinado del Asignador.



  1. Arena

Arena es un grupo de memoria que mantiene una lista de fragmentos, que deben asignarse a pedido del asignador. Los fragmentos admiten la alineación de la memoria. Existen a lo largo de la vida útil de la Arena y se liberarán después de la destrucción (generalmente cuando se complete la consulta).


Los fragmentos se utilizan principalmente para almacenar los datos serializados o deserializados durante Shuffle, o las claves serializadas en HashTables.


El tamaño inicial de un fragmento es de 4096 bytes. Si el fragmento actual es más pequeño que la memoria solicitada, se agregará un nuevo fragmento a la lista.


Si el fragmento actual es más pequeño que 128M, el nuevo fragmento duplicará su tamaño; si es más grande que 128M, el nuevo trozo será, como mucho, 128M más grande de lo que se requiere.


El fragmento pequeño anterior no se asignará para nuevas solicitudes. Hay un cursor para marcar la línea divisoria entre los fragmentos asignados y los no asignados.


  1. Tabla de picadillo

Las HashTables son aplicables para Hash Joins, agregaciones, operaciones de conjuntos y funciones de ventana. La estructura PartitionedHashTable no admite más de 16 sub-HashTables. También admite la fusión paralela de HashTables y cada sub-Hash Join se puede escalar de forma independiente.


Estos pueden reducir el uso general de la memoria y la latencia causada por el escalado.


Si el HashTable actual es más pequeño que 8M, se escalará por un factor de 4;

Si es más grande que 8M, se escalará por un factor de 2;

Si es más pequeño que 2G, se escalará cuando esté lleno al 50 %;

y si es más grande que 2G, se escalará cuando esté lleno al 75 %.


Las HashTables recién creadas se escalarán previamente en función de la cantidad de datos que vayan a tener. También proporcionamos diferentes tipos de HashTables para diferentes escenarios. Por ejemplo, para agregaciones, puede aplicar PHmap.


  1. matriz PODA

PODArray, como sugiere el nombre, es una matriz dinámica de POD. La diferencia entre este y std::vector es que PODArray no inicializa los elementos. Admite alineación de memoria y algunas interfaces de std::vector .


Se escala por un factor de 2. En la destrucción, en lugar de llamar a la función destructora para cada elemento, libera la memoria de todo el PODArray. PODArray se usa principalmente para guardar cadenas en columnas y es aplicable en muchos cálculos de funciones y filtrado de expresiones.

interfaz de memoria

Como la única interfaz que coordina Arena, PODArray y HashTable, el asignador ejecuta la asignación de asignación de memoria (MMAP) para solicitudes de más de 64 millones.


Los menores a 4K se asignarán directamente desde el sistema a través de malloc/free; y los que están en el medio serán acelerados por un ChunkAllocator de almacenamiento en caché de uso general, que brinda un aumento del rendimiento del 10% según nuestros resultados de evaluación comparativa.


ChunkAllocator intentará recuperar un fragmento del tamaño especificado de la FreeList del núcleo actual sin bloqueo; si tal fragmento no existe, lo intentará desde otros núcleos de una manera basada en bloqueo; si eso aún falla, solicitará el tamaño de memoria especificado del sistema y lo encapsulará en un fragmento.


Elegimos Jemalloc sobre TCMalloc después de experimentar ambos. Probamos TCMalloc en nuestras pruebas de alta concurrencia y notamos que Spin Lock en CentralFreeList ocupaba el 40 % del tiempo total de consulta.


La desactivación de la "desactivación agresiva de la memoria" mejoró las cosas, pero eso trajo mucho más uso de la memoria, por lo que tuvimos que usar un subproceso individual para reciclar el caché regularmente. Jemalloc, por otro lado, fue más eficiente y estable en consultas de alta concurrencia.


Después de realizar ajustes para otros escenarios, ofreció el mismo rendimiento que TCMalloc pero consumió menos memoria.

Reutilización de memoria

La reutilización de memoria se ejecuta ampliamente en la capa de ejecución de Apache Doris. Por ejemplo, los bloques de datos se reutilizarán durante la ejecución de una consulta. Durante Shuffle, habrá dos bloques en el extremo del remitente y funcionan alternativamente, uno recibiendo datos y el otro en transporte RPC.


Al leer una tableta, Doris reutilizará la columna predicada, implementará la lectura cíclica, filtrará, copiará los datos filtrados en el bloque superior y luego borrará.


Al ingerir datos en una tabla de clave agregada, una vez que la MemTable que almacena en caché los datos alcanza un tamaño determinado, se agregará previamente y luego se escribirán más datos.


La reutilización de la memoria también se ejecuta en el escaneo de datos. Antes de que comience el escaneo, se asignará una cantidad de bloques libres (dependiendo de la cantidad de escáneres e hilos) a la tarea de escaneo.


Durante la programación de cada escáner, uno de los bloques libres pasará a la capa de almacenamiento para la lectura de datos.


Después de la lectura de datos, el bloque se colocará en la cola del productor para el consumo de los operadores superiores en el cálculo posterior. Una vez que un operador superior haya copiado los datos de cálculo del bloque, el bloque volverá a los bloques libres para la próxima programación del escáner.


El subproceso que asigna previamente los bloques libres también será responsable de liberarlos después del escaneo de datos, por lo que no habrá gastos generales adicionales. La cantidad de bloques libres determina de alguna manera la concurrencia del escaneo de datos.

Seguimiento de memoria

Apache Doris usa MemTrackers para realizar un seguimiento de la asignación y liberación de memoria mientras analiza los puntos de acceso de memoria. Los MemTrackers mantienen registros de cada consulta de datos, ingesta de datos, tarea de compactación de datos y el tamaño de la memoria de cada objeto global, como Cache y TabletMeta.


Admite tanto el conteo manual como el seguimiento automático de MemHook. Los usuarios pueden ver el uso de la memoria en tiempo real en el backend de Doris en una página web.

Estructura de MemTrackers

El sistema MemTracker antes de Apache Doris 1.2.0 estaba en una estructura de árbol jerárquico, que constaba de process_mem_tracker, query_pool_mem_tracker, query_mem_tracker, instance_mem_tracker, ExecNode_mem_tracker, etc.


Los MemTrackers de dos capas vecinas tienen una relación padre-hijo. Por lo tanto, cualquier error de cálculo en un MemTracker secundario se acumulará hasta el final y dará como resultado una mayor escala de incredulidad.



En Apache Doris 1.2.0 y posteriores, simplificamos mucho la estructura de MemTrackers. Los MemTrackers solo se dividen en dos tipos según sus roles: MemTracker Limiter y los demás.


MemTracker Limiter, que monitorea el uso de la memoria, es único en cada tarea de consulta/ingestión/compactación y objeto global; mientras que los otros MemTrackers rastrean los puntos de acceso de la memoria en la ejecución de consultas, como HashTables en las funciones Unir/Agregación/Ordenar/Ventana y datos intermedios en la serialización, para dar una idea de cómo se usa la memoria en diferentes operadores o proporcionar una referencia para el control de la memoria en vaciado de datos.


La relación padre-hijo entre MemTracker Limiter y otros MemTrackers solo se manifiesta en la impresión de instantáneas. Puedes pensar en tal relación como un vínculo simbólico. No se consumen al mismo tiempo, y el ciclo de vida de uno no afecta al del otro.


Esto hace que sea mucho más fácil para los desarrolladores entenderlos y usarlos.


Los MemTrackers (incluido MemTracker Limiter y los demás) se colocan en un grupo de mapas. Permiten a los usuarios imprimir instantáneas generales de tipo MemTracker, instantáneas de tareas de consulta/carga/compactación y averiguar la consulta/carga con el mayor uso de memoria o el mayor uso excesivo de memoria.



Cómo funciona MemTracker

Para calcular el uso de memoria de una determinada ejecución, se agrega un MemTracker a una pila en Thread Local del hilo actual. Al recargar malloc/free/realloc en Jemalloc o TCMalloc, MemHook obtiene el tamaño real de la memoria asignada o liberada y lo registra en Thread Local del hilo actual.


Cuando finaliza una ejecución, el MemTracker relevante se eliminará de la pila. En la parte inferior de la pila se encuentra MemTracker, que registra el uso de la memoria durante todo el proceso de ejecución de consulta/carga.


Ahora, déjame explicarte con un proceso simplificado de ejecución de consultas.


  • Después de que se inicie un nodo backend de Doris, el uso de memoria de todos los subprocesos se registrará en Process MemTracker.


  • Cuando se envía una consulta, se agregará un MemTracker de consulta a la pila de almacenamiento local de subprocesos (TLS) en el subproceso de ejecución del fragmento.


  • Una vez que se programa un ScanNode, se agregará un ScanNode MemTracker a la pila de almacenamiento local de subprocesos (TLS) en el subproceso de ejecución del fragmento. Luego, cualquier memoria asignada o liberada en este subproceso se registrará tanto en Query MemTracker como en ScanNode MemTracker.


  • Después de programar un escáner, se agregarán un Query MemTracker y un Scanner MemTracker a la pila TLS del subproceso del escáner.


  • Cuando finalice el escaneo, se eliminarán todos los MemTrackers en la pila TLS del subproceso del escáner. Cuando finalice la programación de ScanNode, ScanNode MemTracker se eliminará del subproceso de ejecución de fragmentos. Luego, de manera similar, cuando se programa un nodo de agregación, se agregará un MemTracker de AggregationNode a la pila TLS del subproceso de ejecución de fragmentos y se eliminará una vez finalizada la programación.


  • Si se completa la consulta, Query MemTracker se eliminará de la pila TLS del subproceso de ejecución de fragmentos. En este punto, esta pila debería estar vacía. Luego, desde QueryProfile, puede ver el uso máximo de memoria durante toda la ejecución de la consulta, así como cada fase (escaneo, agregación, etc.).



Cómo usar MemTracker

La página web backend de Doris demuestra el uso de la memoria en tiempo real, que se divide en tipos: consulta/carga/compactación/global. Se muestran el consumo de memoria actual y el consumo máximo.



Los tipos globales incluyen MemTrackers of Cache y TabletMeta.



Desde los tipos de consulta, puede ver el consumo de memoria actual y el consumo máximo de la consulta actual y los operadores que involucra (puede saber cómo están relacionados a partir de las etiquetas). Para obtener estadísticas de memoria de consultas históricas, puede consultar los registros de auditoría de Doris FE o los registros de BE INFO.



Limite de memoria

Con el seguimiento de memoria ampliamente implementado en los backends de Doris, estamos un paso más cerca de eliminar OOM, la causa del tiempo de inactividad del backend y las fallas de consultas a gran escala. El siguiente paso es optimizar el límite de memoria en consultas y procesos para mantener el uso de la memoria bajo control.

Límite de memoria en consulta

Los usuarios pueden poner un límite de memoria en cada consulta. Si se supera ese límite durante la ejecución, la consulta se cancelará. Pero desde la versión 1.2, hemos permitido la sobreasignación de memoria, que es un control de límite de memoria más flexible.


Si hay suficientes recursos de memoria, una consulta puede consumir más memoria que el límite sin cancelarse, por lo que los usuarios no tienen que prestar atención adicional al uso de la memoria; si no los hay, la consulta esperará hasta que se asigne nuevo espacio de memoria, solo cuando la memoria recién liberada no sea suficiente para la consulta, se cancelará la consulta.


Mientras que en Apache Doris 2.0, nos dimos cuenta de la seguridad excepcional para las consultas. Eso significa que cualquier asignación de memoria insuficiente hará que la consulta se cancele de inmediato, lo que evita la molestia de verificar el estado "Cancelar" en los pasos posteriores.

Límite de memoria en el proceso

De forma regular, el backend de Doris recupera la memoria física de los procesos y el tamaño de memoria disponible actualmente del sistema. Mientras tanto, recopila instantáneas de MemTracker de todas las tareas de consulta/carga/compactación.


Si un proceso de back-end excede su límite de memoria o no hay memoria suficiente, Doris liberará algo de espacio en la memoria al borrar el caché y cancelar una serie de consultas o tareas de ingesta de datos. Estos serán ejecutados por un subproceso de GC individual con regularidad.



Si la memoria del proceso consumida supera el SoftMemLimit (81 % de la memoria total del sistema, de forma predeterminada) o la memoria del sistema disponible cae por debajo de la marca de agua de advertencia (menos de 3,2 GB), se activará el GC secundario .


En este momento, la ejecución de la consulta se pausará en el paso de asignación de memoria, los datos almacenados en caché en las tareas de ingestión de datos se borrarán a la fuerza y se liberarán parte de la caché de la página de datos y la caché de segmentos obsoleta.


Si la memoria recién liberada no cubre el 10 % de la memoria del proceso, con la sobreasignación de memoria habilitada, Doris comenzará a cancelar las consultas que son las que más "sobreasignan" hasta que se alcance el objetivo del 10 % o se cancelen todas las consultas.


Luego, Doris acortará el intervalo de verificación de la memoria del sistema y el intervalo del GC. Las consultas continuarán cuando haya más memoria disponible.


Si la memoria de proceso consumida supera el MemLimit (90 % de la memoria total del sistema, de forma predeterminada) o la memoria del sistema disponible cae por debajo de la marca de límite inferior (menos de 1,6 GB), se activará el GC completo .


En este momento, se detendrán las tareas de ingesta de datos y se liberará toda la memoria caché de la página de datos y la mayoría de las demás memorias caché.


Si, después de todos estos pasos, la memoria recién liberada no cubre el 20% de la memoria del proceso, Doris buscará en todos los MemTrackers y encontrará las consultas y tareas de ingestión que consumen más memoria, y las cancelará una por una.


Solo después de que se alcance el objetivo del 20 %, se extenderá el intervalo de verificación de la memoria del sistema y el intervalo del GC, y continuarán las consultas y las tareas de ingestión. (Una operación de recolección de elementos no utilizados suele tardar entre cientos de μs y decenas de ms).

Influencias y resultados

Después de las optimizaciones en la asignación de memoria, el seguimiento de la memoria y el límite de memoria, hemos aumentado sustancialmente la estabilidad y el rendimiento de alta concurrencia de Apache Doris como plataforma de almacenamiento de datos analíticos en tiempo real. El bloqueo de OOM en el backend es una escena rara ahora.


Incluso si hay un OOM, los usuarios pueden ubicar la raíz del problema según los registros y luego solucionarlo. Además, con límites de memoria más flexibles para consultas e ingesta de datos, los usuarios no tienen que hacer un esfuerzo adicional para cuidar la memoria cuando el espacio de memoria es adecuado.


En la próxima fase, planeamos asegurar la finalización de las consultas en la sobreasignación de memoria, lo que significa que se tendrán que cancelar menos consultas debido a la escasez de memoria.


Hemos dividido este objetivo en direcciones de trabajo específicas: seguridad de excepción, aislamiento de memoria entre grupos de recursos y el mecanismo de vaciado de datos intermedios.


Si quieres conocer a nuestros desarrolladores, aquí es donde nos encuentras .