Lecturas que escalan con tus núcleos
Redis es rápido. También es, por diseño, monohilo en la ruta de datos: un bucle de eventos, un núcleo, un comando a la vez. Ese es un diseño genuinamente bueno: hace que Redis sea fácil de razonar y elimina toda una categoría de errores de concurrencia. Pero tiene un techo del que no puedes salir a base de dinero. Dale a un motor de un solo bucle de eventos una máquina de 4 núcleos, una de 8 núcleos, una de 32 núcleos, y el rendimiento de lectura acaba en el mismo lugar. Los núcleos extra permanecen inactivos.
Zaris toma el otro camino. Las claves están particionadas y cada partición tiene un propietario, de modo que las lecturas de claves diferentes aterrizan en núcleos diferentes y se ejecutan al mismo tiempo. El resultado es un rendimiento que se mueve cuando añades núcleos: en lugar de una línea plana, una pendiente.
Las cifras
Medimos el rendimiento de GET a través del front-end RESP de Zaris —la misma ruta del protocolo de Redis que usaría un cliente de Redis ordinario— frente a Redis y Memurai en el mismo hardware.
| Motor | 1 núcleo | 4 núcleos |
|---|---|---|
| Zaris (RESP GET) | ~483.000 ops/s | ~946.000 ops/s |
| Redis / Memurai | ~740–780.000 ops/s | ~740–780.000 ops/s (plano) |
Aquí son ciertas dos cosas, y queremos ser francos sobre ambas.
Primero, la advertencia honesta: pasar de 1 núcleo a 4 núcleos aproximadamente duplica el rendimiento de Zaris, no lo cuadruplica. Eso es alrededor de 0,5 de eficiencia de escalado: sublineal. La coordinación entre núcleos, el ancho de banda de memoria y la ruta de red se llevan su parte. Cualquiera que te diga que un almacén distribuido escala de forma perfectamente lineal te está vendiendo algo.
Segundo, la parte que importa: Zaris sube, y un motor de un solo bucle de eventos no puede. Con un núcleo, Redis y Memurai van en realidad por delante: un bucle monohilo ajustado sin coordinación entre núcleos es difícil de batir en un solo núcleo. Pero esa cifra también es su techo. Zaris empieza más abajo y los supera a medida que los núcleos entran en línea, porque el trabajo es genuinamente paralelo. En una máquina moderna grande, "escala a 0,5 de eficiencia" le gana a "clavado a un núcleo" en todo momento.
Por qué escala
Un solo bucle de eventos serializa todo: cada GET, cada SET, cada cliente, canalizados a través de un hilo. Es amable con la caché y sin bloqueos, pero es fundamentalmente el trabajo que cabe en un núcleo.
Zaris, en cambio, le da a cada partición un propietario y despacha por núcleo. Dos clientes que leen dos claves que viven en particiones diferentes nunca tocan el mismo hilo. Añade núcleos, añade particiones, y más de esas lecturas se ejecutan de forma concurrente.
Y no hay impuesto de proxy que pagar por ello. Zaris aplica el hash a las claves con el mismo cálculo de slot CRC16(key) % 16384 que usa Redis Cluster, de modo que un cliente de Redis consciente del clúster enruta cada clave directamente al nodo que la posee:
redis client ──CRC16(key) % 16384 → slot → owner──▶ node that holds the key ──▶ reply
Eso es un solo salto, exactamente como hablar con Redis Cluster: el cliente aplica el hash a la clave y se conecta directamente a su propietario, y cuando el comando aterriza allí se sirve desde el almacén en proceso de ese nodo. Lo que ganas frente a una sola instancia es capacidad de lectura que crece con el clúster en lugar de topar con el límite de un núcleo, sin renunciar al enrutamiento directo al propietario para conseguirlo.
La ruta de escritura también se volvió más rápida
La 2.0.0 no fue solo sobre lecturas. En la ruta de SET eliminamos una lectura-antes-de-escribir que cada escritura estaba pagando, y arreglamos una llamada perdida a Count() en la ruta caliente que había limitado silenciosamente el rendimiento de SET. En el cliente nativo de .NET, las operaciones de la ruta caliente —Get, Put, Delete, Expire, Persist, Ttl y sus formas encadenadas— ahora devuelven ValueTask, de modo que una llamada que se completa de forma síncrona no asigna nada:
// Alloc-free when the value is already local — no Task on the fast path.
ValueTask<ReadResult<Order>> pending = client.GetAsync<Order>("order:42");
Y para cargas de trabajo con valores grandes y frecuentemente actualizados, un modo opcional de fragmentación de objetos grandes almacena los valores calientes grandes como piezas byte[][] sub-LOH, manteniéndolos fuera del Montón de Objetos Grandes (Large Object Heap) y lejos del recolector de basura.
Mídelo honestamente
Si realizas tú mismo este benchmark, dos cosas te morderán:
- Los benchmarks de lectura suelen estar limitados por el cliente. Un único generador de carga se satura mucho antes que el servidor, y entonces estás midiendo tu cliente, no Zaris. Para ver realmente cómo escala el servidor, necesitas suficiente carga concurrente —varios generadores, suficientes conexiones— para mantener ocupados todos los núcleos. Si el rendimiento es plano a medida que añades núcleos al servidor, comprueba primero si tu cliente es el cuello de botella.
- Mide la CPU del servidor correctamente. Léela desde el cgroup, no desde una estimación de un metrics-server del clúster. La estimación va con retraso y suaviza, y te mentirá sobre si el servidor está realmente saturado.
Pruébalo
dotnet add package Clustron.Zaris.SDK --version 2.0.0
Si estás sopesando Zaris frente a Redis para un stack de .NET, la comparación Clustron frente a Redis expone las concesiones, y Zaris 2.0.0 cubre todo lo demás que se incluyó en esta versión: estructuras de datos nativas, Streams y el front-end del protocolo de Redis que hizo que estas cifras fueran medibles en primer lugar. Si vienes de un despliegue de Redis existente, Si vienes de Redis recorre la migración, y el análisis en profundidad del protocolo de Redis explica el diseño del proxy en detalle.
Las descargas y las notas completas de la versión están en GitHub, o instala Zaris en la plataforma que elijas.