Módulo 12 · Nivel empresarial (integración)

Lección 55 — Escalado

Réplicas de lectura, particionado y servicios sin estado.

Publicada
En esta lección
  1. Ejercicio 1 — La ley cero
  2. Ejercicio 2 — Las réplicas
  3. Ejercicio 3 — El cuello de escritura
  4. Ejercicio 4 — La partición
  5. Ejercicio 5 — La escalera completa
  6. Resumen del profesor

Ejercicio 1 — La ley cero

  1. La auditoría: el caché locmem de dev (violación en prod: la réplica A sirviendo el listado viejo que la B actualizó — la 38 ya lo prohibió: REDIS_URL obligatorio fuera de dev), el contador de visitas en memoria (por réplica: el dashboard suma 3 veces — tolerable si el dashboard lo sabe, bug si no), el breaker del 54 por réplica (tolerable: cada réplica fusiona por su cuenta, la divergencia se acota a 30 s), el /tmp de los CSV parciales del export (23: bug — va al bucket del 42 o al job del 31 en un solo worker).
  2. La prueba: réplica A muerta a mitad del checkout continuo: el LB la saca (healthcheck del 43) en ~10 s, los requests en vuelo fallan (el retry del cliente o el 503 del 26), el usuario reintenta y la B lo atiende: el token (18) sigue válido (sin sesión en memoria). Cronología: 10 s de degradación, 0 de sesión perdida, 0 de estado perdido. La ley cero comprueba.
  3. Los cazados: @lru_cache en tarifas_activas() (la 08): divergente por réplica con datos que cambian → migrado a Redis con TTL (38); el global CONTADOR = 0 del ejercicio antiguo: borrado (Redis INCR); el /tmp/export.csv: al bucket. Los tres: el inventario honesto del estado.

Ejercicio 2 — Las réplicas

  1. El test del read-your-writes:
python
@pytest.mark.django_db(transaction=True)
def test_read_your_writes(con_lag_replica):
    ref = reservar(user, event, ["A1"], clock=...).public_ref
    # sin cura (el request va a la réplica con lag 500 ms):
    with lag_replica(0.5):
        assert "mis reservas" NO_VE(ref)         # el "acabo de comprar y no aparece"
    # con la cura (el hint del request: la vista del usuario fuerza primario):
    with lag_replica(0.5), force_primary_for_request():
        assert "mis reservas" VE(ref)

La cura en el router: el middleware marca el request post-escritura (el cookie/flag tras el POST) y el router envía esas lecturas al primario 10 s — el precio: el primario recibe las lecturas personales (el browse sigue a la réplica: el 70% del tráfico).

  1. La medición: primario solo: browse p95 290 ms, primario al 78% en la meseta; con réplica: browse 118 ms, primario al 41% (el margen para la escritura del pico). El número que justifica el peldaño: el primario respira → el lock del asiento (la escritura crítica) tiene aire.

Ejercicio 3 — El cuello de escritura

  1. Las 4 reducciones verificadas: bulk UPDATE (el 36), pasarela fuera de TX (32: la TX del checkout 120 ms, no 1.2 s), índices mínimos (el índice del 09 que no se usa en escritura: retirado), skip_locked. pg_stat_statements: el tiempo total de escritura del checkout: -47% respecto al estado del 36.
  2. La métrica del lock-wait: en la meseta del 36 con 2 eventos grandes: wait_event='Lock' con ~14 sesiones esperando y lock-wait p95 de 380 ms concentrado en el UPDATE del asiento. La métrica lock_wait_ms (el histograma en el dash del 46) es la que decide el §4: el umbral escrito: "partición si lock-wait p95 > 500 ms sostenido".
  3. El plan (extracto): "réplica al browse si lecturas >70% ( hecho), partición por evento si lock-wait p95 > 500 ms (el ejercicio 4 la mide), shard multi-primario si el primario supera 80% sostenido de CPU+IO tras la partición (el límite REAL: ~200k reservas/min con este esquema — el trigger está a ×10 del presente)".

Ejercicio 4 — La partición

  1. La contención por recurso demostrada: 100 compradores al mismo evento: p95 1.9 s (la cola del lock); 100 repartidos entre 2 eventos: p95 1.7 s — el repartido NO se libera: el lock del asiento es por FILA (10: select_for_update por seat), pero el UPDATE masivo del checkout (el bulk del 36) toma locks por el índice común del evento: la contención sigue por evento. La lección: la fila-lock no lo arregla todo: el patrón de escritura (el bulk por evento) contiende por recurso.
  2. Con la partición: el repartido p95 0.9 s (los dos eventos: particiones independientes: el lock-wait se reparte); el mismo-evento 1.8 s (la contención POR evento persiste: es el turnstile del 36 la cura, no la partición). La partición separa eventos; no reduce la contención interna de uno.
  3. El contrato: el test con CaptureQueriesContext verifica que las queries de reservas llevan event_id (el prune del plan: EXPLAIN muestra la partición única); el job del 31 crea reservations_event_<uuid> al publicar (el evento sin partición: va a la partición DEFAULT con warning al log (45) — el insert no falla: el negocio no se detiene por la infra).

Ejercicio 5 — La escalera completa

  1. El doc (extracto):
markdown
# Escalera de TicketFlow (triggers medibles)
1. Vertical: activa por defecto (el tier del 42/44).
2. Réplicas: lecturas > 70% del tráfico → HECHO (browse 118 ms).
3. Caché: el listado dominante → HECHO (38, hit 98.7%).
4. Colas: lo no-crítico → HECHO (29/31).
5. Partición por evento: lock-wait p95 > 500 ms sostenido → MEDIDO: 380 ms. En observación.
6. Shard multi-primario: primario > 80% CPU+IO sostenido tras partición → trigger a ×10.
7. Servicios (53): el PCI/volumen del ADR-0007.
  1. La respuesta al "sharding por si acaso": "El primario está al 41% con la réplica (el ejercicio 2): el trigger del shard está a ×10 del presente y cuesta la complejidad ×10 (el 53). El plan: la partición al 500 ms del lock-wait (medido hoy: 380 ms). El sharding se decide por SU número, no por el miedo: el documento lo tiene."
  1. El mapa final (ASCII):
        [edge/CDN del 42-38]  ETag + WAF del 23
                │
   [LB del 43]─┴─[web ×N replicas sin estado]──[réplica PG (browse)]
                │        │                        [primario PG (escritura, partición por evento)]
        [Redis caché 38]│[breaker 54]                   │
                │        │                        [outbox→stream 30]
        [Celery workers 29]────[DLQ 30]           │
                │                                 │
        [beat/31 locks]                    [bucket 42: exports]

Cada caja del mapa es una lección del curso: la foto final del sistema completo.


Resumen del profesor

  • Ley cero: el estado fuera del proceso (el caché local y el /tmp son bugs de réplica); la prueba es la réplica muerta a mitad de flujo.
  • La réplica libera el browse (con read-your-writes para el "acabo de comprar"); el cuello de escritura se reduce ANTES de particionar, y la partición separa recursos — no la contención interna de uno.
  • La escalera se sube por SU número (lock-wait, lag, p95) con el trigger escrito: el sharding por miedo es el impuesto del escalador ansioso.