Midiendo el Impacto de Nuestros Cambios con K6

Luis Tejerina
PeYa Tech
Published in
8 min readMar 23, 2022

En el equipo de Search & Recommendation estamos siempre en búsqueda de la mejor experiencia para nuestros usuarios [1]. En ese camino intentamos llevar al siguiente nivel de personalización la búsqueda de productos dentro de nuestros comercios asociados.

El problema de cómo resolver una búsqueda es complejo en sí mismo, pero para lo que queremos contarles hoy, nos interesa destacar que es un problema sincrónico.

Con esto nos referimos a que por cada búsqueda que recibimos hay un usuario esperando que le ayudemos a encontrar si un determinado comercio vende leche o no, si hay helado de frutilla, etc. Como todos sabemos, el tiempo que tardamos en responder a esas búsquedas es uno de los atributos de calidad más importantes a tener en cuenta cuando agregamos nuevos componentes a nuestra arquitectura.

En nuestro caso desarrollamos un nuevo servicio que nos permite, a través de modelos generados por nuestro equipo de machine learning, interpretar las búsquedas y expandirlas en base a otros términos de búsqueda similares, y así lograr mejores resultados.

Figura 1: Diagrama de componentes que intervienen en la búsqueda de productos.
Figura 1: Diagrama de componentes que intervienen en la búsqueda de productos.

En la Figura 1 podemos observar una vista de la arquitectura para la búsqueda de productos. En celeste, podemos ver el nuevo servicio que agregamos y queremos poner a prueba. También vemos cómo se alimenta de un proceso de machine learning donde implementamos Word2Vec[2] a partir de datos de nuestro data lake.

¿Cómo validamos que las nuevas funcionalidades no degraden la performance?

Para eso realizamos pruebas de performance. Una prueba de performance es una prueba automatizada donde se intentan imitar uno o más flujos que el usuario hace sobre un sistema de manera controlada y donde, dependiendo del tipo de prueba, la cantidad de usuarios virtuales que se utiliza es distinta.

Existen distintos tipos de pruebas de performance; repasemos algunos para poder determinar cuál nos conviene utilizar en este caso.

Pruebas de estrés:

Figura 2: Usuarios virtuales en función del tiempo para pruebas de estrés
Figura 2: Usuarios virtuales en función del tiempo para pruebas de estrés

Muchas veces nos resulta útil medir con qué nivel de carga nuestro sistema va a fallar, para estar preparados sobre cómo va fallar y poder medir su disponibilidad y resiliencia. En las pruebas de estrés incrementamos la carga de forma creciente para descubrir ese punto de fallo; como mostramos en la Figura 2.

Pruebas de resistencia:

Figura 3: Usuarios virtuales en función del tiempo para pruebas de resistencia
Figura 3: Usuarios virtuales en función del tiempo para pruebas de resistencia

Si las pruebas de estrés nos ayudan a validar disponibilidad y resiliencia, las pruebas de resistencia nos ayudan a validar la confiabilidad de un sistema a largo plazo. En la Figura 3 podemos ver que son tests de larga duración y una carga controlada. Esto nos puede ayudar a encontrar memory leaks, race conditions, etc.

Pruebas de carga:

Figura 4: Usuarios virtuales en función del tiempo para pruebas de carga
Figura 4: Usuarios virtuales en función del tiempo para pruebas de carga

Este tipo de pruebas intenta validar los objetivos de performance que un sistema debe alcanzar bajo un nivel de carga similar al habitual (Figura 4).

Es claro que el tipo de pruebas que necesitábamos era una prueba de carga. Saber cómo performaba nuestro nuevo servicio era muy importante para darnos una idea de cómo iba a impactar en la performance general de las búsquedas.

K6 al rescate

Lo siguiente que teníamos que buscar era la herramienta que nos permitiera realizar estas pruebas. Para eso consultamos con el equipo de performance de PedidosYa. Ellos están al tanto del estado del arte en esta materia y nos recomendaron fuertemente utilizar K6[3]. Además de que nos brindan soporte y un self-service de pruebas de performance con las herramientas necesarias para la ejecución, el scheduling y el monitoreo de las pruebas.

