En este artículo, le explicaré cómo hemos diseñado un sistema en tiempo real con:
Latencia de eventos inferior al segundo
Rendimiento de más de 100.000 eventos/segundo
Fiabilidad a escala
Uso de Scala/Python y herramientas de código abierto
La necesidad empresarial
En Improving, necesitábamos crear una canalización ETL de nivel empresarial para gestionar datos de telemetría no estructurados masivos (JSON, XML) procedentes de múltiples fuentes. Los requisitos eran
Latencia inferior a un segundo desde la ingesta de eventos hasta la disponibilidad de la consulta
Alta disponibilidad y tolerancia a fallos
Capacidad para escalar hasta 10 TB/día de datos de eventos sin procesar.
Flexibilidad de esquema para admitir esquemas IoT en evolución
Pila tecnológica (in situ)
Elegimos tecnologías probadas y escalables:
Diseño del sistema local:
Kafka: La columna vertebral en tiempo real
Temas particionados para el paralelismo (más de 100 particiones)
Compresión habilitada (LZ4)
Factor de replicación = 3 para tolerancia a fallos
Los productores envían eventos en formato Avro con Schema Registry
Diseño clave para baja latencia:
Kafka está ajustado para baja latencia de lote.ms = 1msy acks=1 para priorizar la velocidad (con redundancia en la capa de almacenamiento).
spark.streaming.backpressure.enabled=true
Spark Structured Streaming: Magia de latencia por debajo del segundo
Nuestros trabajos de streaming en PySpark fueron ajustados para micro-batching:
Tamaño de lote: Microlotes dinámicos, limitados a ~1 segundo de datos
Agregaciones con estado: activadas con marcas de agua
Puntos de control: Directorio de puntos de control basado en HDFS para exactamente una vez
Para evitar la sobrecarga de serialización, analizamos los mensajes utilizando fastavro en UDFs y escribimos la salida utilizando DataFrameWriter con formato parquet (ideal para Hive).
Hive: Almacenamiento escalable con búsquedas rápidas
Tablas externas particionadas (por dt_skey, proveedor)
Parquet comprimido con Snappy para optimizar el rendimiento de E/S y permitir lecturas columnares rápidas con predicate pushdown en Hive
Motor Tez optimizado con: Filtros Bloom, Ejecución vectorizada, Poda de particiones
Cada lote de Spark escribía directamente en HDFSy Hive recogía los nuevos datos mediante la reparación de particiones, por ejemplo, activando la función MSCK REPARAR TABLA
o ALTER TABLE ADD PARTITION
(activado por Airflow).
Optimizaciones clave
1. Reducción de la sobrecarga de microlotes
El ajuste de spark.sql.shuffle.partitions y la reutilización de serializadores mantuvo cada microlote por debajo de 50 ms.
Parámetros de ajuste de Spark: executor-cores=5 memoria de ejecutor=8G número de ejecutores = 50 spark.sql.shuffle.partitions=600 → optimizado en base al recuento de particiones de Kafka spark.memory.fraction=0.8 → permite más memoria para barajar y almacenar en caché.
2. Broadcast Joins para datos de referencia
Uno de los principales asesinos de rendimiento en sistemas distribuidos como Spark son las uniones de shuffle-heavy. Para mitigarlo, difundimos pequeños conjuntos de datos de referencia. Sustituimos las uniones aleatorias por uniones de difusión cuando las tablas de consulta pesaban menos de 100 MB.
broadcast_df = spark.read.parquet("/hdfs/ruta/ref_data").cache() ref_broadcast = F.broadcast(broadcast_df)
joined_df = streaming_df.join(ref_broadcast, "region_code", "left")
Spark envía los
ref_datos
a todos los ejecutores.No es necesario barajar el gran conjunto de datos de transmisión.
Resultado: Cero shuffle, join más rápido, latencia por debajo del milisegundo.
3. Lecturas paralelas de Kafka
Cada partición de Kafka se asignó a un hilo ejecutor de Spark dedicado (sin cuellos de botella de partición conjunta).
Configuración de Kafka para un alto rendimiento - Particionamiento de temas: más de 100 particiones para distribuir la ingesta - Factor de replicación: 3(para failover) - Compresión: lz4 para una latencia más baja y una compactación decente - Tamaño de mensaje: limitado a 1MB para evitar bloqueos
4. Cero pausas de GC
Spark está configurado con G1GC y ajuste de memoria para evitar bloqueos de la JVM durante los picos de ingesta. Problema: Las pausas de recolección de basura y la presión de memoria ralentizan la finalización de microlotes.
Ejemplo Uso de G1GC y ajuste: -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=35 -XX:+ExplicitGCInvokesConcurrent
5. 5. Gestión de la desviación de los datos
Utilización de salting y repartitioning para evitar tareas masivas de shuffle en tipos de eventos raros.
Ejemplo de salting y reparticionamiento: from pyspark.sql.functions import when, concat_ws, floor, rand
# Aplicar sal sólo a la clave sesgada salted_df = df.withColumn( "tipo_evento_salado", when(col("tipo_evento") == "ENTREGADO", concat_ws("_", col("tipo_evento"), floor(rand() * 10).cast("int")) ).otherwise(col("tipo_evento")) )
# Reparticionar para distribuir la carga de forma más uniforme repartitioned = salted_df.repartition(10, "event_type_salted")
6. Compactación de archivos pequeños (Hive + HDFS)
Parquet + Snappy como formato por defecto para todas las salidas batch/streaming de Spark. Escribir microlotes cada pocos milisegundos crea montones de pequeños archivos parquetque:
Ralentizan las consultas Hive
Sobrecarga HDFS NameNode
Causan sobrecarga de memoria
Optimización: - Ejecutar un trabajo de compactación diario/rodante usando Spark para fusionar archivos - Utilizar la lógica de partición y agrupación de Hive (evitar particiones demasiado pequeñas) - Combine la salida utilizando .coalesce(n) o .repartition(n) antes de escribir.
Supervisión y capacidad de observación
Kafka: monitorizado mediante exportadores Prometheus y alertas de retraso
Spark: integrado con Grafana para métricas de memoria/CPU del ejecutor
Retraso de extremo a extremo: medido desde la marca de tiempo de escritura en Kafka hasta la marca de tiempo de escritura en Hive, almacenado en la base de datos de métricas.
Hemos creado un panel de latencia para inspeccionar visualmente dónde se producen los cuellos de botella, útil para el ajuste proactivo.
Pruebas y análisis comparativos
Antes de la producción - Simulamos una ingesta de 10 TB/día utilizando productores Kafka. - Comparación con 3 años de datos históricos - Realizamos pruebas de estrés con eventos de ráfaga sintéticos (3 veces la carga normal).
Lecciones aprendidas
Pruebe siempre la latencia de extremo a extremo, no sólo el rendimiento por capa.
Hive puede funcionar en tiempo real si se particiona de forma inteligente y se escribe de forma eficiente.
On-prem no significa lento con el ajuste correcto, open-source puede rivalizar con las pilas nativas de la nube.
Spark Structured Streaming puede ofrecer absolutamente pipelines on-prem de baja latencia.
La estrategia de particionamiento importa más que la potencia de cálculo
El viejo Hive todavía puede brillar cuando se ajusta adecuadamente
Los mecanismos de contrapresión de Kafka salvan vidas en los picos de carga.
Conclusión
En Improving, creemos que la ingeniería del rendimiento es un deporte de equipo. Diseñar una canalización de 8-10 TB/día on-prem con una latencia inferior a un segundo era ambicioso, pero utilizando Kafka + Spark + Hive con un ajuste inteligente, conseguimos que funcionara.
Si estás construyendo ETL pipelines a gran escala o modernizando sistemas on-prem, no dudes en ponerte en contacto con nosotros.