6 lecciones aprendidas de la experiencia de optimización del rendimiento del servicio de Node.js

Klarna ha realizado grandes esfuerzos para ayudar a los desarrolladores a crear servicios seguros y de alta calidad. Una de las herramientas destinadas a los desarrolladores es una plataforma para realizar pruebas A / B. El componente más importante de este sistema es la multitud de procesos que, para cada solicitud entrante, deciden a qué tipo de prueba (A o B) enviar la solicitud. Esto, a su vez, determina qué color mostrar el botón, qué diseño mostrar al usuario o incluso qué paquete de terceros usar. Estas decisiones tienen un impacto directo en la experiencia del usuario.



Klarna usa la plataforma Node.js. El artículo, cuya traducción publicamos hoy, está dedicado a esas lecciones que los especialistas de la compañía lograron aprender de la experiencia de optimizar el desempeño de su servicio.

Lección número 1: las pruebas de rendimiento pueden dar confianza de que la velocidad del sistema no se degrada con cada versión


El desempeño de cada proceso juega un papel muy importante, ya que estos procesos se utilizan sincrónicamente en las rutas de decisión críticas del ecosistema de Klarna. El requisito habitual de rendimiento para tales tareas es que para el 99.9% de las solicitudes, la decisión debe tomarse con un retraso, cuyo tiempo se expresa en un dígito. Para asegurarse de que el sistema no se desvíe de estos requisitos, la compañía desarrolló un transportador para probar el servicio.

Lección número 2: “enrollando” la carga de forma independiente, puede identificar problemas incluso antes de que lleguen a producción


Aunque prácticamente no vimos ningún problema de rendimiento durante los dos años que la plataforma se ha utilizado en producción, las pruebas indicaron claramente algunos problemas. A los pocos minutos de la prueba, a un nivel moderadamente estable de recepción de solicitudes, la duración del procesamiento de la solicitud aumentó considerablemente de los valores normales a varios segundos.


Información sobre el tiempo requerido para procesar la solicitud. Algún tipo de problema detectado

Decidimos, aunque esto todavía no había sucedido en la producción, que era solo cuestión de tiempo. Si la carga real alcanza un cierto nivel, podemos encontrarnos con algo similar. Por lo tanto, se decidió que este tema debería ser investigado.

Lección número 3: las pruebas de estrés a largo plazo pueden identificar una variedad de problemas. Si todo se ve bien, intente aumentar la duración de la prueba.


Otra cosa a la que vale la pena prestar atención es que los problemas de nuestro sistema aparecen después de 2-3 minutos de trabajo bajo carga. Al principio, realizamos la prueba por solo 2 minutos. Y el problema solo se vio cuando el tiempo de ejecución de la prueba se aumentó a 10 minutos.

Lección número 4: no olvide tener en cuenta el tiempo requerido para la resolución de nombres DNS, teniendo en cuenta las solicitudes salientes. No ignore la vida útil de las entradas de caché; esto puede interrumpir seriamente la aplicación


Por lo general, supervisamos los servicios utilizando las siguientes métricas: número de solicitudes entrantes por segundo, duración del procesamiento de las solicitudes entrantes, nivel de errores. Esto nos da indicadores bastante buenos del estado del sistema, indicando si hay algún problema en él.

Pero estas métricas no proporcionan información valiosa durante el mal funcionamiento del servicio. Cuando algo sale mal, necesita saber dónde está el cuello de botella del sistema. Para tales casos, debe supervisar los recursos utilizados por el tiempo de ejecución de Node.js. Es obvio que la composición de los indicadores, cuyo estado se monitorea en situaciones problemáticas, incluye el uso del procesador y la memoria. Pero a veces la velocidad del sistema no depende de ellos. En nuestro caso, por ejemplo, el nivel de utilización del procesador fue bajo. Lo mismo podría decirse sobre el nivel de consumo de memoria.

Otro recurso que determina el rendimiento de los proyectos Node.js es el bucle de eventos. Así como es importante que sepamos cuánta memoria utiliza el proceso, necesitamos saber cuántas "tareas" necesita para procesar el bucle de eventos. El bucle de eventos Node.js se implementa en la biblioteca libuv C ++ ( aquí hay un buen video al respecto). Las "tareas" se denominan en el presente documento como "Solicitud activa". Otra métrica importante es el número de "controladores activos" que están representados por descriptores de archivos abiertos o sockets utilizados por los procesos Node.js. Puede encontrar una lista completa de tipos de descriptores en la documentación de libuv. Como resultado, si la prueba usa 30 conexiones, entonces se puede esperar que el sistema tenga 30 descriptores activos. El indicador que caracteriza el número de solicitudes activas indica el número de operaciones que esperan en línea para un descriptor particular. ¿Qué son estas operaciones? Por ejemplo, operaciones de lectura / escritura. Una lista completa de ellos se puede encontrar aquí .

Después de analizar las métricas del servicio, nos dimos cuenta de que algo andaba mal aquí. Si bien el número de descriptores activos era lo que esperábamos (en esta prueba, alrededor de 30), el número de solicitudes activas fue desproporcionadamente alto: varias decenas de miles.


Descriptores activos y solicitudes activas

Es cierto que todavía no sabíamos qué tipos de solicitudes estaban en la cola. Después de dividir las consultas activas en tipos, la situación se aclaró un poco. A saber, UV_GETADDRINFO consultas UV_GETADDRINFO resultaron ser muy notables. Se generan cuando Node.js intenta resolver el nombre DNS.

¿Por qué el sistema genera tantas solicitudes de resolución de nombres DNS? Resultó que el cliente StatsD estábamos tratando de resolver el nombre de host para cada mensaje saliente. Cabe señalar que este cliente ofrece la posibilidad de almacenar en caché los resultados de las consultas DNS, pero aquí no se tiene en cuenta el TTL de los registros DNS correspondientes. Los resultados se almacenan en caché por un período de tiempo indefinido. Como resultado, si el registro se actualiza después de que el cliente ya haya resuelto el nombre correspondiente, nunca lo sabrá. Dado que el equilibrador de carga StatsD se puede volver a implementar con una dirección IP diferente, y no podemos forzar el reinicio del servicio para actualizar el caché DNS, este enfoque, que utiliza el almacenamiento en caché por tiempo ilimitado, no era adecuado para nosotros.

La solución a la que llegamos fue utilizar un medio externo al cliente para almacenar en caché las consultas DNS. Esto es fácil de hacer ejecutando el "parche de mono" del módulo DNS. El resultado ahora se veía mucho mejor que antes.


Información sobre el tiempo requerido para procesar la solicitud. Resultado de usar un caché DNS externo

Lección # 5: Realizar operaciones de E / S en modo por lotes. Tales operaciones, incluso asíncronas, son consumidores serios de recursos.


Después de resolver el problema anterior, activamos algunas características del servicio que se desactivaron anteriormente y lo volvimos a probar. En particular, hemos incluido un código que envía un mensaje al tema de Kafka para cada solicitud entrante. La prueba, una vez más, reveló picos significativos en los resultados de las mediciones del tiempo de respuesta (estamos hablando de segundos), observados durante largos intervalos de tiempo.


Información sobre el tiempo requerido para procesar la solicitud. La prueba reveló un fuerte aumento en el tiempo requerido para la formación de respuestas.

Estos resultados apuntan a un problema obvio precisamente en la función que incluimos antes de la prueba. En particular, nos enfrentamos al hecho de que enviar mensajes a Kafka lleva demasiado tiempo.


Información sobre el tiempo requerido para generar mensajes para Kafka

Decidimos utilizar la mejora más simple aquí: colocar los mensajes salientes en una cola en la memoria y enviar estos mensajes en modo por lotes cada segundo. Al volver a ejecutar la prueba, encontramos mejoras claras en el tiempo requerido para que el servicio forme una respuesta.


Información sobre el tiempo requerido para procesar la solicitud. Mejoras después de organizar el procesamiento de mensajes por lotes

Lección número 6: antes de intentar realizar mejoras en el sistema, prepare pruebas, cuyos resultados sean confiables


El trabajo descrito anteriormente para optimizar el rendimiento de un servicio no hubiera sido posible sin un mecanismo de prueba que le permita obtener resultados reproducibles y uniformes. La primera versión de nuestro sistema de prueba no dio resultados uniformes, por lo que no podíamos confiar en ella para tomar decisiones importantes. Habiendo invertido en la creación de un sistema de prueba confiable, pudimos probar el proyecto en diferentes modos, experimentar con correcciones. El nuevo sistema de prueba, en su mayor parte, nos dio la confianza de que los resultados obtenidos fueron algo real y no algunos números que vinieron de la nada.

Digamos algunas palabras sobre las herramientas específicas utilizadas para organizar las pruebas.

La carga fue generada por una herramienta interna que facilitó la ejecución de Locust en modo distribuido. En general, todo se redujo a la ejecución de un solo comando, después de lo cual se lanzaron los generadores de carga, se les transfirió el script de prueba y se recopilaron los resultados visualizados por el panel de control de Grafana. Los resultados correspondientes se presentan en el material en gráficos con un fondo oscuro. Así es como se ve el sistema en la prueba desde el punto de vista del cliente.

El servicio bajo prueba proporciona información de medición en Datalog. Esta información se presenta aquí con gráficos con un fondo brillante.

Estimados lectores! ¿Qué sistemas de prueba de servicio de Node.js utiliza?

Source: https://habr.com/ru/post/484616/


All Articles