El lenguaje de consulta N1QL ahora tiene una cuota de memoria por petición

A diferencia de otros servicios, el lenguaje de consulta SQL hasta ahora no ha tenido la opción de ajustar su huella de memoria. Hasta ahora.

Con el lanzamiento de Couchbase Server 7.0El servicio de consulta incluye ahora una cuota de memoria por petición.

Fondo

La razón principal de esta disparidad entre SQL++ (antes N1QL) y otros servicios de Couchbase se reduce a un simple hecho: mientras que la mayor parte del consumo de memoria de servicios como Data o Índice es caché -y cada vez que entran nuevos documentos- siempre hay algo que se puede desalojar en caso de que haya mucha demanda de espacio. El grueso de servicio de consulta se basan en valores transitorios (ya sean documentos obtenidos o valores calculados) que comienzan su vida en una etapa de una solicitud individual y luego expiran antes de que termine la solicitud.

En SQL++ no hay nada que desalojar y sustituir. No hay que hacer equilibrios para mantener el reloj en marcha. Si no hay recursos disponibles, la única opción es el fracaso.

Además, partes del EventosÍndice y Búsqueda de texto completo se ejecutan dentro del servicio de consulta. Utilizan recursos de memoria de SQL++, pero SQL++ no tiene control sobre ellos.

Aunque el consumo de memoria de SQL++ no es un problema en general - Las peticiones cargan y descartan documentos rápidamente y el mundo es un lugar feliz. De vez en cuando, aparece alguna petición codiciosa que estropea el juego para todos. Había que solucionar este problema para Couchbase usuarios.

Pero dejemos de lado (por el momento) los componentes sobre los que SQL++ no tiene control, y consideremos si un conjunto de valores transitorios en todo el nodo sería siquiera deseable.

El funcionamiento de una reserva de valores de este tipo podría ser el siguiente: Cada vez que una petición necesita un valor, asigna el tamaño correspondiente de la reserva global, y en cuanto ha terminado con él, lo devuelve a la reserva. Cuando se agota la memoria, todas las asignaciones fallan hasta que se libera memoria suficiente.

Pero, ¿qué ocurre cuando aparece una petición codiciosa? Coge todo lo que puede y no lo suelta. ¿Y el resto de peticiones frugales? Sin desalojos posibles, la única opción es el fracaso. Las demás peticiones terminan una a una -por error y con error- hasta que la culpable finalmente fracasa.

Este comportamiento es similar al del profesor que envía a toda la clase al despacho del director después de que le hayan pegado con una tiza, en lugar de investigar y enviar sólo al culpable.

Al servicio de consultas le han salido ojos en la nuca y puede ver quién lanzó la tiza.

Introduzca la cuota de memoria por petición

Cuando se activa la cuota de memoria por petición, cada petición de consulta obtiene su propia reserva. El seguimiento de la memoria funciona como de costumbre, pero ahora, cuando el pool se agota, es sólo el culpable que falla.

"Pero un ajuste en todo el nodo sería mucho más práctico". Te oigo decir. Y tiene razón.

Hemos implementado uno en sigilo: el servicio de consulta permite un número fijo de peticiones en ejecución al mismo tiempo. Esta opción está controlada por el parámetro servicers y por defecto es cuatro veces el número de núcleos del nodo de consulta. La cuota global de memoria del nodo equivale al número de servidores multiplicado por la cuota por petición.

Las dos cuotas están íntimamente entrelazadas: la cuota por solicitud es explícita porque queríamos dejar claro que lo que se controla son las solicitudes individuales, no el nodo en su totalidad.

¿Cómo se utiliza?

Hay dos ajustes para la cuota de memoria por petición en SQL++:

    • En /admin/configuración parámetro REST del nodo cuota de memoria
    • En /consulta/servicio parámetro REST de la solicitud cuota_memoria

Estos ajustes expresan en megabytes la cantidad máxima de memoria que una petición puede utilizar en un momento dado.

Por defecto cuota de memoria es cero, es decir, la cuota de memoria está desactivada. La dirección cuota_memoria tiene prioridad sobre la configuración del nodo, siempre que el valor solicitado no la supere.

Veamos algunos ejemplos. Este comando establece su cuota de memoria a 10MB para todo el nodo y replica la configuración a todos sus otros nodos:

Y este comando establece su cuota de memoria a 10 MB para la solicitud única, como se puede ver a continuación:

Por último, esta de abajo establece su cuota de memoria a 10 MB para la duración de la cbq sesión:

Respuestas y espacios clave del sistema

