Consigna y contexto
Una tabla de hechos de Apache Iceberg almacena 18 meses de pedidos mientras se ejecutan simultáneamente trabajos por lotes de Spark, streams de Flink y consultas de Trino. El equipo desea dividir customer_name en first_name y last_name y cambiar la partición de diaria a mensual. No es posible reescribir todos los archivos históricos, las consultas antiguas deben seguir funcionando, las escrituras en streaming no pueden detenerse y un despliegue fallido debe ser reversible.
La entrevista evalúa si el candidato distingue entre compatibilidad de esquemas, identidad de campos, disposición de particiones, commits de snapshot y actualizaciones de lectores/escritores. Iceberg documenta IDs de campo estables y una evolución independiente de esquemas y particiones; una respuesta sólida convierte esas garantías en una migración por fases en lugar de limitarse a decir “modificar la tabla y hacer backfill”.
Qué evalúa el entrevistador
Se busca evaluar la distinción entre nombres e identidad de campo, entre evolución del esquema y backfill histórico, y entre una nueva especificación de partición y el movimiento físico de archivos antiguos. El candidato debe explicar la atomicidad de los snapshots, la compatibilidad de los motores y la evidencia de rollback, incluyendo lo que sucede cuando un escritor compite concurrentemente con la migración.
Preguntas clarificadoras
- ¿Qué versiones de Spark, Flink, Trino, catálogo e Iceberg leen y escriben en la tabla?
- ¿Se puede procesar sintácticamente
customer_namede manera confiable considerando diferentes idiomas, nombres únicos y restricciones de privacidad? - ¿Dependen los clientes antiguos de las posiciones, de
SELECT *o de esquemas serializados? - ¿Pueden los lectores mezclar especificaciones de partición y aun así aplicar date predicate pushdown?
- ¿Cómo se coordinan los checkpoints de streaming y las versiones del esquema?
- ¿Admite el catálogo commits atómicos, retención de snapshots y rollback por ID?
Respuesta en 30 segundos
“Realizaría un inventario de la compatibilidad de los motores y el uso de columnas, añadiría las nuevas columnas utilizando los IDs de campo de Iceberg y mantendría la columna antigua durante un período de observación. La evolución de las particiones es independiente: los archivos nuevos usan transformaciones mensuales mientras que los antiguos siguen siendo legibles. Desplegaría primero lectores compatibles, luego escritores y después consumidores; validaría consultas, checkpoints y commits concurrentes, y mantendría el snapshot anterior para rollback. El backfill histórico es un trabajo versionado, no un cambio de esquema implícito.”
Solución paso a paso
Paso 1: Definir una matriz de compatibilidad y un contrato de cambio
Registre el esquema, los IDs de campo, las especificaciones de partición actuales, la retención de snapshots, las versiones de los escritores y las versiones de los lectores. Mantenga contratos separados para la semántica de las columnas y la disposición de las particiones para que el parseo, el renombrado y las reescrituras físicas no se conviertan en un único commit opaco.
Trate la división como aditiva al principio: agregue first_name y last_name, conserve customer_name y defina reglas para espacios en blanco, monónimos, nombres multilingües y errores de parseo. No asigne silenciosamente un nuevo significado a la columna antigua. Planifique su eliminación solo después de que todos los consumidores hayan migrado.
Paso 2: Utilizar IDs de campo, no posiciones de columna
Iceberg vincula los campos mediante IDs estables. Un registro de cambios puede verse así:
old: id=7 customer_name:string
new: id=21 first_name:string, id=22 last_name:string
old id=7 remains until consumers migrateNunca reutilice el ID 7 para un significado diferente tras eliminarlo. Verifique también los IDs secundarios de structs, maps y lists anidados. El CI debe comparar el mapa de IDs antes y después; un cambio que afecte solo al nombre no es prueba de una evolución segura.
Paso 3: Separar el backfill de las escrituras online
Permita que los escritores por lotes y de streaming pueblen primero las nuevas columnas y publiquen métricas de calidad de parseo. Realice el backfill de los archivos históricos por partición más adelante. Lea un snapshot fijo o watermark y registre el snapshot de entrada, la versión del código, el recuento de filas, los fallos de parseo y los checksums en un manifiesto. Si un commit concurrente de Iceberg entra en conflicto, vuelva a leer el snapshot más reciente y reintente; nunca sobrescriba el commit de otro escritor.
Si un nombre histórico no se puede parsear de forma confiable, mantenga null más name_parse_status en lugar de inventar datos de identidad. Un backfill es un cómputo versionado independiente, no un requisito del comando del esquema en sí.
Paso 4: Evolucionar la especificación de partición de forma independiente
Agregue una especificación de partición usando una transformación por mes para los datos nuevos mientras los archivos antiguos conservan la especificación por día. El planificador debe identificar la especificación de cada archivo y podarlo (prune) correctamente.
spec-0: day(ts) -> existing files
spec-1: month(ts) -> new filesPruebe el volumen de escaneo, el pruning y el comportamiento ante archivos pequeños en una tabla sombra. Los consumidores deben leer los metadatos a través del catálogo en lugar de construir rutas de almacenamiento de objetos a partir de los nombres de directorios.
Paso 5: Escalonar los lanzamientos de lectores y escritores
Despliegue primero lectores que toleren ambas columnas, luego escritores que pueblen las nuevas columnas y solo entonces consumidores que requieran los nuevos campos. Valide la recuperación de checkpoints de streaming frente a ambos snapshots. Registre el soporte específico de cada motor para renombramientos, tipos anidados, promoción de tipos, transformaciones y especificaciones mixtas; utilice una vista o una actualización del motor donde falte soporte.
Haga de cada cambio un snapshot pequeño y registre el escritor, el catálogo, el mapa de IDs y la especificación de partición. Evite combinar la eliminación de columnas, cambios de tipo y una reescritura masiva en la misma ventana de observación.
Paso 6: Establecer filtros (gates) en los commits y preservar el rollback
Ejecute verificaciones estructurales en IDs, tipos y especificaciones; verificaciones de datos en recuentos de filas, tasas de nulos, fallos de parseo, agregaciones y cortes de fecha; y verificaciones de comportamiento en consultas SQL antiguas/nuevas, recuperación de checkpoints, conflictos de commits y pruning. Conserve muestras no parseadas para su revisión.
Guarde previous_snapshot_id y new_snapshot_id antes del lanzamiento. Si falla un filtro, apunte el catálogo nuevamente al snapshot anterior, retenga los nuevos archivos y evidencias, pause los consumidores que requieran las nuevas columnas y vuelva a ejecutar las particiones afectadas a partir de entradas fijas.
Respuesta modelo
“Primero realizaría un inventario del soporte de Spark, Flink, Trino y el catálogo para los IDs de campo, luego agregaría las dos nuevas columnas mientras retengo customer_name. Un parser determinista y name_parse_status cuantifican la calidad; un backfill histórico utiliza un snapshot fijo, una versión de código y un manifiesto. Los archivos nuevos usan month(ts) mientras que los antiguos usan day(ts); los metadatos de la tabla manejan ambos.”
“Actualizaría lectores compatibles, luego escritores y luego consumidores. Antes del lanzamiento validaría IDs, nulos, agregaciones, consultas antiguas y nuevas, recuperación de streams y commits concurrentes. Retendría el snapshot anterior y revertiría el puntero del catálogo si falla algún filtro crítico (hard gate).”
Errores comunes
- Usar la posición de la columna como identidad → los bytes antiguos se leen erróneamente → validar IDs de campo estables.
- Tratar un renombre como una nueva columna semántica → los valores antiguos adquieren un nuevo significado → agregar, desaconsejar (deprecate) y luego eliminar.
- Mover todos los archivos antiguos tras cambiar la especificación → commits gigantes y rollback deficiente → permitir que las especificaciones coexistan y reescribir selectivamente.
- Actualizar primero un lector que solo use la nueva columna → los escritores antiguos emiten nulos → lectores compatibles, escritores, luego consumidores.
- Probar únicamente el comando del esquema → las consultas o la recuperación del stream siguen fallando → ejecutar filtros estructurales, de datos y de comportamiento.
- Hacer backfill directamente en el snapshot activo → no hay un punto de recuperación limpio → usar una salida versionada y snapshot IDs.
Preguntas de seguimiento y respuestas
Pregunta de seguimiento 1: ¿Por qué nunca se debe reutilizar el ID de un campo eliminado?
Los archivos de datos antiguos aún contienen el ID original. Reutilizarlo hace que los lectores interpreten los bytes antiguos como un nuevo campo semántico, por lo que se debe asignar un nuevo ID y retener el campo antiguo hasta que los consumidores y las repeticiones (replays) históricas hayan migrado.
Pregunta de seguimiento 2: ¿Pueden coexistir de forma segura especificaciones de partición antiguas y nuevas?
Sí, si los metadatos registran la especificación de cada archivo y los motores reales aplican transformaciones y predicados correctamente. Verifique el pruning, el comportamiento de las zonas horarias y el recuento de archivos pequeños con consultas similares a las de producción en lugar de inferir el comportamiento a partir de los nombres de directorios.
Pregunta de seguimiento 3: ¿Cómo recuperarse de un conflicto de commits concurrentes?
Lea el snapshot más reciente, confirme que la entrada siga siendo válida, vuelva a computar solo las particiones afectadas y envíe nuevamente. Nunca fuerce un archivo de metadatos antiguo sobre el snapshot de otro escritor; los conflictos repetidos exigen una menor concurrencia o particiones más pequeñas.
Pregunta de seguimiento 4: ¿Cuándo se puede eliminar la columna antigua?
Una vez que los lectores, escritores, replays, auditorías y exportaciones hayan migrado, el período de observación sea estable y los snapshots retenidos cubran la ventana de rollback. Inventaríe los consumidores ocultos antes de enviar la eliminación.
Pregunta de seguimiento 5: ¿Qué debería suceder con los nombres que no se pueden parsear?
Escriba null más una razón y una referencia controlada al valor original, supervise por idioma y formato, y evite colocar suposiciones en un campo de identidad. Corríjalo en una versión posterior o en un flujo de trabajo de revisión humana.