Single-Threading puede ser rápido

A pesar de usar una implementación mono hilo, Redis, el sistema de almacenamiento clave-valor en memoria de alto rendimiento, es conocido por su increíble velocidad.

¿Cómo es posible que Redis pueda gestionar cientos de miles de solicitudes por segundo? Antes que nada, hay que aclarar que Redis sí usa múltiples hilos, no es estrictamente un sistema monohilo.

Aunque es cierto que mantiene un hilo responsable del procesado de las peticiones de cliente y para el manejo de las estructuras de datos, Redis usa otros hilos de fondo para ejecutar otras tareas necesarias para su funcionamiento.

Entonces, ¿por qué es tan rápido?

Optimizaciones multihilo

El uso de ejecución multihilo permite reducir la presión que se tiene en la ejecución monohilo de las peticiones entrantes en escenarios de alta concurrencia. Este proceso multihilo se aplica únicamente al análisis del protocolo de peticiones de datos, manteniendo la ejecución monohilo en el procesado de comandos y la manipulación de datos.

Con la publicación de Redis 8.0 llegó una nueva implementación E/S multihilo que anunciaba grandes mejoras en la transferencia de datos (entre el 37% y el 112%, según los datos publicados por Redis) en CPUs multinúcleo.

Probando la nueva implementación multihilo

Como ya hemos comentado, Redis procesa las peticiones en un único hilo, dividiendo la ejecución en los siguientes 4 pasos:

  1. Lectura de la petición desde el socket
  2. Análisis de la petición
  3. Procesado de la petición, ejecutando las operaciones necesarias sobre los datos
  4. Escritura de la respuesta en el socket

No se comenzará a procesar una nueva petición hasta que estos 4 pasos se hayan completado en orden para la petición actual.

La escritura de los resultados en el socket que se realiza en el paso 4 es típicamente una operación lenta en cuanto al tiempo necesario para completarla. Es en este punto donde podemos beneficiarnos de la nueva implementación multihilo de la E/S, configurando nuestro Redis para que ejecute las operaciones E/S en un nuevo hilo. De esta forma, Redis está en disposición de comenzar a procesar una nueva petición en paralelo, mejorando así el rendimiento.

La propiedad io-threads de Redis (esta propiedad es inmutable) será la que utilicemos para configurar la escritura multihilo en el socket. Recordemos que esta propiedad se inicializa a 1 por defecto, y que se usa para indicar el número de hilos máximo que se podrán crear para las escrituras en el socket.

También tenemos disponible la propiedad de configuración io-threads-do-reads en Redis que nos daría la posibilidad de usar ejecuciones multihilo en la lectura y análisis de peticiones desde el socket (pasos 1 y 2 descritos anteriormente). Esta opción, sin embargo, y de acuerdo a la información que dan desde Redis, no tiene un impacto significativo sobre el rendimiento.

Por tanto, vamos a centrarnos únicamente en el impacto que tiene la configuración de io-threads.

Tests sintéticos: ¿cómo se han ejecutado?

Herramienta de benchmarking

Para las pruebas se ha usado memtier_benchmark, una herramienta de benchmarking Open Source desarrollada por Redis Labs que integran en sus procesos de desarrollo, principalmente para hacer tests de no regresión y optimización de rendimiento.

memtier_benchmark fue presentada en el blog oficial de Redis y el proyecto está disponible en GitHub.

Nuestro Redis de pruebas

Vamos a usar un cluster Redis, desplegado en un Openshift Container Platform (OCP), con la siguiente configuración:

Usaremos 4 imágenes diferentes para montar los nodos, así podremos hacer una comparativa de rendimiento entre las distintas versiones:

Objetivos de los tests

Con la ejecución de estos tests perseguimos:

Como hemos visto, podemos establecer cualquier valor por encima del valor por defecto de un hilo en el parámetro io-threads. Nos interesa ver el impacto en el rendimiento que tienen los cambios en esta configuración.

Por otro lado, sabemos que tras el anuncio que hizo Redis en el que se explicaba que dejaría de ser open source surgieron unos cuantos forks del proyecto. Tras revisar aquellos que habían tenido más actividad y que más habían crecido en número de contribuidores, decidimos limitar las pruebas a Valkey.

