Guía

Ingesta de ficheros: patrones para cargas confiables

Copiar un archivo a una carpeta no es una estrategia de ingesta. Estos son los patrones que separan una carga de datos que sobrevive a producción de una que se cae con el primer archivo raro.

Agosto 20266 min de lectura

El primer pipeline de ingesta de ficheros que construye cualquier equipo suele ser el mismo: un job que lista una carpeta, lee lo que encuentra y lo escribe en la tabla destino. Funciona en la demo. Funciona en los primeros meses. Y después empieza a fallar de formas que nadie diseñó explícitamente. Un origen reenvía el mismo archivo dos veces porque su proceso de exportación tuvo un timeout, y ahora hay filas duplicadas en la capa de negocio. Un job se cae a mitad de un archivo de 40 GB y nadie sabe si ya se cargaron 10.000 filas o 3 millones. Un equipo de otro sistema agrega una columna nueva al CSV sin avisar y el parser explota, o peor, la ignora en silencio. Y cuando alguien pregunta "¿de qué archivo vino este registro raro?", no hay respuesta, porque nunca se guardó esa información. Nada de esto es un problema de volumen: es un problema de diseño. La ingesta de ficheros no escala por accidente; escala porque se construyó pensando en duplicados, fallos parciales, cambios de esquema y trazabilidad desde el primer día.

Ingesta incremental e idempotente

El error más común es tratar cada ejecución como si fuera la primera: listar toda la zona de aterrizaje, comparar contra lo ya cargado y reprocesar lo que "parece" nuevo. Esto funciona con cien archivos. Con cien mil, el propio listado del directorio se vuelve el cuello de botella, y la comparación se vuelve frágil ante renombrados, archivos movidos o relojes desincronizados entre sistemas.

El patrón correcto es mantener un cursor de progreso: un checkpoint que registra qué se procesó (por nombre, por marca de tiempo de llegada o por un log de eventos de creación de objetos) y que el siguiente ciclo solo consulta desde ese punto en adelante. Esto puede resolverse con un listado incremental basado en particiones por fecha en la ruta de aterrizaje, con un mecanismo de notificación de eventos del almacenamiento (mucho más barato que listar cuando el volumen crece), o con motores que ya incorporan esta lógica de forma nativa, como los conectores de streaming estructurado de Spark o extensiones equivalentes que gestionan el descubrimiento de archivos por notificación en lugar de listado completo.

Detectar lo nuevo no es suficiente: la carga también debe ser idempotente. Si el job se reinicia después de un fallo a mitad de proceso, volver a ejecutarlo no debería duplicar datos. Esto se logra combinando un checkpoint transaccional (que garantiza que un archivo se marca como procesado solo si la escritura fue exitosa) con claves de negocio o de archivo que permiten un merge/upsert en lugar de un append ciego cuando hace falta reprocesar. En la capa de aterrizaje (bronze o raw) el patrón habitual es distinto: ahí el append puro es aceptable, porque la deduplicación y la lógica de negocio se resuelven en capas posteriores, no en el punto de entrada.

Schema drift y registros malformados sin tumbar el pipeline

Un pipeline de ingesta que falla completo porque un archivo trae una columna nueva, un tipo de dato distinto o una fila corrupta está mal diseñado, sin importar cuántas pruebas unitarias tenga. La ingesta debe distinguir entre schema-on-write, donde el archivo debe ajustarse a un contrato estricto antes de aceptarse, y schema-on-read, donde se acepta el dato en bruto y la interpretación de tipos se aplica después. En zonas de aterrizaje y capas raw, schema-on-read con validación mínima de estructura es casi siempre la opción correcta: valida que el archivo sea parseable, no que cumpla reglas de negocio.

Cuando llega un cambio de esquema, hay tres respuestas razonables y una mala. La mala es fallar el job entero por una columna nueva sin darse cuenta de que es justo eso lo que pasó. Las razonables: agregar la columna automáticamente y continuar, rescatar el dato que no encaja en una columna aparte para no perderlo, o detener explícitamente el flujo solo cuando el cambio es lo bastante grave como para requerir intervención humana. La decisión correcta depende del contrato con el origen, no de la tecnología.

Con registros malformados el principio es el mismo: aislar, no bloquear. Un archivo con 5 de 200.000 filas corruptas no debería impedir que las otras 199.995 lleguen a destino.

Regla práctica: en la capa de aterrizaje, valida estructura (¿es un CSV/JSON/Parquet parseable?), no semántica (¿el campo "monto" es positivo?). Esa segunda validación pertenece a una capa posterior, donde ya hay contexto de negocio para decidir qué hacer con el dato sospechoso.

Metadatos y trazabilidad

