Bloqueos distribuidos
1) Por qué (y cuándo) se necesitan bloqueos distribuidos
Un bloqueo distribuido es un mecanismo que garantiza la exclusión mutua de una sección crítica entre varios nodos de un clúster. Tareas típicas:- Liderazgo (leader election) para el problema de fondo/scheduler.
- Limitar un único ejecutante sobre un recurso compartido (mover archivos, migrar esquemas, paso de pago exclusivo).
- Procesamiento secuencial de la unidad (wallet/order) si no es posible lograr la idempotencia/ordenación de otra manera.
- Si puede hacer upsert idempotent, CAS (compare-and-set) o cola por clave (por-key ordering).
- Si el recurso permite operaciones conmutativas (CRDT, contadores).
- Si el problema se resuelve mediante una transacción en el mismo almacén.
2) Modelo de amenazas y propiedades
Fallos y dificultades:- Red: retrasos, partición (partition), pérdida de paquetes.
- Procesos: pausa GC, stop-the-world, crash después de tomar la cerradura.
- Tiempo: la deriva del reloj y el desplazamiento rompen los enfoques TTL.
- Volver a poseer: el proceso de "zombis' después de la red puede pensar que todavía es dueño del castillo.
- Seguridad: no más de un propietario válido (seguridad).
- Vitalidad: la cerradura se libera cuando el propietario falla (liveness).
- Justicia: no hay hambre.
- Independencia del reloj: la corrección no depende del wall-clock (o compensado por el fencing tokens).
3) Modelos principales
3. 1 Lease (castillo de alquiler)
El castillo se emite con TTL. El propietario debe extenderlo hasta su vencimiento (heartbeat/keepalive).
Pros: autocomplacencia con crash.
Riesgos: si el propietario «raya» y sigue trabajando, pero ha perdido la extensión, puede surgir una doble posesión.
3. 2 Fencing token (token de valla)
Con cada captura exitosa, se emite un número de crecimiento monótono. Los usuarios de recursos (DAB, cola, almacenamiento de archivos) comprueban el token y rechazan las operaciones con el número antiguo.
Esto es extremadamente importante con TTL/lease y particiones de red: protege contra el propietario «viejo».
3. 3 cerraduras Quorum (sistemas CP)
Se utiliza el consenso distribuido (Raft/Paxos; etcd/ZooKeeper/Consul), la entrada está asociada a la logia de consenso → no hay división-break en la mayoría de los nodos.
Además: fuertes garantías de seguridad.
Menos: sensibilidad al quórum (cuando se pierde, la vitalidad coja).
3. 4 candados AP (in-memory/caché + replicación)
Por ejemplo, Redis-clúster. Alta disponibilidad y velocidad, pero sin fuertes garantías de seguridad en las particiones de red. Requieren fencing en el lado azul.
4) Plataformas y patrones
4. 1 etcd/ZooKeeper/Consul (recomendado para locks de strong)
Nodos efímeros (ZK) o sesiones/leases (etcd): la clave existe mientras la sesión está en vivo.
Keepalive de la sesión; pérdida de quórum → la sesión expira → el castillo se libera.
Nodos ordinales (ZK 'EPHEMERAL _ SEQUENTIAL') para cola de espera → equidad.
go cli, _:= clientv3. New(...)
lease, _:= cli. Grant(ctx, 10) // 10s lease sess, _:= concurrency. NewSession(cli, concurrency. WithLease(lease. ID))
m:= concurrency. NewMutex(sess, "/locks/orders/42")
if err:= m. Lock(ctx); err!= nil { / handle / }
defer m. Unlock(ctx)
4. 2 Redis (suavemente)
El clásico es 'SET key value NX PX ttl'.
Problemas:- Replicación/Failover puede admitir propietarios simultáneos.
- Redlock de múltiples instancias reduce el riesgo, pero no elimina; discutido en entornos con una red poco fiable.
Es más seguro aplicar Redis como una capa de coordinación rápida, pero siempre complementar fencing token en el recurso de destino.
Ejemplo (Lua-unlock):lua
-- release only if value matches if redis. call("GET", KEYS[1]) == ARGV[1] then return redis. call("DEL", KEYS[1])
else return 0 end
4. 3 candados DB
PostgreSQL advisory locks: look dentro del clúster Postgres (proceso/sesión).
Bueno, cuando todas las secciones críticas y así en el mismo DB.
sql
SELECT pg_try_advisory_lock(42); -- take
SELECT pg_advisory_unlock(42); -- let go
4. 4 Archivos/« cerraduras »en la nube
S3/GCS + metadato de objeto flock con condiciones 'If-Match' (ETag) → esencialmente CAS.
Adecuado para backups/migraciones.
5) Diseño de cerradura segura
5. 1 Identidad del propietario
Almacene 'owner _ id' (nodo # proceso # pid # start _ time) + token aleatorio para conciliar cuando no esté bloqueado.
Un segundo bloqueo no debe quitar la cerradura de otra persona.
5. 2 TTL y renovación
TTL <T_fail_detect (tiempo de detección de fallos) y ≥ p99 de funcionamiento de la sección crítica × stock.
Extensión - periódicamente (por ejemplo, cada 'TTL/3'), con deadline.
5. 3 Fencing token en el azul
La sección que modifica un recurso externo debe pasar 'fencing _ token'.
Cink (BD/caché/almacenamiento) almacena 'last _ token' y rechaza los más pequeños:sql
UPDATE wallet
SET balance = balance +:delta, last_token =:token
WHERE id =:id AND:token > last_token;
5. 4 Cola de espera y justicia
En ZK - 'EPHEMERAL _ SEQUENTIAL' y observadores: el cliente espera la liberación de su predecesor más cercano.
En etcd: claves con revisión/versión; el orden de prioridad por 'mod _ revision'.
5. 5 Comportamiento con split-brain
Enfoque CP: sin quórum no se puede tomar la cerradura - es mejor pararse que romper la seguridad.
Enfoque AP: se permite el progreso en las islas divididas → se necesita fencing.
6) Liderazgo (leader election)
En etcd/ZK - «leader» es una exclusiva llave de epímero; el resto están firmados para cambios.
El líder escribe heartbeats; pérdida - reelección.
Todas las operaciones del líder acompañan a fencing token (número de era/revisión).
7) Errores y su procesamiento
El cliente tomó la cerradura, pero el crash antes de que funcionen las normas →, nadie se verá afectado; TTL/sesión será liberado.
El castillo expiró en medio del trabajo:- Watchdog es obligatorio: si la extensión falla - abortar la sección crítica y retroceder/compensar.
- No hay «dodel sudor»: sin candado no se puede seguir con la sección crítica.
Una larga pausa (GC/stop-the-world) → la prórroga no ocurrió, otro se llevó el candado. El flujo de trabajo debe detectar la pérdida de propiedad (canal keepalive) e interrumpir.
8) Dedlock, prioridades e inversión
Los dedlocks en el mundo distribuido son raros (el castillo suele ser uno), pero si hay varios castillos, póngase en un solo orden de toma (lock ordering).
Inversión de prioridades: el propietario de baja prioridad mantiene el recurso mientras los de alta prioridad esperan. Soluciones: límites TTL, preemption (si el negocio lo permite), sharding del recurso.
Ayunar: utilice las colas de espera (ZK-suborden nodos) para hacer justicia.
9) Observabilidad
Métricas:- `lock_acquire_total{status=ok|timeout|error}`
- `lock_hold_seconds{p50,p95,p99}`
- 'fencing _ token _ value' (monotonía)
- `lease_renew_fail_total`
- 'split _ brain _ prevented _ total' (cuántos intentos se han denegado por falta de quórum)
- `preemptions_total`, `wait_queue_len`
- `lock_name`, `owner_id`, `token`, `ttl`, `attempt`, `wait_time_ms`, `path` (для ZK), `mod_revision` (etcd).
- Los durmientes «acquire → critical section → release» con el resultado.
- Crecimiento de 'lease _ renew _ fail _ total'.
- `lock_hold_seconds{p99}` > SLO.
- Castillos «huérfanos» (sin heartbeat).
- Colas de espera infladas.
10) Ejemplos prácticos
10. 1 Redis-cerradura segura con fencing (pseudo)
1. Almacene el contador de tokens en un sistema de confianza (por ejemplo, Postgres/etcd).
2. Con el éxito 'SET NX PX' leemos/incorporamos el token y hacemos todos los cambios del recurso con la verificación del token en el DB/servicio.
python acquire token = db. next_token ("locks/orders/42") # monotone ok = redis. set("locks:orders:42", owner, nx=True, px=ttl_ms)
if not ok:
raise Busy()
critical op guarded by token db. exec("UPDATE orders SET... WHERE id=:id AND:token > last_token",...)
release (compare owner)
10. 2 etcd Mutex + watchdog (Go)
go ctx, cancel:= context. WithCancel(context. Background())
sess, _:= concurrency. NewSession(cli, concurrency. WithTTL(10))
m:= concurrency. NewMutex(sess, "/locks/job/cleanup")
if err:= m. Lock(ctx); err!= nil { /... / }
// Watchdog go func() {
<-sess. Done ()//loss of session/quorum cancel ()//stop working
}()
doCritical (ctx )//must respond to ctx. Done()
_ = m. Unlock(context. Background())
_ = sess. Close()
10. 3 Liderazgo en ZK (Java, Curator)
java
LeaderSelector selector = new LeaderSelector(client, "/leaders/cron", listener);
selector. autoRequeue();
selector. start(); // listener. enterLeadership() с try-finally и heartbeat
10. 4 Postgres advisory lock con dlline (SQL + app)
sql
SELECT pg_try_advisory_lock(128765); -- attempt without blocking
-- if false --> return via backoff + jitter
11) Pruebas de reproducción (Días de juego)
Pérdida de quórum: desactivar 1-2 nodos etcd → intentar tomar la cerradura no debe pasar.
GC-pausa/stop-the-world: retrasar artificialmente el flujo del propietario → comprobar que watchdog interrumpe el trabajo.
Split-brain: emulación de la separación de la red entre el propietario y el vigilante del castillo → el nuevo propietario recibe un token fencing más alto, el viejo - rechazado por el azul.
Clock skew/drift: quite el reloj al propietario (para Redis/lease) → asegúrese de que los tokens/verificaciones proporcionan la corrección.
Crash before release: proceso de caída → bloqueo se libera por TTL/sesión.
12) Anti-patrones
Cerradura TTL limpia sin fencing cuando se accede a un recurso externo.
Confiar en el tiempo local para la corrección (sin HLC/fencing).
Distribución de candados a través de un maestro Redis en un entorno con Feilover y sin confirmación de réplicas.
Sección crítica infinita (TTL «por los siglos»).
Quitar la cerradura «alienígena» sin conciliar 'owner _ id '/token.
La ausencia de backoff + jitter → intentos de «tormenta».
Un solo bloqueo global «para todo» es una bolsa de conflictos; El charding con llave es mejor.
13) Lista de verificación de implementación
- Se ha definido el tipo de recurso y se puede prescindir de CAS/cola/idempotencia.
- Mecanismo seleccionado: etcd/ZK/Consul para CP; Redis/caché - sólo con fencing.
- Implementado: 'owner _ id', extensión TTL +, watchdog, correcto unlock.
- Un recurso externo comprueba la fencing token (monotonía).
- Hay una estrategia de liderazgo y fracaso.
- Métricas personalizadas, alertas, lógica de tokens y revisiones.
- Se proporcionan backoff + jitter y temporizadores en el acquire.
- Días de juego: quórum, split-brain, pausas GC, clock skew.
- Documentación del orden en que se toman varios candados (si es necesario).
- Plan de degradación (brownout): qué hacer cuando el castillo no está disponible.
14) FAQ
P: ¿La cerradura Redis 'SET NX PX' es suficiente?
R: Sólo si el recurso comprueba fencing token. De lo contrario, dos propietarios son posibles en una división de red.
P: ¿Qué elegir «predeterminado»?
R: Para garantías estrictas - etcd/ZooKeeper/Consul (CP). Para tareas fáciles dentro de un solo DB - advisory locks Postgres. Redis es sólo con fencing.
P: ¿Qué TTL poner?
R: 'TTL ≥ p99 de duración de la sección crítica × 2' y lo suficientemente corto para limpiar rápidamente los «zombies». Renovación - cada 'TTL/3'.
P: ¿Cómo evitar el ayuno?
R: Cola de espera por orden (ZK sequential) o algoritmo fairness; límite de intentos y planificación justa.
P: ¿Necesita sincronización de tiempo?
R: Para la corrección - no (use fencing). Para la previsibilidad operativa - sí (NTP/PTP), pero no confíe en el wall-clock en la lógica de la cerradura.
15) Resultados
Los bloqueos distribuidos confiables se construyen en stores de quórum (etcd/ZK/Consul) con lease + keepalive, y necesariamente se complementan con fencing token a nivel de recurso modificable. Cualquier enfoque TTL/Redis sin cercas es un riesgo de rotura. Piense primero en la causalidad y la idempotencia, use bloqueos donde no sea posible sin ellos, mida, pruebe los modos fallidos - y sus «secciones críticas» seguirán siendo críticas sólo por el significado y no por el número de incidentes.