Saltar al contenido principal

Si vienes de Redis: apunta tu aplicación .NET a Zaris

· 6 min de lectura
Clustron Team
Ingeniería de Sistemas Distribuidos

Dos rutas de Redis a Zaris

Si tus servicios de .NET ya hablan con Redis, pasar a Zaris no tiene por qué ser una reescritura. Zaris es un almacén clave/valor distribuido, en memoria y replicado, construido de forma nativa para .NET, y desde la versión 2.0.0 habla dos protocolos a la vez: su propio protocolo binario eficiente para clientes de .NET de primera clase, y un front-end RESP2 compatible con Redis al que puede apuntar cualquier cliente de Redis en cualquier lenguaje.

Eso te da dos rutas de migración honestas, y no son mutuamente excluyentes. Puedes empezar con la ruta RESP sin cambios para poner Zaris bajo carga hoy mismo y luego adoptar el cliente nativo donde merezca la pena. Este artículo recorre ambas y te ofrece una regla clara para elegir.

Zaris 0.9.0 ya está aquí — y con disponibilidad general

· 5 min de lectura
Clustron Team
Ingeniería de Sistemas Distribuidos

Zaris 0.9.0 — Disponibilidad General

Hoy lanzamos Zaris 0.9.0 y, con él, Zaris alcanza la disponibilidad general. Está listo para producción, es multiplataforma y ahora cuenta con soporte comercial.

Si aún no te has topado con él: Zaris es un almacén clave/valor distribuido y en memoria para .NET. Es rápido, replicado, y puedes operarlo desde un navegador, PowerShell o directamente desde tu aplicación. Esta versión es la culminación de ese trabajo: el punto en el que un "proyecto prometedor" se convierte en "algo que puedes poner en producción".

Construye Sistemas Reactivos con Clustron Watch

· 6 min de lectura

La mayoría de los sistemas distribuidos no empiezan siendo complicados.

Empiezan con algo sencillo: un bucle.

Escribes un pequeño trabajador que comprueba si hay trabajo:

while (true)
{
var job = await client.GetAsync<Job>("jobs:next");

if (job.IsSuccess)
{
await ProcessAsync(job.Value);
}

await Task.Delay(1000);
}

En esta etapa, todo parece razonable. La lógica es clara, el sistema se comporta de forma predecible y la carga cognitiva es muy baja.

Pero a medida que el sistema crece, este patrón se convierte silenciosamente en una de las partes más costosas de tu arquitectura.


El coste oculto del sondeo en los sistemas distribuidos​

El sondeo introduce una ineficiencia fundamental: estás preguntando repetidamente al sistema por cambios que normalmente no se han producido.

A medida que escalas:

  • Varias instancias comienzan a sondear los mismos datos
  • Cada instancia realiza lecturas repetidas, la mayoría de las cuales no devuelven información útil
  • Introduces latencia artificial: los cambios solo se detectan en el siguiente ciclo de sondeo
  • El sistema empieza a consumir recursos simplemente para confirmar que nada ha cambiado

Esto conduce a un sistema que es técnicamente correcto, pero operativamente ineficiente.

Y lo que es más importante, el sondeo te obliga a hacer concesiones que no deberías tener que hacer:

  • Sondear con frecuencia → mayor coste, menor latencia
  • Sondear con menos frecuencia → menor coste, mayor latencia

No hay una respuesta correcta, solo un compromiso.


Un modelo diferente: reaccionar en lugar de preguntar​

La alternativa no es solo una optimización: es una forma diferente de pensar en los sistemas.

En lugar de preguntar:

"¿Ha cambiado algo?"

Diseñas tu sistema para que responda a:

"Algo acaba de cambiar."

Este es el cambio de un modelo basado en extracción (pull) a un modelo basado en inserción (push).

En Clustron, esto se implementa mediante Watch.


Modelo de sondeo frente a observación de cambios


Presentando Watch: un flujo de cambios de estado​

Clustron Watch te permite suscribirte a los cambios del sistema y recibir eventos a medida que ocurren.

En esencia, Watch convierte tus datos en un flujo continuo de actualizaciones.

Observar una sola clave​

var (subscription, initialRecord) = await client.Watch.WatchKeyAsync(
"order:1001",
new WatchOptions { IncludeInitialSnapshot = true },
ev =>
{
Console.WriteLine($"{ev.EventType} → {ev.Key} = {ev.Value}");
});