¡Manos a la obra!

Para comenzar a realizar pruebas de performance con K6 necesitamos definir un script de pruebas. Los scripts se codean en javascript y tienen la siguiente estructura:

La variable options nos deja configurar los escenarios y objetivos de nuestra prueba. Esto lo veremos en detalle más adelante.

  1. La función setup, donde se espera que se coloque el código de inicialización que creamos necesario. Esta función se ejecuta una sola vez para toda la prueba.
  2. La siguiente función es la que se conoce como función objetivo o VU code. Donde tendremos que colocar el código que queramos que ejecute cada usuario virtual.
    Un usuario virtual es la abstracción que nos presenta K6 para simular concurrencia en nuestro servicio.
  3. Finalmente la función teardown nos permite ejecutar código al terminar la prueba.

En PedidosYa, nuestros servicios se despliegan en un entorno multi-tenant, lo que nos permite dividir la carga por regiones aumentando la disponibilidad y la resiliencia en caso de fallos.

Esto era importante para calcular la carga que teníamos que generar. En principio utilizamos datos previos de la región con mayor tráfico, y luego lo multiplicamos para asegurarnos mayor confiabilidad.

En K6 definimos los patrones de carga a través de escenarios[4]. Es posible definir múltiples escenarios, cada uno con un comportamiento diferente y su ejecución puede ser secuencial o paralela. Cada escenario es ejecutado por un executor: estos definen la forma en que los usuarios virtuales se van a repartir y cómo van a realizar las iteraciones sobre nuestra función objetivo para alcanzar el nivel de carga configurado.

Existen muchos executors[5]; para nuestra vamos a utilizar Constant arrival rate. Este executor intenta mantener una cantidad fija de iteraciones por unidad de tiempo. Para eso varía la cantidad de usuarios virtuales en función del tiempo que tome completar cada iteración. Esto nos sirve para mantener una carga constante sobre nuestro servicio sin importar cómo este responda.

Con esta información podemos completar nuestro escenario.

Consejos a la hora de definir escenarios:

  • Hay que tener cuidado con la cantidad de usuarios virtuales que generamos. Cada usuario virtual consume recursos y podemos dejar sin memoria a las instancias que ejecuten nuestras pruebas muy rápidamente.
  • Una buena idea para generar grandes cargas sin utilizar instancias de mucho tamaño es dividir la carga en múltiples ejecuciones simultáneas del script de pruebas.

Luego de definido nuestro escenario vamos a definir nuestros objetivos.

K6 nos permite medir esos objetivos y nos va a informar al finalizar la prueba si se cumplieron o no. Hay muchas métricas disponibles [6] y también podemos crear nuestras propias métricas y agregarlas como objetivos particulares.

Nuestro objetivo era asegurarnos que el tiempo de respuesta del nuevo servicio fuera lo suficientemente bajo como para no afectar la performance general del sistema. Por eso indicamos un percentil 90 de menos de 10 ms. Además, también queríamos que el porcentaje de errores sea bajo, por lo que agregamos otro objetivo en ese sentido.

Ahora solamente nos falta definir la función objetivo.

Analicemos la función objetivo: en primer lugar definimos los datos de prueba a utilizar.Los países están definidos como array, pero las queries las definimos como SharedArray [7]. Esto nos permite que esa variable sea compartida en memoria por todos los usuarios virtuales que genere K6. Lo cual es muy importante si el set de datos es lo suficientemente grande para evitar consumir más memoria de la que necesitamos. De lo contrario, se genera una copia de los datos para cada usuario.

Siempre conviene pensar en utilizar datos lo más realistas posibles, como también nos conviene agregar algún grado de aleatoriedad a los datos utilizados. Esto nos permite evitar algunos cachés y le agrega más fiabilidad a la prueba.

Luego tenemos la llamada al servicio puesto a prueba y finalmente debemos utilizar la función check para indicarle a K6 si el resultado obtenido fue el esperado o no.

