Benchmark de tecnologías Backend

Probando escenarios cercanos a la realidad

John Mauricio Carmona Escobar
Bancolombia Tech
7 min readOct 24, 2022

--

El siguiente benchmark tiene como objetivo validar el rendimiento de diferentes stacks tecnológicos. Pretende lograr una comparación un poco más cercana a la realidad, no solo realizar la ejecución de escenarios simples, sino replicar escenarios más cercanos a los que día a día tenemos que enfrentar, en donde la interacción con otros sistemas toma relevancia y la latencia es un factor importante.

Pretendemos además hacer que el benchmark pueda ser fácilmente replicable, con el fin de validar diferentes escenarios y garantizar que el comportamiento presentado sea un resultado confiable y no el producto de la aparición de un comportamiento puntual.

Los resultados que veremos a continuación no pretenden tener la última palabra al momento de tomar la decisión de que stack tecnológico usar. Lo anterior ya que se deben tener en cuenta otros criterios importantes como el nivel de concurrencia, estilo de programación, experiencia del equipo, curva de aprendizaje, entre otros. Para lo anterior recomendamos leer el siguiente artículo.

Los stacks que pondremos a prueba en el siguiente benchmark son:

  • Java 11 (Imperative) — Spring boot 2.6
  • Java 11 (Reactive) — Spring webflux 5.3
  • Elixir 1.13- Cowboy 2.2
  • Go 1.18 — Gin 1.8.1

Puedes revisar el código de cada una de las implementaciones en el siguiente link, además podrás encontrar el detalle de cómo ejecutar las pruebas.

Ambiente

Para la ejecución de cada una de las pruebas generamos un entorno de forma aislada de la siguiente forma:

En el gráfico anterior tenemos 4 cajas que representan instancias de EC2, cada una con una única responsabilidad.

La caja del medio es donde tendremos nuestro stack, el cual, como mencionamos anteriormente, se encuentra aislado para que los demás componentes no afecten sus resultados. Los demás componentes de la derecha corresponden, uno, a una base de datos Postgres, y el otro, a una instancia con Nodejs que representará nuestro servicio externo y que se encargará de simular latencias. Por último, una instancia encargada de realizar las pruebas de carga y recolectar los resultados.

Escenarios

Health check

El primer escenario que validaremos es un caso sencillo, donde el servicio solo nos retornará una cadena de texto. Normalmente podemos encontrar diferentes benchmark que validan dicho escenario, y aunque es un escenario importante no representa del todo los casos que nuestros stacks deben enfrentar.

Las gráficas anteriores muestran el comportamiento de las transacciones por segundo (tps) soportadas por cada uno de los stacks durante 5 minutos de carga (Entre más alto mejor).

Caso 1

A partir de este momento intentaremos replicar casos un poco más cercanos a la realidad, en donde nuestro servicio no actúa solo sino que deberá interactuar con otros servicios.

Este escenario realizará un par de llamados a servicios externos (en nuestras implementaciones estos podrían equivaler a una validación de seguridad, un llamado a otra aplicación, una consulta de información, entre otros). Luego realizará una consulta a la base de datos para posteriormente realizar un último llamado a un servicio externo y finalizar actualizando su estado.

Lo anterior se puede resumir en el siguiente diagrama de secuencia:

Primero ejecutaremos nuestras pruebas en donde nuestro servicio externo simulará una latencia de 50 milisegundos en cada petición.

Luego ejecutaremos la misma prueba pero esta vez aumentando la latencia a 200 milisegundos.

Latencia de 50ms
Latencia de 200ms

Un primer análisis de los resultados anteriores nos muestra que aunque la latencia aumenta casi 4 veces, las alternativas en los stacks no bloqueantes siguen comportándose de forma similar; diferente a la alternativa bloqueante (Java imperative) que se ve afectada en la misma proporción, pasando de tener un rendimiento de casi 200 tps a un poco menos de 50 tps.

Nota: El driver de base de datos de Elixir ofrece una mayor optimización en la actualización de los datos (update), lo que le da una pequeña ventaja en el escenario anterior.

Caso 2