Esto hace dos cosas importantes:

  1. Te proporciona el estado actual (initialRecord)
  2. Te suscribe a todos los cambios futuros

Esta combinación es fundamental. Sin ella, tendrías que obtener manualmente el valor actual y luego suscribirte, lo que introduce condiciones de carrera.


Instantánea + streaming: una garantía de corrección​

Uno de los aspectos sutiles pero esenciales de Watch es la capacidad de incluir una instantánea inicial.

Cuando IncludeInitialSnapshot está habilitado:

  • Recibes el valor actual en el momento de la suscripción
  • Luego recibes todas las actualizaciones posteriores en orden

Esto garantiza que:

  • No pierdas el estado existente
  • No pierdas los cambios futuros
  • No necesites lógica de sincronización adicional

En otras palabras, Watch proporciona una vista completa y consistente de la evolución del estado.


Observar colecciones: más allá de las claves individuales​

Si bien observar una sola clave es útil, la mayoría de los sistemas reales operan sobre grupos de datos relacionados.

Clustron admite esto mediante la observación basada en prefijos:

var subscription = await client.Watch.WatchPrefixAsync(
"jobs:",
new WatchOptions { IncludeInitialSnapshot = false },
ev =>
{
Console.WriteLine($"Job update: {ev.Key}");
});

Esto te permite observar todos los cambios dentro de un grupo lógico.

Desde una perspectiva arquitectónica, aquí es donde Watch se vuelve significativamente más potente:

  • Ya no reaccionas a valores individuales
  • Reaccionas a transiciones de estado de todo el sistema

Comprender el modelo de eventos​

Cada cambio en el sistema produce un WatchEvent.

Un evento típico contiene:

  • La clave que cambió
  • El tipo de cambio (Put o Delete)
  • El nuevo valor (si corresponde)
  • Una revisión que representa la versión del cambio

Esta revisión es particularmente importante. Proporciona un orden y te permite razonar sobre la secuencia de eventos en un sistema distribuido.

En efecto, Watch te proporciona un registro de transiciones de estado, entregado en tiempo real.


Qué permite esto en la práctica​

Una vez que adoptas este modelo, varios problemas comunes se simplifican.

Procesamiento de trabajos​

En lugar de sondear en busca de nuevo trabajo, los trabajadores reaccionan de inmediato cuando se crean o actualizan trabajos.

Esto elimina los ciclos inactivos y reduce la latencia.


Invalidación de caché​

En lugar de depender del TTL o de la actualización periódica, las cachés pueden actualizarse a sí mismas en respuesta a cambios reales.

Esto mejora tanto la frescura como la eficiencia.


Coordinación distribuida​

Los sistemas que dependen de un estado compartido —como la elección de líder o la propiedad de concesiones— se benefician significativamente de las actualizaciones en tiempo real.

Los cambios se propagan al instante, lo que permite una coordinación más rápida y fiable.


Presencia y supervisión​

El seguimiento de los componentes activos se vuelve trivial:

  • Cuando aparece un nodo → recibes un evento
  • Cuando desaparece un nodo → recibes un evento

Sin sondeo, sin conjeturas.


Consideraciones operativas​

Watch está diseñado para cargas de trabajo de larga duración y orientadas a eventos.

Algunas consideraciones importantes:

Mantén los controladores ligeros​

Tu controlador de eventos no debe realizar trabajo pesado directamente.

En su lugar:

  • Procesa rápidamente
  • Delega las tareas más pesadas a trabajadores en segundo plano

Gestiona el ciclo de vida de la suscripción​

Detén siempre las suscripciones cuando ya no sean necesarias:

await subscription.StopAsync();

Esto garantiza que los recursos se liberen de forma limpia.


Comprende el alcance​

  • Observar una clave → solo esa clave produce eventos
  • Observar un prefijo → solo las claves coincidentes producen eventos

Los eventos están delimitados y aislados, lo que evita interferencias no deseadas entre componentes.


De los bucles de sondeo a los sistemas reactivos​

El cambio del sondeo a la observación no se trata solo de rendimiento.

Cambia la estructura de tu aplicación.

Los sistemas de sondeo se construyen en torno a:

Loop → Check → Wait → Repeat

Los sistemas reactivos se construyen en torno a:

Event → Handler → Action

Esto da lugar a sistemas que son:

  • Más receptivos
  • Más eficientes
  • Más fáciles de razonar

Reflexiones finales​

El sondeo suele ser la forma más sencilla de empezar y, en sistemas pequeños, funciona lo suficientemente bien.

Pero a medida que los sistemas crecen, el coste del sondeo se vuelve cada vez más difícil de justificar.

Clustron Watch ofrece un enfoque diferente: uno en el que tu sistema reacciona al cambio en lugar de buscarlo.

Stop asking your system if something changed.

Let it tell you when it does.

Primeros pasos​

Prueba una observación sencilla:

await client.Watch.WatchPrefixAsync("orders:", null, ev =>
{
Console.WriteLine($"{ev.Key} updated");
});

A partir de ahí, empieza a eliminar los bucles de sondeo, uno por uno.

Rápidamente descubrirás que tu sistema se vuelve no solo más rápido, sino fundamentalmente más simple.

Caché Distribuida en .NET

· 4 min de lectura

El uso de caché es una de las formas más eficaces de mejorar el rendimiento de una aplicación.
La mayoría de las aplicaciones de .NET comienzan con una simple caché en memoria usando MemoryCache o IMemoryCache. Esto funciona perfectamente cuando la aplicación se ejecuta en un solo servidor.

Sin embargo, las aplicaciones modernas rara vez se ejecutan en una sola máquina.

Cuando las aplicaciones escalan horizontalmente en varias instancias, la caché en memoria deja de funcionar de forma fiable. Cada instancia mantiene su propia caché, lo que significa que el estado de la aplicación se vuelve inconsistente en todo el clúster.

Aquí es donde la caché distribuida se vuelve esencial.


Caché Distribuida


El problema con las cachés en memoria

Considera una aplicación típica de ASP.NET desplegada detrás de un balanceador de carga.

          Load Balancer
|
---------------------------
| | |
Server A Server B Server C

Cada servidor tiene su propia caché en memoria.

Si una solicitud actualiza datos en caché en el Servidor A, las cachés del Servidor B y del Servidor C quedan desactualizadas.

Esto da lugar a varios problemas:

  • estado de la aplicación inconsistente
  • entradas de caché obsoletas
  • calentamientos de caché duplicados
  • comportamiento impredecible

Para resolver este problema, la caché debe ser compartida entre todos los nodos.


¿Qué es una caché distribuida?

Una caché distribuida es una caché centralizada o en clúster a la que pueden acceder varias instancias de la aplicación.

En lugar de almacenar los datos en caché dentro del proceso de la aplicación, los datos se almacenan en un almacén clave‑valor distribuido.

           Application Nodes

App A App B App C
| | |
+---------+---------+
|
Distributed Cache

Todos los nodos de la aplicación leen y escriben entradas de caché desde el mismo almacén distribuido.

Esto garantiza:

  • datos en caché consistentes
  • estado de la aplicación compartido
  • mejor escalabilidad

Ejemplo básico de caché distribuida en .NET

Una caché distribuida se utiliza normalmente a través de una abstracción como IDistributedCache.

Ejemplo:

public class ProductService
{
private readonly IDistributedCache _cache;

public ProductService(IDistributedCache cache)
{
_cache = cache;
}

public async Task<string?> GetProductAsync(string id)
{
var cached = await _cache.GetStringAsync(id);

if (cached != null)
return cached;

var product = await LoadProductFromDatabase(id);

await _cache.SetStringAsync(id, product);

return product;
}

private Task<string> LoadProductFromDatabase(string id)
{
return Task.FromResult($"Product {id}");
}
}

La aplicación comprueba primero la caché. Si el valor no está presente, los datos se cargan desde la base de datos y luego se almacenan en la caché.

Este patrón se denomina caché aparte (cache‑aside).


Soluciones comunes de caché distribuida

Varias tecnologías proporcionan capacidades de caché distribuida.

Redis​

Redis es una de las cachés distribuidas más utilizadas.

Proporciona:

  • almacenamiento clave‑valor
  • alto rendimiento
  • operaciones en memoria

Sin embargo, Redis a menudo requiere componentes adicionales cuando se utiliza para la coordinación distribuida.


Cachés respaldadas por bases de datos​

Algunas aplicaciones utilizan bases de datos relacionales como cachés.

Aunque es sencillo, este enfoque introduce:

  • contención en la base de datos
  • rendimiento reducido
  • escalabilidad limitada

