¿Estás visitando desde Argentina?
Ingresá a Linware Argentina ⯈
Continuar en Linware Argentina ⯈
×
¿Qué estás buscando?
BUSCAR!
BLOG
Mejora de la asignación de memoria con el cliente de salida Libbeat y Elasticsearch
Publicada el 22/02/2024

El viaje para mejorar el desempeño no está completo sin sumergirnos en el camino que siguen los eventos hacia su destino. Cuando Filebeat o cualquier otro ritmo publica eventos en un Elasticsearch® remoto, depende de su API _bulk . Esto significa que Beats no solo realiza solicitudes a Elasticsearch, sino que también analiza sus respuestas, lo que como resultado aumenta la utilización de la memoria. 

Anteriormente, se mostro cómo lograr reducir la asignación de Beats al reducir el uso de Logger en la ruta activa del evento, lo que redujo la memoria asignada general.

Investigación

Antes de poder resolver cualquier problema de rendimiento, primero debemos comprender la línea de base. Para eso, comenzamos analizando un pprof del conjunto de pruebas comparativas y nos centramos en la asignación de memoria de la función PublishEvent. Esta función es responsable de enviar la solicitud _bulk y analizar la respuesta de Elasticsearch.

Al observar el método PublishEvents, vimos que asigna 5 MB directamente y ~41 GB de memoria indirecta de otros métodos. Con base en estos resultados, comenzamos analizando esas asignaciones indirectas y de dónde provienen. 

Observando la ruta del código y las funciones involucradas, vimos que ejecuta la llamada HTTP _bulk, para luego consumir la respuesta usando el io.ReadAll . Al seguir la definición de esa función , vimos una advertencia de obsolescencia.

Deprecated: As of Go 1.16, this function simply calls io.ReadAll.
 
Esta fue la primera sugerencia de mejora para abordar, reemplazando la función obsoleta. Se comenzo creando un punto de referencia de Go para medir el rendimiento de execHttpRequest. La metodología fue crear una reproducción de referencia de Go y crear una línea de base para evaluar el impacto de eliminar la función obsoleta.

El siguiente paso es abordar la forma en que consumimos la respuesta misma. Dado que la respuesta es algo que sucederá en el contexto de NewConnection, tendría sentido reutilizar el mismo búfer para consumir esa respuesta en comparación con llamar previamente a io.ReadAll para cada respuesta. De esta manera, se utiliza una implementación más actualizada y más eficiente en memoria y reducimos la cantidad de asignaciones que vamos a realizar cada vez que inicializamos una conexión.

Una vez que se haya terminado con los cambios, debemos volvemos a nuestra prueba comparativa de Go y validamos el impacto. Para facilitar la comparación de resultados, Go proporciona una herramienta llamada benchstat que puede ayudarnos a comparar los dos resultados.

 
# benchstat baseline.txt initialize_per_connection.txtngoos: darwinngoarch: arm64npkg: github.com/elastic/beats/v7/libbeat/esleg/eslegclientn                    baseline.txt    initialize_per_connection.txt   n                       sec/op       sec/op     vs base              nExecHTTPRequest-12    2.995µ ± 2%   2.905µ ± 1%  -3.01% (p=0.000 n=8)nn                    baseline.txt    initialize_per_connection.txt    n                        B/op          B/op      vs base              nExecHTTPRequest-12   6.055Ki ± 0%   5.555Ki ± 0%  -8.26% (p=0.000 n=8)nn                    baseline.txt   initialize_per_connection.txt   n                     allocs/op    allocs/op   vs base              nExecHTTPRequest-12     27.00 ± 0%   26.00 ± 0%  -3.70% (p=0.000 n=8)
 
De esta manera se redujo el tiempo de ejecución en un 3%, el consumo de memoria en un 8% y el número de asignaciones en ~3%. Los números anteriores nos dan confianza en que los cambios que proponemos mejorarán el rendimiento. 

La cantidad real de memoria asignada depende del tamaño de la respuesta que obtenemos de Elasticsearch, por lo que otra área que podemos considerar es si hay algo que podamos hacer para reducir ese tamaño de respuesta. El código que deserializa la respuesta ya está realizando una inicialización diferida y se salta secciones de la respuesta. Pero para analizarlo, todavía necesitamos analizar la respuesta completa, incluida la parte de la respuesta que no necesitamos.

La idea es consumir solo el campo de la respuesta que usemos, reduciendo los bytes de respuesta por parte de Elasticsearch. Al observar la documentación de Elasticsearch, vemos la función de filtrado de respuestas , que podemos aprovechar y reducir los campos de respuesta para respuestas masivas a los que solo se requieren de nuestro deserializador personalizado.

La asignación acumulativa de execHTTPRequest disminuyó de los 32 GB de asignación originales a 18 GB de asignación. Esto supone aproximadamente un 43 % menos de asignación de memoria en general. Ahora vemos que usamos io.copy para consumir los bytes de respuesta, lo cual es más eficiente para la memoria, como se vio en nuestros puntos de referencia de Go anteriormente, y la reducción del tamaño de respuesta en combinación con io.copy muestra una mejora en cómo Cuánta memoria asignamos.

Esto es importante, ya que ahora hemos reducido la cantidad de memoria que el GC tiene que limpiar y hemos liberado algunos de los ciclos de la CPU para otras tareas. Cambiar el pprof al perfil de CPU y, en particular, comparar los dos perfiles de CPU del mismo punto de referencia, tomados en el mismo punto de control.

Nos muestra que en nuestro escenario de referencia, logramos mejorar los ciclos de CPU en 1,78 segundos para los métodos PublishEvents y 1,3 en los métodos BulkConnectPublishFails.
 

El resultado

El resultado de nuestra investigación resultó en mejoras realizadas para Beats en v8.11.0 . Curiosamente, esta investigación también nos llevó a realizar cambios en el complemento de salida Logstash® Elasticsearch para implementar la misma técnica de filtrado de respuestas.

Lecciones aprendidas

Esta investigación demostró una cosa: se debe dar seguimiento a las advertencias obsoletas. Es una buena oportunidad para revisar la implementación y encontrar formas de mejorar la asignación de memoria. Además, para aplicaciones que envían/reciben grandes cantidades de datos, debemos usar solo las partes de la respuesta que necesitamos porque el tamaño de la respuesta se asigna en el montón de nuestra aplicación cuando nos preocupamos por el contenido de la respuesta.

Obtenga más información sobre las posibilidades con Filebeat .

Ir al Blog