lección 6

Día 4 -- BLACK FRIDAY: el pipeline no escala

El volumen se triplica con 3000 repartidores temporales. El consumer lag se dispara, el dashboard se retrasa. Diagnosticas el bottleneck y optimizas en caliente bajo presion de Ana.

45 min

### Jueves, 7:15 AM -- Alerta de PagerDuty te despierta

Tu teléfono vibra a las 7:15 con una alerta de PagerDuty: "CRITICAL: Consumer lag en driver.location supera 50.000 mensajes. Pipeline flashroute-eta-pipeline. Latencia estimada: 4.2 minutos." Saltas de la cama. Es Black Friday -- FlashRoute activó 2000 repartidores temporales anoche para cubrir el pico. Eso son 3000 GPSs enviando cada 5 segundos: 600 eventos/segundo. Tu pipeline fue diseñado para 200.

A las 7:25, mientras te vistes, Slack explota. Ana Vega: "El dashboard no se actualiza. Veo posiciones de hace 4 minutos. Es BLACK FRIDAY. Tengo el TRIPLE de repartidores en la calle y no sé dónde están. Esto es CRÍTICO." Marcos, tres minutos después, escueto: "Dashboard de ETAs con 4 min de lag. Qué necesitas para arreglarlo?" Nota que Marcos pregunta "qué necesitas" -- no "cómo lo arreglas". Confía en que sabes la solución, solo quiere desbloquearte.

### Diagnóstico rápido: dónde está el bottleneck

Antes de escalar a ciegas, necesitas identificar DÓNDE está el cuello de botella. En un pipeline de streaming hay 5 puntos posibles de saturación. Abres Spark UI y la consola de Kafka para diagnosticar:

  1. 01.Kafka producer (Carlos): improbable -- el producer escala automáticamente y no hay errores de publicación
  2. 02.Particiones Kafka: SOSPECHOSO -- 12 particiones con 3000 drivers puede generar skew
  3. 03.Spark consumers: el cluster tiene 4 executors con 2 cores = 8 tasks de lectura para 12 particiones
  4. 04.Procesamiento (Haversine + join): el join con deliveries_df podría ser lento si la tabla creció
  5. 05.Sink DynamoDB: si el batch_writer se satura, el micro-batch tarda más de 30s y se acumula lag
1# Script de diagnóstico: consumer lag por particion
2# Ejecutas esto desde tu portatil a las 7:30 AM
3
4def diagnose_kafka_lag():
5 """Simula la salida de kafka-consumer-groups.sh --describe"""
6 partitions = [
7 {"partition": 0, "current": 1_847_293, "end": 1_852_104, "lag": 4811},
8 {"partition": 1, "current": 1_845_011, "end": 1_851_892, "lag": 6881},
9 {"partition": 2, "current": 1_849_500, "end": 1_852_003, "lag": 2503},
10 {"partition": 3, "current": 1_844_200, "end": 1_851_750, "lag": 7550},
11 {"partition": 4, "current": 1_847_800, "end": 1_852_200, "lag": 4400},
12 {"partition": 5, "current": 1_843_100, "end": 1_851_600, "lag": 8500},
13 {"partition": 6, "current": 1_848_900, "end": 1_852_100, "lag": 3200},
14 {"partition": 7, "current": 1_846_500, "end": 1_851_900, "lag": 5400},
15 {"partition": 8, "current": 1_842_800, "end": 1_851_500, "lag": 8700},
16 {"partition": 9, "current": 1_847_100, "end": 1_852_000, "lag": 4900},
17 {"partition": 10, "current": 1_844_900, "end": 1_851_800, "lag": 6900},
18 {"partition": 11, "current": 1_848_200, "end": 1_852_300, "lag": 4100},
19 ]
20
21 total_lag = sum(p["lag"] for p in partitions)
22 max_lag = max(p["lag"] for p in partitions)
23 min_lag = min(p["lag"] for p in partitions)
24
25 print(f"=== DIAGNOSTICO CONSUMER LAG ===")
26 print(f"Total lag: {total_lag:,} mensajes")
27 print(f"Lag promedio/particion: {total_lag // 12:,}")
28 print(f"Peor particion: p{max(partitions, key=lambda p: p['lag'])['partition']} ({max_lag:,})")
29 print(f"Mejor particion: p{min(partitions, key=lambda p: p['lag'])['partition']} ({min_lag:,})")
30 print(f"Skew ratio: {max_lag / min_lag:.1f}x")
31 print(f"")
32 print(f"A 600 ev/s, retraso estimado: {total_lag / 600:.0f}s ({total_lag / 600 / 60:.1f} min)")
33 print(f"")
34 print(f"DIAGNOSTICO: throughput insuficiente + skew moderado")
35 print(f"ACCION: escalar executors de Spark (rápido) + evaluar más particiones (medio plazo)")
36
37diagnose_kafka_lag()