Plataformas clave‑valor distribuidas​

Los sistemas distribuidos modernos a menudo utilizan plataformas clave‑valor distribuidas que proporcionan tanto caché como primitivas de coordinación.

Estos sistemas admiten:

  • caché distribuida
  • bloqueos distribuidos
  • elección de líder
  • notificaciones de cambios
  • coordinación del clúster

Uso de Clustron como caché distribuida

Clustron proporciona una plataforma clave‑valor distribuida diseñada para aplicaciones de .NET.

Como se ejecuta como un clúster distribuido, todas las instancias de la aplicación interactúan con el mismo almacén de datos compartido.

Ejemplo:

using Clustron.DKV.Client;

var client = await DKVClient.InitializeRemote(
"teststore",
new[]
{
new DkvServerInfo("localhost", 7861)
});

await client.PutAsync("product:1", "Laptop");

var value = await client.GetAsync<string>("product:1");

Console.WriteLine(value);

En este ejemplo:

  • los nodos de la aplicación se conectan al clúster
  • los datos se almacenan en el almacén distribuido
  • todos los nodos ven los mismos datos

Esto permite que la caché permanezca consistente en todo el clúster.


Cuándo usar la caché distribuida

La caché distribuida es especialmente útil en sistemas que:

  • ejecutan varias instancias de la aplicación
  • procesan grandes volúmenes de solicitudes
  • dependen en gran medida de las lecturas de la base de datos
  • requieren un estado de aplicación compartido

Los casos de uso típicos incluyen:

  • aplicaciones web de ASP.NET
  • arquitecturas de microservicios
  • sistemas de procesamiento en segundo plano
  • plataformas de API

Resumen

La caché en memoria funciona bien para aplicaciones de un solo servidor, pero los sistemas distribuidos modernos requieren una capa de caché compartida.

La caché distribuida permite que varias instancias de la aplicación compartan los mismos datos en caché, mejorando la escalabilidad y la consistencia.

Plataformas como Clustron proporcionan un almacén clave‑valor distribuido que facilita la implementación de la caché distribuida y la coordinación en aplicaciones de .NET.

Elección de Líder en Sistemas Distribuidos

· 5 min de lectura

Cuando las aplicaciones escalan a través de varias máquinas, la coordinación se convierte en uno de los problemas más difíciles de resolver.
Los sistemas distribuidos a menudo ejecutan varios nodos idénticos que pueden realizar todos el mismo trabajo. Sin coordinación, esos nodos pueden intentar la misma tarea simultáneamente, lo que provoca trabajo duplicado, condiciones de carrera o estados inconsistentes.

Una de las formas más comunes de resolver este problema es la elección de líder.

La elección de líder garantiza que un nodo se convierta temporalmente en el líder mientras los demás nodos permanecen como seguidores. El líder realiza las tareas de coordinación mientras los seguidores esperan a que cambie el liderazgo.

Este patrón se utiliza ampliamente en bases de datos distribuidas, planificadores de trabajos, plataformas de orquestación de contenedores y sistemas de configuración.


Elección de Líder en Sistemas Distribuidos


Un ejemplo sencillo

Imagina un sistema distribuido de trabajadores con tres nodos:

Worker A Worker B Worker C

Cada trabajador es capaz de ejecutar trabajos en segundo plano.

Si todos los trabajadores ejecutan el mismo trabajo programado, el trabajo se ejecutará tres veces.
En cambio, queremos que exactamente un trabajador ejecute el trabajo.

Aquí es donde la elección de líder resulta útil. Un trabajador se convierte en el líder, y solo el líder ejecuta el trabajo.


El patrón más simple de elección de líder

Un enfoque común utiliza un almacén de coordinación compartido.

La idea es sencilla:

  1. Cada nodo intenta adquirir el liderazgo.
  2. El nodo que lo consigue se convierte en el líder.
  3. El líder renueva periódicamente su liderazgo.
  4. Si el líder falla, otro nodo toma el relevo.

Esto se implementa normalmente mediante una concesión (lease).

Una concesión actúa como un bloqueo con un tiempo de expiración. Si el nodo que mantiene la concesión desaparece, otro nodo puede adquirir el liderazgo de forma segura.


Un ejemplo mínimo en .NET

El siguiente ejemplo simplificado demuestra la idea detrás del liderazgo basado en concesiones.