El siguiente caso representa un escenario un poco más sencillo que el anterior, el cual realizará una consulta de información en la base de datos y posteriormente un llamado a un servicio externo.

Latencia de 100ms

Caso 1 + Caso 2

A continuación realizaremos la ejecución de los casos 1 y 2 de forma simultánea.

Latencia de 80ms

Caso 3

Durante el caso 3 solo realizaremos una consulta de información a la base de datos.

Generación de números primos

Con este escenario pretendemos simular servicios que realizan operaciones relacionadas netamente con tareas vinculadas a la CPU, cómo por ejemplo el cálculo de los 1.000 primeros números primos.

Dado que en Java reactivo (webflux) es fácil usar un grupo de subprocesos dedicado, podemos separar las tareas vinculadas a la CPU en un grupo de subprocesos con una cantidad fija igual al número de procesadores disponibles. Lo anterior nos genera un mejor rendimiento al evitar al máximo el context switch (Almacenamiento del estado de un proceso, de modo que pueda restaurarse y reanudar la ejecución en un punto posterior).

Solo servicio externo

El escenario a continuación realizará el llamado un servicio externo que presenta una latencia de 80 milisegundos.

Caso 1 + números primos

Y, por último la ejecución en paralelo del caso 1 más la generación de números primos que nos permita ver si los recursos están siendo usados de forma óptima.

El siguiente escenario ejecutará el caso número 1 por cinco minutos y durante su ejecución se realizarán múltiples llamadas al servicio de números primos por un minuto (línea azul).

Latencia de 80ms

En las gráficas anteriores podemos observar cómo el stack bloqueante (Java imperativo) debido a que gran parte del tiempo se encuentra bloqueado esperando una respuesta, no es capaz de hacer un uso óptimo de sus recursos dejando estos inutilizados.

Uso de recursos de memoria y cpu

Durante la ejecución de los escenarios anteriores se activó el monitoreo de recursos en dichas instancias las cuales arrojaron los siguientes resultados.

Los siguientes valores fueron tomados en la muestra realizada con instancias t2.micro con el fin de validar el uso óptimo en ambientes con pocos recursos.

Podemos resumir las gráficas anteriores en:

  • El consumo de recursos en Java reactivo es en promedio de 600M de memoria y un uso promedio del 70% de la cpu.
  • El consumo en Java imperativo es en promedio de 550M de memoria y solo se puede ver un uso de la cpu en los escenarios de tareas vinculadas a cpu (últimos escenarios ejecutados) ya que en los demás los hilos se encuentran bloqueados la mayor parte del tiempo esperando una respuesta.
  • El consumo de recursos en Go es bastante estable con un uso medio de 350M de memoria y un uso máximo de 65% de cpu.
  • El consumo de recursos en Elixir es en promedio de 450M de memoria y un uso de la cpu llegando en algunos escenarios al 95%.

Es importante anotar nuevamente que los escenarios fueron ejecutados múltiples veces y en diferentes máquinas con el fin de garantizar que los datos presentados reflejaran un comportamiento real de cada uno de los stacks puestos a prueba.

Conclusiones

  • El uso de alternativas no bloqueantes nos permite hacer un uso más eficiente de los recursos.
  • En contextos de baja concurrencia y latencias internas muy bajas, podemos elegir fácilmente alternativas bloqueantes sin mayores inconvenientes. Por otro lado, si necesitamos soportar un mayor número de concurrencias la elección debe ser una alternativa no bloqueante.
  • Los stacks no bloqueantes muestran un comportamiento similar entre ellos.
  • En los stacks de Java el rendimiento al iniciar la prueba no es el más óptimo debido al calentamiento (warmup) que debe ocurrir en la jvm. El anterior problema se pretende solucionar en proyectos como spring native.

Por último, puedes revisar el resultado completo de las ejecuciones en el siguiente link así como el resultado de las mismas pruebas ejecutadas en instancias con menos recursos (t2.micro) donde podrá observar un comportamiento similar. Además puede complementar el bechmark con otros escenarios en https://bancolombia.github.io/performance-benchmark-stacks/

Gracias.

--

--