Diagnóstico a las 7:30 AM: identificar total lag, skew y retraso estimado

### Solución inmediata: escalar Spark en caliente

Le escribes a Marcos a las 7:35: "Diagnóstico: lag de 68K mensajes (~2 min retraso). Causa: cluster Spark subdimensionado para 3x volumen. Solución: escalar executors de 4 a 8 y subir maxOffsetsPerTrigger. Lo puedo hacer en 15 minutos sin downtime reiniciando el streaming query." Marcos: "Hazlo." Dos palabras. Ya tienes permiso (que ni necesitabas -- tu dominio).

1# CONFIGURACION NORMAL (200 ev/s)
2spark_config_normal = {
3 "spark.executor.instances": "4",
4 "spark.executor.cores": "2",
5 "spark.executor.memory": "4g",
6 "spark.sql.shuffle.partitions": "12",
7 "maxOffsetsPerTrigger": "10000",
8}
9
10# CONFIGURACION BLACK FRIDAY (600+ ev/s)
11spark_config_peak = {
12 "spark.executor.instances": "8", # 4 -> 8 (doble paralelismo)
13 "spark.executor.cores": "3", # 2 -> 3 (más tasks por executor)
14 "spark.executor.memory": "6g", # 4g -> 6g (más estado en memoria)
15 "spark.sql.shuffle.partitions": "24", # 12 -> 24 (más paralelismo shuffle)
16 "spark.streaming.backpressure.enabled": "true", # Auto-ajuste velocidad
17 "maxOffsetsPerTrigger": "50000", # 10K -> 50K (drenar lag rápido)
18}
19
20# Para aplicar en EMR sin downtime:
21# 1. Escalar cluster: aws emr modify-instance-groups --cluster-id j-XXX
22# 2. Reiniciar streaming query (Spark reanuda desde checkpoint)
23# El checkpoint garantiza exactly-once: no pierde ni duplica eventos

Configuración normal vs Black Friday: duplicar recursos para 3x volumen

### Problema 2: el sink a DynamoDB también es lento

Después de escalar Spark, miras Spark UI y ves que el foreachBatch (escritura a DynamoDB) tarda 14 segundos de los 30 del trigger. Con 3x eventos por batch, hay 3x escrituras a DynamoDB. El batch_writer agrupa de 25 en 25 pero las llamadas HTTP son secuenciales. Necesitas paralelizar las escrituras:

1from concurrent.futures import ThreadPoolExecutor
2import boto3
3from datetime import datetime, timedelta
4
5def write_etas_dynamodb_parallel(batch_df, batch_id):
6 """
7 Version optimizada para Black Friday:
8 - Divide el batch en chunks de 25 (máximo DynamoDB batch_writer)
9 - Escribe en paralelo con ThreadPoolExecutor (8 threads)
10 - Reduce tiempo de escritura de 14s a ~3s
11 """
12 if batch_df.isEmpty():
13 return
14
15 rows = batch_df.collect()
16 chunks = [rows[i:i+25] for i in range(0, len(rows), 25)]
17
18 def write_chunk(chunk):
19 dynamo = boto3.resource('dynamodb', region_name='eu-west-1')
20 table = dynamo.Table('delivery_etas')
21 with table.batch_writer() as writer:
22 for row in chunk:
23 writer.put_item(Item={
24 'delivery_id': row['delivery_id'],
25 'driver_id': row['driver_id'],
26 'eta_minutes': int(row['eta_minutes']),
27 'distance_km': str(round(float(row['distance_km']), 2)),
28 'is_at_risk': bool(row['is_at_risk']),
29 'zone': row['zone'],
30 'calculated_at': datetime.now().isoformat(),
31 'ttl': int((datetime.now() + timedelta(minutes=30)).timestamp())
32 })
33
34 # 8 threads escriben en paralelo -> 8x speedup teorico
35 with ThreadPoolExecutor(max_workers=8) as pool:
36 pool.map(write_chunk, chunks)
37
38# Resultado: foreachBatch pasa de 14s a 3s
39# Total micro-batch: ~8s (lectura 2s + proceso 3s + escritura 3s)
40# Con trigger=30s, hay 22s de margen -> estable