public class LeaderElection
{
private readonly ILeaseStore _store;
private readonly string _nodeId;

public LeaderElection(ILeaseStore store, string nodeId)
{
_store = store;
_nodeId = nodeId;
}

public async Task RunAsync()
{
while (true)
{
var acquired = await _store.TryAcquireLeaseAsync("cluster-leader", _nodeId, TimeSpan.FromSeconds(10));

if (acquired)
{
Console.WriteLine($"{_nodeId} is leader");

await PerformLeaderWork();

await _store.RenewLeaseAsync("cluster-leader", _nodeId);
}

await Task.Delay(2000);
}
}

private Task PerformLeaderWork()
{
Console.WriteLine("Running scheduled tasks...");
return Task.CompletedTask;
}
}

En este ejemplo:

  • Cada nodo intenta adquirir la concesión "cluster-leader".
  • El nodo que lo consigue se convierte en el líder.
  • El líder renueva periódicamente la concesión.
  • Si el líder se bloquea, la concesión expira y otro nodo toma el relevo.

Este patrón proporciona conmutación por error automática.


Problemas con la elección de líder hecha a mano

Aunque el ejemplo anterior parece sencillo, los sistemas distribuidos del mundo real introducen complicaciones:

Las particiones de red pueden hacer que los nodos crean que siguen siendo líderes.

La deriva del reloj puede provocar problemas de expiración de la concesión.

La detección lenta de fallos puede retrasar la transferencia del liderazgo.

Construir un mecanismo robusto de elección de líder requiere gestionar cuidadosamente los casos límite de los sistemas distribuidos.

Por eso muchos sistemas se apoyan en plataformas de coordinación especializadas como:

  • etcd
  • ZooKeeper
  • Consul

Estos sistemas implementan primitivas de coordinación fiables.


Elección de líder con Clustron

Clustron proporciona una plataforma de clustering para sistemas distribuidos en .NET.

En lugar de construir tú mismo la infraestructura de coordinación, las aplicaciones pueden utilizar las primitivas integradas de la plataforma.

Ejemplo usando el concepto de concesión de Clustron:

using Clustron.DKV.Client;

var client = await DKVClient.InitializeRemote(
"teststore",
new[] { new DkvServerInfo("localhost", 7861) });

var lease = await client.GrantLeaseAsync(TimeSpan.FromSeconds(10));

if (lease.Acquired)
{
Console.WriteLine("This node is the leader.");

while (true)
{
await client.RefreshLeaseAsync(lease.Id);
await Task.Delay(3000);
}
}

Aquí el nodo solicita una concesión del clúster.

Si se concede la concesión, el nodo se convierte en el líder.
Mientras el nodo siga renovando la concesión, el liderazgo permanece válido.

Si el nodo se bloquea o deja de renovar la concesión, otro nodo puede adquirir el liderazgo automáticamente.


Cuándo es útil la elección de líder

La elección de líder aparece en muchos sistemas distribuidos del mundo real:

  • Planificadores de trabajos distribuidos
  • Procesadores de tareas en segundo plano
  • Gestores de clúster
  • Coordinación de microservicios
  • Controladores de configuración

Cada vez que un sistema requiere que exactamente un nodo coordine la actividad, la elección de líder se vuelve esencial.


Resumen

La elección de líder es un bloque de construcción fundamental para los sistemas distribuidos.

Permite que los clústeres de nodos coordinen su comportamiento manteniendo al mismo tiempo una alta disponibilidad.

Un enfoque basado en concesiones proporciona una estrategia de implementación sencilla y eficaz, y plataformas distribuidas como Clustron ponen estas primitivas directamente a disposición de las aplicaciones de .NET.

En lugar de construir la lógica de coordinación desde cero, los desarrolladores pueden centrarse en el comportamiento de la aplicación mientras la plataforma de clustering gestiona el liderazgo, la conmutación por error y la coordinación.

Bloqueos Distribuidos Explicados

· 5 min de lectura
Clustron Team
Ingeniería de Sistemas Distribuidos

Una guía práctica para desarrolladores de .NET

Introducción​

A medida que las aplicaciones escalan a través de varias máquinas, la coordinación se convierte en uno de los aspectos más desafiantes del diseño de sistemas.

En una aplicación de una sola máquina, la exclusión mutua es sencilla. Los desarrolladores pueden apoyarse en primitivas del lenguaje como lock en C#, Mutex o Semaphore para garantizar que solo un hilo acceda a un recurso compartido a la vez.