Si su cuota de memoria está fijada (por cualquier medio), entonces varias funciones de SQL++ pueden contener información adicional, incluyendo:

    • Métricas
    • Controla
    • Espacios de claves del sistema

Analicemos más detenidamente cada una de estas respuestas.

Respuestas: Métricas

La sección de métricas de la respuesta contiene un memoriautilizada que muestra la cantidad de memoria del documento utilizada para ejecutar la solicitud.

Si no se utiliza memoria de documento, se omite esta métrica. La misma omisión se produce con mutaciones o errorCount también.

Respuestas: Controla

La sección de controles de la respuesta también informa de la cuota de memoria según su configuración. Esto es lo que parece:

Espacios de claves del sistema

Espacios de claves del sistema - ambos sistema:solicitudes_activas y sistema:solicitudes_realizadas - también contienen información sobre la cuota de memoria. La dirección memoriautilizada y memoryQuota también aparece aquí. Eche un vistazo al siguiente ejemplo:

¿Cómo se utiliza la memoria?

Antes de profundizar en algunos de los mecanismos de la operación de cuota de memoria, probablemente deberíamos aprender un poco acerca de cómo una solicitud utiliza la memoria.

Como probablemente ya habrá adivinado, el memoriautilizada se ha introducido para medir los requisitos de memoria de una sentencia individual antes de permitir su ejecución. Hagamos un par de experimentos y veamos cómo se comporta, empezando por éste:

Como puede ver arriba, la memoria utilizada no es el tamaño del conjunto de resultados.

Intentémoslo de nuevo, pero esta vez sin formatear. De este modo, el tamaño del conjunto de resultados será lo más parecido posible al tamaño de los datos almacenados:

Tampoco es el tamaño de los datos obtenidos.

Intentemos eliminar el coste de mostrar los resultados en la pantalla:

Misma consulta, mismo formato, pero diferente almacenamiento y, sin embargo, diferente cantidad de memoria utilizada.

Para llevar: Para algunos tipos de sentencias, el consumo de memoria depende más de las circunstancias de esa ejecución concreta que de la propia sentencia.

Operación de la fase de ejecución de la solicitud

La fase de ejecución de una solicitud emplea una cadena de operadores que se ejecutan en paralelo. Cada operador recibe valores de la fase anterior, los procesa y los envía a la siguiente.

La infraestructura de intercambio de valores entre operadores incluye una cola de valores para que cada operador no se vea bloqueado por el anterior o el siguiente. (De hecho, el motor de ejecución es más complicado. Algunos operadores están integrados en otros y algunos sólo existen para llevar a cabo tareas de orquestación, por lo que las colas de valores no siempre están implicadas, pero aún así).

Por ejemplo, una consulta simple como ésta:

utiliza un Exploración de índices para producir claves, que se envían a un Visite para recuperar documentos de la caché clave-valor, que se envían a un archivo Filtro para excluir los documentos que no proceden, y los que sí proceden se envían a un Proyección para extraer los campos y convertirlos en JSON (si es necesario) y, finalmente, pasarlos a un archivo Corriente que los devuelve al cliente.

Los valores que completan el curso se eliminan finalmente durante la recogida de basura.

En el ejemplo anterior, si hubiera núcleos disponibles para ejecutar todos estos operadores en paralelo -y todos los operadores se ejecutaran exactamente a la misma velocidad-, nunca habría más de cinco documentos atravesando el canal en un momento dado, aunque la solicitud pudiera procesar cualquier número de documentos.

Por supuesto, un Escanear podría producir llaves mucho más rápido que un Visite podría reunir documentos y el marshalling podría ser costoso. El envío de resultados por cable de vuelta al cliente podría ser lento, por lo que incluso si hay núcleos disponibles, las colas descritas anteriormente se utilizarán como búferes para los valores que esperan ser procesados a lo largo de la línea. A su vez, esto aumenta temporalmente la cantidad de memoria que necesita una petición para procesar la secuencia de valores entrantes.

Este patrón explica por qué tanto hacer el Proyección más eficiente (pretty=false), o Corriente (enviar a un archivo en lugar de al terminal) tiene un efecto beneficioso sobre el consumo de memoria: operadores más rápidos significan menos valores atascados en las colas de intercambio de valores.

Con el aumento de la carga de peticiones, el núcleo SQL++ tiene que programar más operadores, lo que significa que mientras no se ejecutan, la cola de valores del operador anterior aumenta de tamaño, lo que significa que se necesita aún más memoria para procesar las peticiones individuales. En total, los nodos cargados utilizan más memoria que los que tienen poca actividad.