Sink DynamoDB paralelo: 8 threads concurrentes reducen 14s a 3s

En picos de volumen, la solución no siempre es "más máquinas". A veces el bottleneck está en CÓMO escribes (secuencial vs paralelo), en el tamaño del batch, o en que la tabla DynamoDB necesita más WCUs (Write Capacity Units) provisionadas. Diagnostica con datos (Spark UI: duración de cada fase del micro-batch) antes de escalar a ciegas.

### El resultado: lag drenado en 20 minutos

Aplicas los cambios a las 7:50. Reinicio del streaming query (tarda ~2 minutos por cold start). A las 7:55 empieza a drenar el lag acumulado con el maxOffsetsPerTrigger alto. A las 8:10, el lag baja de 68.000 a menos de 500 mensajes. Latencia del dashboard: <30 segundos. Le escribes a Ana: "Dashboard operativo. Latencia < 30s." Respuesta: "Ok." Marcos a las 8:30: "He visto que resolviste en 20 min. Bien. Quiero un runbook para que la próxima vez lo pueda hacer cualquiera sin esperarte a ti." Anotado.

### Lección aprendida: preparar para picos

El error no fue que el pipeline se saturara -- eso es ESPERADO cuando el volumen se triplica. El error fue no haber preparado un plan de escalado la semana anterior cuando ya se sabía que Black Friday triplicaría los repartidores. Marcos no te reprende (es tu primera semana) pero su petición del runbook es clara: como owner, anticipar picos es tu responsabilidad.

  • Pre-pico: escalar cluster a config "peak" la noche anterior (automatizable con cron)
  • Alerta temprana: alarma cuando lag > 5K mensajes (30s retraso), no esperar a 50K
  • Runbook: pasos claros numerados que cualquier DE pueda seguir a las 3AM
  • Post-pico: reducir cluster a config normal 24h después del pico (ahorro de costes)
  • Medio plazo: auto-scaling basado en métrica de lag (pedir a Marcos presupuesto para evaluarlo)

El coste de mantener la configuración "peak" permanentemente sería ~4x el coste normal del cluster EMR (de $800/mes a $3200/mes). Marcos te preguntará "merece la pena?" La respuesta es NO -- solo necesitas el peak 3-4 días al año (Black Friday, Navidad, rebajas). Mejor escalar manualmente la noche anterior que quemar dinero 361 días al año.

## ejercicios

[01]

Diagnosticar consumer lag y proponer solución

Dado un conjunto de métricas de lag por particion, diagnostica el problema (skew, throughput insuficiente o ambos) y calcula la configuración optima para drenar el lag en menos de 10 minutos.

Cargando editor...
[02]

Escribir el runbook de escalado

Marcos te pide un runbook automatizable para escalar el pipeline en picos. Implementa las funciones que verifican el estado, calculan la configuración necesaria y generan los comandos AWS CLI para escalar EMR.

Cargando editor...
[03]

Implementar sink DynamoDB con escritura paralela

El sink secuencial a DynamoDB tarda 14 segundos por batch en Black Friday. Implementa una versión con ThreadPoolExecutor que divida en chunks de 25 y escriba en paralelo con 8 threads. Mide la mejora de rendimiento.

Cargando editor...
[04]

Evaluar estrategia de particionamiento Kafka

Carlos pregunta si deberia aumentar las particiones del topic GPS para Black Friday futuro. Analiza el impacto de diferentes números de particiones en paralelismo, skew y coste. Recomienda una configuración.

Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...