Sin embargo, estas primitivas solo funcionan dentro de un único proceso o máquina.

Una vez que las aplicaciones se ejecutan en varios servicios o nodos, los mecanismos de bloqueo tradicionales dejan de funcionar. Dos servicios que se ejecutan en diferentes máquinas no pueden apoyarse en una instrucción lock local para coordinar el acceso a un estado compartido.

Aquí es donde los bloqueos distribuidos se vuelven esenciales.

Los bloqueos distribuidos permiten que los servicios de un clúster coordinen el acceso a recursos compartidos garantizando al mismo tiempo que solo un participante mantenga el bloqueo en un momento dado.


Bloqueos locales frente a bloqueos distribuidos​

Un bloqueo local típico en C# tiene este aspecto:

private static readonly object _lock = new object();

public void ProcessOrder()
{
lock(_lock)
{
Console.WriteLine("Processing order safely.");
}
}

Esto funciona perfectamente dentro de un único proceso.

Sin embargo, si tu aplicación se ejecuta en varias instancias de servicio, cada instancia tiene su propio objeto _lock. El bloqueo ya no coordina el trabajo en todo el sistema.

Worker A → lock acquired Worker B → lock acquired Worker C → lock acquired

Todos los trabajadores proceden simultáneamente, lo que anula el propósito del bloqueo.

Los sistemas distribuidos requieren un mecanismo de coordinación compartido.


Bloqueo Distribuido


La idea detrás de los bloqueos distribuidos​

Un bloqueo distribuido utiliza un sistema compartido para representar la propiedad del bloqueo.

En lugar de bloquear la memoria, un nodo registra la propiedad del bloqueo en un sistema de coordinación distribuida como:

  • un almacén clave‑valor distribuido
  • un servicio de coordinación
  • una caché distribuida

Concepto de ejemplo:

Lock Key: order:123 Owner: worker‑1

Si otro trabajador intenta adquirir el mismo bloqueo, la operación falla hasta que el bloqueo se libera o expira.


Escenario del mundo real: procesamiento distribuido de facturas​

Imagina un clúster de trabajadores que procesan facturas.

Worker A Worker B Worker C

Todos los trabajadores buscan facturas pendientes.

Sin bloqueo:

Worker A → processes invoice #101 Worker B → processes invoice #101 Worker C → processes invoice #101

La misma factura podría procesarse varias veces.

Un bloqueo distribuido garantiza que solo un trabajador procese la factura.


Bloqueos distribuidos con Clustron DKV​

Clustron DKV proporciona primitivas integradas para la coordinación distribuida.

Los bloqueos se gestionan a través de la API de bloqueos (Locks API), y las operaciones sobre claves bloqueadas deben incluir el identificador de bloqueo (lock handle) correcto.


Adquirir un bloqueo​

Un cliente adquiere primero un bloqueo para una clave específica.

var locks = ((IDkv)client).Locks;

var key = "order:123";

var lockHandle = await locks.AcquireAsync(key, TimeSpan.FromSeconds(10));

if (lockHandle == null)
{
Console.WriteLine("Failed to acquire lock.");
return;
}

Console.WriteLine("Lock acquired.");

El identificador de bloqueo devuelto representa la propiedad del bloqueo.


Realizar operaciones bajo el bloqueo​

Una vez adquirido un bloqueo, las operaciones deben incluir el identificador de bloqueo.

var result = await client.PutAsync(
key,
"processed",
Put.WithLock(lockHandle.Handle));

if (result.IsSuccess)
{
Console.WriteLine("Value updated safely.");
}

Solo el propietario del bloqueo puede modificar el valor.


Lectura con el bloqueo​

Las lecturas también pueden respetar la propiedad del bloqueo.

var value = await client.GetAsync<string>(
key,
Get.WithLock(lockHandle.Handle));

if (value.IsSuccess)
{
Console.WriteLine($"Value: {value.Value}");
}

Operaciones en competencia sin el bloqueo​

Si otro trabajador intenta modificar la clave sin el bloqueo, la operación falla.

var attempt = await client.PutAsync(key, "new-value");

if (!attempt.IsSuccess)
{
Console.WriteLine($"Operation rejected: {attempt.Status}");
}

Resultado:

KvStatus.Locked

Esto evita la modificación concurrente del mismo recurso.