La operación de cuota de memoria

A efectos de la discusión anterior, he ignorado todos aquellos casos en los que la memoria crece sin que se intercambien valores: los hash JOINs, ORDER BYs y GROUP BYs son algunos ejemplos que me vienen a la mente.

Estos casos particulares son tratados por el primer modo de funcionamiento de Memory Quota: el buffer de ordenación, agregado o hash crece por encima de un umbral específico, memory quota lanza un error y la petición falla.

Sin embargo, como hemos visto, hay una serie de circunstancias que hacen que el consumo de memoria crezca sin culpa por parte de la petición.

En estos casos, la función Cuota de memoria emplea técnicas para intentar controlar el uso de la memoria y ayudar a que las peticiones se completen sin necesitar excesivos recursos.

El latido del consumidor

Un gasoducto funciona bien si tanto los productores como los consumidores avanzan al mismo ritmo.

Si el productor no se ejecuta, la solicitud se detiene. Sin embargo, si el consumidor no se ejecuta, no sólo se detiene la solicitud, sino que la cola de valores del productor también aumenta de tamaño.

Para contrarrestar esta posibilidad, el operador consumidor está equipado con un heartbeat (latido del corazón), que es controlado por el productor. Cuando el consumidor está a la espera pero no intenta recibir valores tras un número determinado de operaciones de envío con éxito por parte del productor, éste cederá hasta que el consumidor consiga ejecutarse.

Este enfoque no es una ciencia exacta, ya que por desgracia el lenguaje utilizado para desarrollar SQL++ no permite ceder a operadores específicos. Pero funciona como un esfuerzo cooperativo: si suficientes productores ceden, todos los consumidores tendrán una oportunidad justa de disponer del tiempo del núcleo, lo que significa que el uso de memoria debería disminuir de forma natural.

La cuota por operador

Dado que el rendimiento no es una ciencia exacta, los operadores individuales pueden acumular un uso sustancial de memoria incluso cuando los consumidores consiguen ejecutarse de vez en cuando, porque los consumidores individuales siguen teniendo menos tiempo de kernel que sus productores.

Para hacer frente a esta disparidad, SQL++ también dispone de una reserva de memoria por productor. Cuando esta reserva se agota, un productor cede (y no falla), reanudando las operaciones cuando el consumidor recibe un valor.

Esta cesión hace que los productores anteriores agoten su propia reserva y cedan, permitiendo así que toda la solicitud avance sin consumir toda la reserva de solicitudes, posiblemente (pero no necesariamente) a expensas del rendimiento.

Trucos varios

Hasta este punto, las consultas han confiado en el recolector de basura para devolver la memoria de valores al montón, y el gestor de memoria asigna estructuras de valores.

Como parte del esfuerzo de seguimiento de la memoria, hemos introducido técnicas para marcar la memoria como no utilizada antes de que el propio recolector de basura obtenga tiempo de CPU y consiga procesar todos los valores no utilizados pendientes.

También existen pequeños pools ad hoc para almacenar algunas estructuras de valores no utilizadas, ya asignadas y disponibles para su reutilización, de modo que el recolector de basura no tenga que ejercitarse una y otra vez para tipos específicos de asignación de memoria dinámica y, en su lugar, quede libre para procesar la memoria que importa.

Conclusión

Antes de la versión 7.0 de Couchbase Server, el servicio de consulta tenía un historial de ser un poco laissez faire con el uso de memoria por petición. Ahora tiene un conjunto claro de zanahorias y palos para mantener el uso de memoria bajo control. Espero que le sirva de algo.

No se limite a leer sobre ello; pruébelo usted mismo:
Descargar Couchbase Server 7

 

Autor

Publicado por Marco Greco, Arquitecto de software, Couchbase

En su vida anterior, Marco fue director de tecnología, radiofísico, arquitecto de software, administrador de sistemas, administrador de bases de datos, formador y manitas en general en la mayor clínica de radioterapia de Italia. Tras cambiar de carrera y de país, pasó más de dos décadas en varios puestos de soporte y desarrollo en Informix primero e IBM después, antes de finalmente dar el paso y unirse a Couchbase, para ayudarles a convertir N1QL en oro. Es titular de varias patentes y autor de sus propios proyectos de código abierto.

Dejar una respuesta

¿Listo para empezar con Couchbase Capella?

Empezar a construir Lorem

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor venium quis.

Únase a la Comunidad

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor venium quis.

Llamada de descargas

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor venium quis.