Algunos consejos sobre la función objetivo:

  • Siempre probamos los scripts en entornos locales primero. Esto permite asegurarnos que el código de la función objetivo es correcto antes de mandarlo a correr contra algún entorno real.
  • Hay que tener cuidado con la cantidad de memoria que utiliza la función objetivo. Como dijimos, cada usuario virtual posee su propia porción de memoria. Por lo que, aunque definamos una sola vez en el script alguna variable, esta se replicará por cada usuario virtual.
  • Tenemos que tener en vista las dependencias externas de nuestro servicio; si esas dependencias son compartidas con otros servicios podemos introducir ruido con nuestras pruebas, o en el peor de los casos afectar la disponibilidad de otras partes de nuestro sistema. Un caso muy común es que el servicio puesto a prueba realice llamadas a otros servicios ajenos a nosotros. Una forma de solventar esto es creando mocks de nuestras dependencias y manteniendo nuestra prueba aislada.

El momento de la verdad

Para correr la prueba podemos ejecutar el siguiente comando:

k6 run script.js

Una vez finalizada la prueba K6 nos imprimirá por consola un reporte[8].

Figura 5: Reporte de ejecución K6
Figura 5: Reporte de ejecución K6

Algunas de las métricas importantes que miramos en el reporte son:

  • checks: Esta métrica nos indica el porcentaje de respuestas correctas que recibimos. Como vemos, no recibimos errores durante la prueba.
  • http_req_duration: Es el tiempo que tardó el servidor en procesar y responder el request. No tiene en cuenta la resolución DNS ni los tiempos de establecimiento de conexión. Vemos que a la izquierda de la métrica se muestra un tick, esto es porque K6 detectó que nuestro objetivo para esta métrica fue cumplido con un P95 de 7ms.
  • http_req_failed: Es el porcentaje de request que fallaron. Como era parte de nuestro objetivo, vemos el tick que nos indica que se cumplió.

Para finalizar el análisis de la prueba de carga sobre nuestro nuevo servicio miramos algunas métricas en las herramientas de monitoreo.

Figura 6: Throughput durante la prueba
Figura 6: Throughput durante la prueba
Figura 7: Cantidad de Pods utilizados
Figura 7: Cantidad de Pods utilizados
Figura 8: Uso de memoria
Figura 8: Uso de memoria
Figura 9: Uso de CPU
Figura 9: Uso de CPU

Podemos observar en la Figura 6 que el patrón de carga fue el esperado. Logramos generar el patrón de carga que queríamos y, como vemos en la Figura 8 y la Figura 9, el mayor consumo de recursos fue el CPU, lo que provoco que el servicio escale Figura 7.

Mirando los tiempos de respuesta que nos informó K6, pudimos asegurarnos que el nuevo servicio performaba como esperábamos, lo que nos permitió ganar en confianza a la hora de salir a producción y llevar esta increíble funcionalidad a nuestros usuarios.

Pruebas de carga para todos y todas

Como vimos, las pruebas de carga son una herramienta muy útil para poder enfrentar nuevos desafíos con la confianza de llevarlos a producción sin afectar nuestra performance actual.

Además, K6 nos demostró ser una herramienta perfecta para esto, con una gran versatilidad para poder definir programáticamente nuestros objetivos de performance y validarlos siempre que lo necesitemos.

En el equipo de Search & Recommendation siempre intentamos llevar a cabo las mejores prácticas y superarnos día a día; esto nos permite llevarles a los usuarios resultados mejores y más rápidos.

Referencias

Molyneaux, I. (2009). The Art of Application Performance Testing. O’Reilly Media.

[1] https://medium.com/peya-tech/en-busca-de-la-mejor-estrategia-de-ranking-experimentaci%C3%B3n-y-confirmaci%C3%B3n-din%C3%A1mica-6e26f91dd5bf

[2] https://towardsdatascience.com/word2vec-explained-49c52b4ccb71

[3] https://k6.io/docs/

[4] https://k6.io/docs/using-k6/scenarios/

[5] https://k6.io/docs/using-k6/scenarios/executors/

[6] https://k6.io/docs/using-k6/metrics/

[7] https://k6.io/docs/javascript-api/k6-data/sharedarray/

[8] https://k6.io/docs/getting-started/res

--

--