Liberar el bloqueo​

Una vez finalizado el trabajo, el bloqueo puede liberarse.

await lockHandle.ReleaseAsync();

Console.WriteLine("Lock released.");

Después de liberar el bloqueo, otro trabajador puede adquirirlo.


Flujo de trabajo de ejemplo en el clúster​

Worker A → Acquire Lock → Success Worker B → Acquire Lock → Failed Worker C → Acquire Lock → Failed

Worker A → Update Key Worker A → Release Lock

Solo un trabajador realiza la operación, lo que garantiza la corrección.


Gestión de fallos​

Los bloqueos en Clustron tienen un límite de tiempo.

Al adquirir un bloqueo, debe especificarse una duración:

await locks.AcquireAsync(key, TimeSpan.FromSeconds(10));

Si el cliente que mantiene el bloqueo se bloquea o se desconecta, el bloqueo expira finalmente, lo que permite que otro trabajador lo adquiera.

Esto evita que los bloqueos permanezcan retenidos de forma permanente.


Buenas prácticas​

Al implementar bloqueos distribuidos:

  • Mantén las secciones críticas cortas
  • Evita los bloqueos globales cuando sea posible
  • Particiona los bloqueos por recurso
  • Diseña siempre pensando en los escenarios de fallo

Reflexiones finales​

Los bloqueos distribuidos son una de las primitivas de coordinación más fundamentales en los sistemas distribuidos.

Permiten que los servicios que se ejecutan en varias máquinas coordinen de forma segura el acceso a recursos compartidos preservando al mismo tiempo la corrección del sistema.

Plataformas como Clustron DKV lo simplifican proporcionando primitivas de bloqueo integradas, operaciones atómicas y sólidas garantías de consistencia.

En el próximo artículo, exploraremos otra primitiva esencial de los sistemas distribuidos:

La Elección de Líder.

Construcción de Sistemas Distribuidos

· 5 min de lectura
Clustron Team
Ingeniería de Sistemas Distribuidos

Una guía práctica para desarrolladores de .NET

Introducción​

La mayoría de los sistemas backend comienzan su vida en una sola máquina.

Una aplicación de ASP.NET Core procesa solicitudes, una base de datos almacena los datos de la aplicación y los trabajadores en segundo plano gestionan tareas asíncronas como el envío de correos electrónicos o la generación de informes. En este entorno, la concurrencia es manejable y la coordinación entre componentes es relativamente sencilla.

Sin embargo, a medida que el sistema crece, las cosas empiezan a cambiar.

El tráfico aumenta, se despliegan más instancias de la aplicación detrás de un balanceador de carga y los servicios comienzan a ejecutarse en varias máquinas. Lo que antes era una sola aplicación evoluciona hacia un sistema distribuido.

En este punto, la naturaleza del problema cambia drásticamente.

Los desafíos ya no se limitan a escribir una lógica de negocio correcta. Ahora los desarrolladores deben lidiar con la coordinación entre máquinas, los fallos de red, las interrupciones parciales y el mantenimiento de la consistencia en el estado distribuido.

Aquí es donde comienza la ingeniería de sistemas distribuidos.


Arquitectura de Sistemas Distribuidos


¿Qué es un sistema distribuido?​

Un sistema distribuido es un conjunto de computadoras independientes que aparecen ante el usuario como un único sistema coherente.

En lugar de ejecutar toda la lógica en una sola máquina, las responsabilidades se distribuyen entre varios nodos que se comunican a través de la red.

Algunos ejemplos son:

  • Arquitecturas de microservicios
  • Bases de datos distribuidas
  • Cachés en clúster
  • Colas de mensajes
  • Plataformas de infraestructura en la nube

Estos sistemas proporcionan escalabilidad y resiliencia, pero introducen nuevas complejidades.


Por qué los sistemas distribuidos son difíciles​

A diferencia de una aplicación de una sola máquina, los sistemas distribuidos deben lidiar con realidades como:

Fallos de red​

En los sistemas distribuidos, la comunicación se produce a través de una red. Las redes pueden ser lentas, poco fiables o estar temporalmente no disponibles.

Una solicitud puede fallar porque el nodo remoto se bloqueó, porque la red descartó paquetes o porque se produjo un tiempo de espera agotado.

El sistema debe gestionar estos escenarios con elegancia.


Fallos parciales​

En un sistema distribuido, los componentes pueden fallar de forma independiente.