Cada registro que entra al lago de datos debería poder responder, sin consultar logs externos, tres preguntas: ¿cuándo se ingirió?, ¿de qué archivo vino? y ¿ese archivo era el que se esperaba? La primera se resuelve agregando una columna de timestamp de ingesta en el momento de la carga, no reutilizando ninguna fecha que venga dentro del archivo. La segunda se resuelve capturando el nombre (o ruta completa) del archivo origen como columna adicional en cada fila. La tercera se resuelve con un checksum o hash del archivo, calculado antes de procesarlo, que permite detectar reenvíos exactos del mismo archivo o corrupciones en tránsito.

Esto no es burocracia: es lo que convierte un reproceso de "borra todo y reza" en una operación quirúrgica. Con esos tres metadatos se puede reprocesar exactamente los registros de un archivo específico, auditar de dónde salió un valor incorrecto que llegó a un reporte, y distinguir un archivo corregido y reenviado de un duplicado accidental. La convención de nombres del propio archivo (origen, dataset, fecha, versión) es la primera capa de este linaje, antes incluso de tocar el contenido.

Confiabilidad operativa: reintentos, backups y dead-letter

Los fallos van a ocurrir: la red se cae a mitad de una descarga, un archivo llega truncado, el sistema origen escribe un archivo vacío por error. Un pipeline confiable no es el que nunca falla, sino el que tiene una respuesta definida para cada tipo de fallo. Eso implica reintentos con backoff para errores transitorios (timeouts, límites de tasa), separación clara entre error recuperable y error de datos, y sobre todo una zona de cuarentena o dead-letter donde va todo archivo que no se pudo parsear, junto con el motivo del fallo, en lugar de descartarlo silenciosamente o bloquear la cola completa.

El movimiento de archivos entre zonas (aterrizaje, procesado, cuarentena) debe ser atómico: escribir primero en una ubicación temporal y mover solo al confirmar éxito evita el escenario de "el archivo desapareció de landing pero nunca llegó a bronze". Y conservar una copia del archivo original, incluso después de procesado, no es opcional: es lo único que permite reconstruir una tabla desde cero si se descubre un bug en la lógica de transformación meses después.

Estrategia frente a schema driftQué haceCuándo conviene
Fallar el pipelineDetiene la carga ante cualquier columna o tipo no esperadoDatasets con contrato estricto (financiero, regulatorio) donde un cambio silencioso es peor que una alerta
Agregar columnas automáticamenteIncorpora columnas nuevas al esquema y continúa la cargaOrígenes internos con evolución frecuente y controlada, donde el downstream tolera columnas nuevas
Rescatar en columna aparteGuarda el dato que no encaja (tipo o campo inesperado) en una columna cruda sin perderloCuando no se puede garantizar el contrato del origen pero tampoco se puede perder información
Ignorar silenciosamenteDescarta lo que no encaja en el esquema esperadoCasi nunca recomendable: es la causa más común de pérdida de datos no detectada
Un pipeline de ingesta no es confiable porque nunca falla; es confiable porque cuando falla, falla de forma controlada, visible y reversible.

Cuándo usarlo en la práctica

Antes de escribir la primera línea de código de un pipeline de ingesta de ficheros, conviene resolver estas decisiones de diseño:

  • Define una convención de nombres y de estructura de carpetas para la zona de aterrizaje (origen, dataset, fecha, versión) antes de recibir el primer archivo real.
  • Decide el modo de operación según el caso de uso real: batch programado para la mayoría de los casos, disparado por evento de llegada solo con volumen bajo, continuo solo si el costo adicional se justifica con una necesidad genuina de baja latencia.
  • Implementa un checkpoint de progreso desde el día uno, aunque el volumen inicial sea pequeño: es mucho más barato diseñarlo temprano que migrarlo cuando ya hay millones de archivos procesados.
  • Separa validación estructural (en la entrada) de validación de negocio (en capas posteriores); no mezcles ambas en el mismo punto del pipeline.
  • Agrega como mínimo tres metadatos a cada registro ingerido: timestamp de ingesta, nombre del archivo origen y un identificador de lote o checksum.
  • Diseña una zona de cuarentena para archivos malformados desde el inicio, no como parche posterior a un incidente.
  • Conserva los archivos originales después de procesarlos; el costo de almacenamiento es marginal comparado con el costo de no poder reconstruir una tabla.
  • Automatiza reintentos para fallos transitorios, pero nunca automatices reintentos para errores de datos: esos deben ir a cuarentena, no a un bucle infinito.

Ninguno de estos puntos requiere una plataforma específica: son decisiones de arquitectura que aplican igual si el motor de ingesta es un script propio, un job de Spark o un servicio administrado de algún proveedor cloud. La tecnología cambia; el diseño que hace que una ingesta sea confiable, no.