Datos y hojas de cálculo

Vindle optimiza importaciones masivas de CSV sustituyendo PHP por Rust

Vindle redujo el tiempo de procesamiento de 27 GB de datos de productos al trasladar el filtrado de CSV desde PHP a una herramienta personalizada en Rust, eliminando pasadas redundantes de análisis.

Illustration of a conveyor belt sorting data blocks efficiently
Ilustración creada para este artículo

Traducido automáticamente del original en inglés.

Vindle, una plataforma de comparación de precios, renovó recientemente su canal de ingesta de datos para gestionar una nueva integración masiva con Bol.com. Ante un aumento de tres órdenes de magnitud en el volumen de productos, el equipo de ingeniería reemplazó su analizador inicial basado en PHP por una aplicación personalizada en Rust. Este cambio les permitió procesar eficientemente 27 GB de feeds CSV comprimidos con gzip, manteniendo estrictos horarios de actualización cada hora.

Qué ocurrió

El desafío comenzó cuando Vindle integró Bol.com, un minorista general que ofrece más de 73 millones de productos. Los datos llegaban en 26 archivos CSV comprimidos con gzip, sumando aproximadamente 27 GB, con tamaños individuales que oscilaban entre 1,5 MB y 4,8 GB. Dado que Vindle actualiza los precios cada hora, el sistema debía analizar, filtrar y almacenar estas entradas rápidamente. La implementación inicial utilizaba la función fgetcsv() de PHP dentro de una aplicación Symfony. Aunque este enfoque era rápido de desarrollar y suficiente para feeds más pequeños, se convirtió en un cuello de botella al procesar millones de filas donde más del 99% eran descartadas debido a categorías irrelevantes o datos faltantes.

Para abordar la brecha de rendimiento, el equipo introdujo primero Xan, una herramienta de línea de comandos para CSV escrita en Rust. Esto les permitió descargar el filtrado de filas y la selección de columnas desde PHP, transmitiendo solo los datos relevantes de vuelta a la aplicación principal. Sin embargo, Vindle también requería estadísticas detalladas sobre por qué se rechazaban las filas, como marcas inválidas o regiones de envío no compatibles. Xan no podía proporcionar estas métricas en una sola pasada, lo que obligaba al equipo a ejecutar la herramienta dos veces: una para las estadísticas y otra para los datos filtrados. Este enfoque de doble pasada añadía entre 20 y 40 segundos por feed y duplicaba el costoso trabajo de análisis de CSV.

Un intento de paralelizar estos dos procesos de Xan utilizando tuberías Unix y tee fracasó debido a problemas de contrapresión (backpressure). La rama más lenta bloqueaba todo el canal, y la complejidad del manejo de errores aumentaba sin aportar mejoras de velocidad. Finalmente, el equipo escribió un pequeño programa dedicado en Rust que realizaba el filtrado y la recopilación de estadísticas en una sola pasada. Esta herramienta lee desde la entrada estándar, aplica reglas de exclusión, escribe las filas válidas en la salida estándar y envía las estadísticas a la salida de error estándar. Esta solución restauró la eficiencia de una sola pasada mientras proporcionaba los conocimientos necesarios, todo ello sin requerir una reescritura completa del núcleo de Symfony.

Detalles clave

  • Bol.com proporciona más de 73 millones de productos en 26 archivos CSV comprimidos con gzip que suman aproximadamente 27 GB.
  • La implementación inicial en PHP usaba fgetcsv(), pero tenía dificultades con el volumen de datos que requerían un filtrado intensivo.
  • El filtrado elimina entre el 90% y el 99,9977% de las filas basándose en criterios como categoría, validez del precio y ubicación de envío.
  • Usar Xan para el preprocesamiento mejoró la velocidad, pero requería dos pasadas separadas para recopilar tanto los datos filtrados como las estadísticas de rechazo.
  • Paralelizar los procesos de Xan mediante tuberías Unix introdujo cuellos de botella por contrapresión y no mejoró el rendimiento global.
  • Una herramienta CLI personalizada en Rust que utiliza la librería simd-csv ahora maneja el filtrado y las estadísticas en una sola pasada, transmitiendo los resultados directamente a PHP.

Contexto

A menudo se asume que el análisis de CSV es trivial, pero a gran escala se vuelve intensivo en CPU y E/S. En escenarios de alto volumen, el costo de leer, descomprimir y tokenizar cada carácter de un archivo se acumula rápidamente. Cuando la mayoría de las filas se descartan, la sobrecarga de analizarlas completamente antes de rechazarlas representa computación desperdiciada. Herramientas como Xan aprovechan optimizaciones de bajo nivel, como las instrucciones SIMD (Single Instruction, Multiple Data), para acelerar este análisis. Sin embargo, las herramientas genéricas pueden carecer de la lógica empresarial específica necesaria para filtrados complejos o seguimiento estadístico. Escribir un binario pequeño y enfocado en un lenguaje de sistemas como Rust permite a los desarrolladores combinar análisis de alto rendimiento con lógica personalizada, evitando la sobrecarga de intérpretes de propósito general como PHP para bucles ajustados.

Por qué importa

Para equipos que ejecutan aplicaciones autoalojadas, este caso destaca la importancia de identificar el límite adecuado entre el código de aplicación de alto nivel y el procesamiento de datos de bajo nivel. Mover todo el flujo de trabajo de ingesta a Rust habría introducido una deuda técnica significativa y complejidad de mantenimiento. Al mantener Symfony a cargo de la orquestación, las credenciales y la persistencia en base de datos, el equipo conservó los beneficios de productividad de PHP mientras resolvía el cuello de botella específico de rendimiento con una herramienta especializada. Este enfoque híbrido permite a los ingenieros optimizar rutas críticas sin sacrificar la facilidad de desarrollo que ofrece su framework principal.

Además, el cambio hacia el streaming de datos reduce la dependencia del disco. El proceso original requería escribir grandes archivos temporales en el disco, consumiendo espacio de almacenamiento significativo y ancho de banda de E/S. La nueva herramienta en Rust procesa los datos desde la entrada estándar hasta la salida estándar, habilitando un flujo de trabajo sin disco. Esto es crucial para entornos con almacenamiento limitado o donde la contención de E/S puede impactar otros servicios. Demuestra que las mejoras de rendimiento a menudo provienen de cambios arquitectónicos, como eliminar pasadas redundantes y reducir el almacenamiento intermedio, en lugar de simplemente optimizar la sintaxis del código.

Qué puedes hacer

  • Realiza un perfilado de tus canales de ingesta de datos para identificar si el análisis o el filtrado son el cuello de botella principal antes de reescribir el código.
  • Considera descargar el procesamiento pesado fila por fila a binarios externos escritos en lenguajes de sistemas como Rust o Go.
  • Evita paralelizar tareas idénticas de análisis mediante tuberías Unix si las ramas tienen cargas de trabajo desiguales, ya que la contrapresión limitará el rendimiento.
  • Diseña herramientas CLI para aceptar entrada desde la entrada estándar y escribir en la salida estándar para habilitar el streaming y reducir el uso de disco.
  • Mantén la lógica de negocio y la gestión de estado en tu aplicación principal, usando herramientas especializadas solo para el procesamiento intensivo.

Más noticias

Todas las noticias