Un nodo de un clúster puede bloquearse mientras otros permanecen en buen estado. Una instancia de servicio puede reiniciarse mientras otros servicios siguen ejecutándose.

Esto significa que el sistema debe funcionar correctamente incluso cuando solo una parte de él está operativa.


Desafíos de consistencia​

Cuando los datos se replican en varios nodos, mantenerlos consistentes se vuelve difícil.

Por ejemplo:

  • Dos nodos pueden intentar actualizar los mismos datos simultáneamente.
  • Los retrasos de red pueden provocar que los nodos difieran temporalmente sobre el estado actual.
  • Un nodo puede bloquearse después de realizar solo una parte de una operación.

Diseñar sistemas que mantengan un estado consistente a pesar de estos escenarios requiere una coordinación cuidadosa.


Bloques de construcción clave de los sistemas distribuidos​

La mayoría de los sistemas distribuidos se basan en unas pocas primitivas fundamentales.

Comprender estas primitivas ayuda a los desarrolladores a diseñar arquitecturas fiables.


Almacenamiento distribuido​

A menudo, los datos deben almacenarse en varias máquinas.

Esto permite:

  • escalado horizontal
  • tolerancia a fallos
  • alta disponibilidad

Los almacenes clave-valor distribuidos se utilizan comúnmente para este propósito.


Coordinación​

Los nodos deben coordinar acciones como:

  • elección de líder
  • bloqueo distribuido
  • pertenencia al clúster
  • actualizaciones de configuración

Sin mecanismos de coordinación, varios nodos podrían intentar operaciones conflictivas.


Consenso​

Algunos sistemas requieren garantías sólidas sobre el estado compartido.

Los algoritmos de consenso como Raft o Paxos permiten que los nodos lleguen a acuerdos sobre decisiones incluso en presencia de fallos.

Estos algoritmos son fundamentales para las bases de datos distribuidas y los servicios de coordinación.


Particionamiento y replicación​

Los datos normalmente se:

  • particionan entre nodos para distribuir la carga
  • replican para proteger contra fallos

Equilibrar el rendimiento, la disponibilidad y la consistencia es uno de los principales desafíos de diseño.


Patrones comunes de los sistemas distribuidos​

Varios patrones aparecen repetidamente en los sistemas del mundo real.

Algunos ejemplos son:

  • Elección de líder para coordinar las actividades del clúster
  • Bloqueos distribuidos para garantizar la exclusión mutua
  • Colas de trabajos para el procesamiento asíncrono
  • Limitadores de tasa para controlar el volumen de solicitudes
  • Protocolos de transacción para actualizaciones atómicas entre varios nodos

Estos patrones forman la base de muchas plataformas a gran escala.


El papel de los sistemas de coordinación​

Para simplificar el desarrollo distribuido, muchas organizaciones se apoyan en sistemas de coordinación como:

  • etcd
  • ZooKeeper
  • Consul

Estos sistemas proporcionan primitivas como:

  • elección de líder
  • bloqueos distribuidos
  • pertenencia al clúster
  • almacenamiento de configuración

Las aplicaciones pueden entonces construir comportamientos de nivel superior sobre estas primitivas.


Los sistemas distribuidos en el ecosistema .NET​

Aunque las herramientas de infraestructura distribuida se asocian a menudo con otros ecosistemas, las aplicaciones de .NET operan cada vez más en entornos distribuidos.

Los sistemas modernos de .NET utilizan con frecuencia:

  • Kubernetes
  • cachés distribuidas
  • microservicios
  • arquitecturas orientadas a eventos

Como resultado, la coordinación y la gestión del estado distribuido son cada vez más importantes también para los desarrolladores de .NET.


Reflexiones finales​

Construir sistemas distribuidos es fundamentalmente diferente de construir aplicaciones tradicionales de una sola máquina.

Los desafíos de la comunicación en red, los fallos parciales y la consistencia de los datos introducen complejidades que requieren patrones e infraestructura especializados.

Afortunadamente, al comprender los bloques de construcción clave —almacenamiento, coordinación, consenso y replicación— los desarrolladores pueden diseñar sistemas que escalan de forma fiable y permanecen resilientes ante los fallos.

En futuros artículos, exploraremos algunas de estas primitivas con más detalle, incluida la elección de líder, los patrones de coordinación distribuida y las implementaciones prácticas en .NET.