También nos interesa comparar el rendimiento de Valkey frente a las últimas versiones de Redis. Este último ha recibido muy buenos comentarios en cuanto a la mejora en términos de rendimiento desde la publicación de su versión 8.

Resultados de los tests

Cada test se comienza con un cluster Redis recién creado en un namespace concreto en nuestro OCP. Igualmente, se crea un pod nuevo desde el que se ejecutará la herramienta de benchmarking al comienzo de cada prueba.

Los valores utilizados para io-threads fueron: 1, 8, 12 y 16.

En la siguiente tabla se recogen los valores obtenidos en los distintos tests, mostrando la media de operaciones por segundo, para cada imagen y cada una de las configuraciones de io-threads.

io-threads 1 8 12 16
redis-stack-server:7.2.0 441.316 446.944 290.793 190.601
redis:7-bookworm 432.611 451.702 289.670 188.995
valkey:8-bookworm 431.714 860.290 766.879 584.707
redis:8-bookworm 453.247 1.027.698 1.030.549 1.025.972

Estos mismos datos en forma de gráfica:

Valores io-threads en gráfico de barras
Valores io-threads en gráfico de barras

Parece que el punto óptimo se sitúa en una configuración de io-threads correspondiendo a 2 veces el número de CPUs configuradas por nodo de Redis.

A partir de este valor vemos cómo el rendimiento se ve degradado en Valkey 8, mientras que Redis 8 mantiene un rendimiento estable si vamos más allá de este punto óptimo. Por tanto, los resultados de Redis 8 son más consistentes.

Como esperábamos, no se ha observado ninguna mejora de rendimiento en Redis 7 con el incremento de io-threads. Al contrario, nos ha sorprendido observar una degradación.

Redis 8 ha rendido mejor que Valkey 8 en todos los escenarios. Por tanto, nos quedamos con la regla de 2xCPU como configuración por defecto para io-threads y, en principio, la balanza se decanta, al menos por ahora, por el uso de Redis 8 como la imagen a usar para nuestros clusters de Redis.

Aplicando el conocimiento adquirido: optimización de recursos en un escenario real

El escenario: una aplicación desplegada en Kubernetes, haciendo uso intensivo de un cluster de Redis, requiriendo de tasas realmente altas de peticiones por segundo.

El cluster de Redis, en periodos de pico de uso de la aplicación se ha de escalar a 100 nodos para poder soportar el rendimiento requerido. Actualmente, este cluster está formado por nodos instanciados a partir de la imagen Redis Stack 7.2, con 1 CPU por nodo.

Nuestro objetivo es intentar reducir el número de nodos del cluster a la mitad, pasando de 100 a 50 nodos, usando la imagen Redis 8. Para ello, duplicaremos el número de CPUs por nodo, pasando de 1 a 2 CPUs, y estableciendo la configuración de io-threads en 4, como habíamos determinado, con el fin de beneficiarnos de las mejoras que ofrece la nueva implementación del I/O threading.

Para validar que la plataforma con esta nueva configuración es capaz de soportar el uso en periodos de pico, se usa un conjunto de tests complejos elaborados precisamente para someterla a cargas similares a las previstas en esos periodos y así poder validar que estamos en disposición de aguantar los periodos de demanda extrema.

Estos tests End-to-End (E2E) se han desarrollado a medida basándose en Spring Framework, haciendo uso del cliente de [Redis Lettuce(https://github.com/redis/lettuce).

Aprovechando la ventana de disponibilidad de la plataforma, se ejecutó la suite completa de tests con las siguientes configuraciones, para poder comparar los resultados:

Aprovechamos que el Operador de Redis desarrollado para gestionar todos los clusters de Redis (actualmente liberado como Open Source) despliega, junto a los pods del propio cluster, un pod encargado de extraer una gran cantidad de métricas y las expone para poder ser recuperadas por Prometheus, junto a las métricas propias de Kubernetes, y mostradas en un panel en Grafana.

Pasemos a revisar los resultados obtenidos.

Comandos procesados

Comandos procesados en Redis Stack 7.2 - 100 nodes
Comandos procesados en Redis Stack 7.2 - 100 nodes
Comandos procesados en Redis 8 - 50 nodes
Comandos procesados en Redis 8 - 50 nodes
Comandos procesados en Valkey 8 - 50 nodes
Comandos procesados en Valkey 8 - 50 nodes

Vemos como el cluster montado con Redis 8 aprovecha la ventaja de la configuración de io-threads y la nueva implementación, llegando prácticamente a alcanzar el número de comandos por unidad de tiempo del cluster de referencia con Redis Strack 7.2. Vemos que cada nodo prácticamente llega a duplicar el número de comandos procesados por unidad de tiempo respecto a los valores de Redis Stack 7.2.

Valkey 8 presenta un comportamiento similar a Redis 8. Lamentablemente, al comienzo del test se produjo el desalojo de el pod de uno de los nodos, lo que explica la pequeña caída que se aprecia en la gráfica.

Hits

Hits en Redis Stack 7.2 - 100 nodes
Hits en Redis Stack 7.2 - 100 nodes
Hits en Redis 8 - 50 nodes
Hits en Redis 8 - 50 nodes
Hits en Valkey 8 - 50 nodes
Hits en Valkey 8 - 50 nodes

El mismo comportamiento que vimos en los comandos procesados por unidad de tiempo se repite en el número de hits. Se llega casi a alcanzar el rendimiento de referencia de Redis Stack 7.2 con la mitad de nodos tanto en Redis 8 como en Valkey 8.

Para dar una idea de la cantidad de datos que se manejan durante los tests, se incluyen las siguientes gráficas que muestran el número de keys almacenadas por nodo.

Cache entries

Cache entries Redis Stack 7.2 - 100 nodes
Cache entries Redis Stack 7.2 - 100 nodes
Cache entries Redis 8 - 50 nodes
Cache entries Redis 8 - 50 nodes
Cache entries Valkey 8 - 50 nodes
Cache entries Valkey 8 - 50 nodes

Si echamos un vistazo al uso de CPU, vemos resultados más consistentes en Redis 8 que en Valkey 8.

CPU usage

CPU usage Redis Stack 7.2 - 100 nodes
CPU usage Redis Stack 7.2 - 100 nodes
CPU usage Redis 8 - 50 nodes
CPU usage Redis 8 - 50 nodes

Para finalizar la comparativa de las métricas obtenidas, echemos un vistazo a la tasa de transferencia de salida de red de los nodos.

Network out throughput

Network out throughput Redis Stack 7.2 - 100 nodes
Network out throughput Redis Stack 7.2 - 100 nodes
Network out throughput Redis 8 - 50 nodes
Network out throughput Redis 8 - 50 nodes
Network out throughput Valkey 8 - 50 nodes
Network out throughput Valkey 8 - 50 nodes

Conclusiones finales

La nueva implementación de I/O threading incluida en Redis 8 y Valkey 8 realmente marca la diferencia con las antiguas implementaciones. Haciendo uso de múltiples hilos para hacer la escritura de los resultados de los comandos al socket se alivia significativamente la carga en el hilo principal, permitiendo un incremento impresionante del rendimiento por nodo.

De los datos de los tests podemos extraer que se ha alcanzado una mejora de rendimiento del 90-95%.

Usando Redis 8, con las mejoras que introduce y, sobre todo, con la implementación de I/O Threading, podemos llegar a una reducción considerable de recursos. Tras las pruebas pudimos pasar los periodos de pico con la mitad de nodos por cluster, subiendo la CPU de 1 a 2.

Si nos basamos únicamente en las métricas de Redis aquí expuestas (y algunas más que no se han incluido por no resultar tan interesantes) Redis 8 y Valkey 8 presentan rendimientos muy similares. Sin embargo, también revisamos logs y métricas de la aplicación cliente. En estas observamos que Redis 8 se comportó de forma más estable que Valkey 8. Consiguió mantener tasas de transferencia durante todos los tests más estables, mientras que Valkey 8 mostró algunas fluctuaciones.

Redis 8, tras su vuelta al mundo Open Source, ha demostrado un rendimiento impecable. También ha mostrado tener un mayor nivel de optimización, además de comportarse más estable y más consistente antes las distintas configuraciones de io-thread.

Por todo esto, nos quedamos con Redis 8, al menos por ahora.

Referencias

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete