notebookutils.notebook.run frente a runMultiple: paralelismo en Microsoft Fabric
En la arquitectura de datos moderna dentro de Microsoft Fabric, la orquestación de notebooks es una pieza crítica para construir procesos de ingeniería de datos robustos. A medida que los proyectos de Business Intelligence escalan, pasar de un único proceso monolítico a una serie de tareas modulares se vuelve obligatorio. Sin embargo, la forma en que invocamos estos módulos define no solo el tiempo total de ejecución (Wall Clock Time), sino también el consumo de nuestra capacidad (CUs) y la estabilidad del sistema.
Como consultores, a menudo vemos arquitecturas que replican el comportamiento de los antiguos jobs de SQL Server Integration Services (SSIS), ejecutando tareas una tras otra de forma estrictamente secuencial. Si bien este enfoque es fácil de depurar, ignora por completo la naturaleza distribuida de Spark. En Microsoft Fabric, disponemos de la librería notebookutils (anteriormente conocida como mssparkutils en otros entornos de Azure) para gestionar estas ejecuciones. La decisión entre usar notebookutils.notebook.run de forma iterativa o saltar a runMultiple es lo que separa un pipeline rudimentario de una solución de grado empresarial.
Antes de entrar en el código, debemos entender que cada ejecución de un notebook hijo implica una gestión de sesión de Spark. Si no se configura correctamente, podrías estar pagando un precio muy alto en tiempos de arranque de sesiones o, peor aún, bloqueando tu capacidad por alcanzar los límites de concurrencia permitidos.
notebookutils.notebook.run: El enfoque secuencial y sus riesgos
La función notebookutils.notebook.run es la herramienta más directa para ejecutar un notebook desde otro. Su comportamiento es síncrono: el notebook maestro (llamador) se detiene y espera a que el notebook hijo (llamado) finalice su ejecución. Una vez que el hijo termina, devuelve un valor (si se ha configurado notebookutils.exit()) y el maestro continúa con la siguiente celda.
En un escenario de retail, imagina que tienes que procesar las ventas de 10 regiones distintas. Si utilizas un bucle for tradicional para llamar a run, el tiempo total será la suma de los tiempos de cada región, más el overhead de inicialización de Spark para cada una de ellas si no compartes la sesión.
# Ejemplo de ejecución secuencial poco eficiente
regiones = ["Norte", "Sur", "Este", "Oeste"]
for region in regiones:
print(f"Procesando región: {region}")
# El notebook maestro se detiene aquí hasta que termine el hijo
resultado = notebookutils.notebook.run("Procesar_Ventas_Region", 3600, {"Region": region})
print(f"Resultado: {resultado}")El principal problema aquí no es solo el tiempo. Es la falta de resiliencia. Si la tercera región falla, el bucle se rompe (a menos que añadas lógica compleja de try-except) y las regiones restantes quedan sin procesar. Este es un error común que arrastra el rendimiento y la fiabilidad de las cargas en el almacén de datos de Fabric.
notebookutils.notebook.runMultiple: El motor de paralelismo
Para escenarios donde las tareas son independientes entre sí, runMultiple es la opción ganadora. Esta función permite enviar un conjunto de ejecuciones de notebooks al motor de Spark de forma simultánea. Pero no se trata solo de un «dispara y olvida»; runMultiple acepta una estructura de configuración que puede definir dependencias, creando un grafo acíclico dirigido (DAG) dentro de una sola celda de Python.
Al usar runMultiple, Fabric optimiza el uso de los ejecutores y puede reducir drásticamente el tiempo total. Si el procesamiento de cada región tarda 5 minutos, la ejecución secuencial tardaría 20 minutos, mientras que con runMultiple (y suficiente capacidad), podría terminar en poco más de 5 minutos.
# Ejemplo de ejecución paralela con runMultiple
conf = [
{"path": "Procesar_Ventas_Region", "parameters": {"Region": "Norte"}},
{"path": "Procesar_Ventas_Region", "parameters": {"Region": "Sur"}},
{"path": "Procesar_Ventas_Region", "parameters": {"Region": "Este"}},
{"path": "Procesar_Ventas_Region", "parameters": {"Region": "Oeste"}}
]
# Ejecutamos con una concurrencia máxima de 4
resultados = notebookutils.notebook.runMultiple(conf, {"concurrency": 4})
print("Todas las ejecuciones han finalizado.")Es vital destacar que runMultiple devuelve un objeto que contiene el estado de todas las ejecuciones. Esto facilita enormemente el control de errores, permitiéndonos identificar exactamente qué rama del proceso ha fallado sin detener el resto.
Límites de concurrencia y gestión de recursos
No todo es velocidad. Como consultor, uno de los errores más frecuentes que veo es intentar ejecutar 20 notebooks en paralelo en una capacidad F2 o F4. Fabric tiene límites estrictos de concurrencia de Spark que dependen del tamaño de tu capacidad. Si excedes estos límites, los notebooks entrarán en cola o fallarán por falta de recursos.
Además, cada notebook ejecutado mediante run o runMultiple puede consumir una sesión de Spark independiente. En Fabric, esto se gestiona mediante el concepto de «High Concurrency Mode». Si los notebooks comparten la misma configuración de entorno y pool, pueden reutilizar la sesión, lo que reduce el tiempo de arranque de segundos a prácticamente cero. Sin esto, podrías pasar más tiempo esperando a que arranque Spark que procesando datos de bases de datos espejadas o archivos Delta.
La importancia del parámetro DAG
Una característica avanzada de runMultiple es la capacidad de definir dependencias. Esto es útil cuando, por ejemplo, necesitas procesar las tablas de dimensiones (Producto, Cliente, Calendario) antes de procesar la tabla de hechos (Ventas), pero quieres que todas las dimensiones se procesen en paralelo entre sí.
Tabla comparativa: run vs runMultiple
| Característica | notebookutils.notebook.run | notebookutils.notebook.runMultiple |
|---|---|---|
| Tipo de ejecución | Síncrona / Secuencial | Asíncrona / Paralela |
| Complejidad de código | Baja (un bucle simple) | Media (requiere JSON de config) |
| Gestión de errores | Detiene el flujo al primer error | Permite continuar y reportar al final |
| Escalabilidad | Pobre en grandes volúmenes | Excelente (según capacidad CU) |
| Dependencias | Manuales (orden del código) | Nativas (mediante el parámetro dag) |
Errores comunes en la orquestación de notebooks
- No controlar el tiempo de espera (Timeout): Por defecto, un notebook puede esperar indefinidamente. Es fundamental establecer un timeout razonable para evitar que una sesión colgada consuma recursos de la capacidad durante horas.
- Ignorar el valor de retorno: Muchos ingenieros olvidan usar
notebookutils.exit("Mensaje")en el notebook hijo. Sin esto, el maestro no puede recibir metadatos sobre el éxito de la operación. - Exceso de paralelismo: Configurar una concurrencia de 50 en una capacidad pequeña provocará errores de throttling. Siempre mide el impacto en las Capacity Metrics antes de subir el nivel de paralelismo.
- Mezclar lógica de negocio con orquestación: El notebook maestro debe ser puramente un orquestador. Si empiezas a realizar transformaciones pesadas en el mismo notebook que lanza
runMultiple, estás desperdiciando recursos.
Para asegurar que los datos escritos sean eficientes y no degraden el rendimiento futuro, recuerda revisar técnicas de optimización como los checkpoints de Delta en Fabric, especialmente cuando lanzas múltiples procesos de escritura simultáneos sobre la misma tabla (aunque esto último debe evitarse para prevenir conflictos de escritura).
Checklist para una orquestación profesional en Fabric
- Modulariza: Cada notebook debe hacer una sola cosa (una tabla, una fase de limpieza).
- Usa runMultiple: Siempre que las tareas sean independientes, evita los bucles secuenciales.
- Gestiona la concurrencia: Ajusta el parámetro
concurrencysegún el tamaño de tu capacidad F-SKU. - Implementa reintentos: Fabric es un entorno distribuido; los fallos transitorios ocurren. Envuelve tus llamadas en lógica de reintento si la criticidad es alta.
- Monitoriza: Revisa el Spark UI y la App de Capacity Metrics para entender el coste real de tu paralelismo.
Preguntas frecuentes
¿Puedo usar runMultiple para ejecutar notebooks en diferentes Workspaces?
Actualmente, notebookutils.notebook.run y runMultiple están diseñados principalmente para operar dentro del mismo Workspace o mediante rutas relativas/absolutas permitidas por los permisos del usuario. Para ejecuciones entre diferentes Workspaces, la solución suele pasar por Data Factory Pipelines.
¿Qué ocurre si uno de los notebooks en runMultiple falla?
Por defecto, runMultiple intentará completar todas las tareas que no dependan del nodo fallido. Al final de la ejecución, obtendrás un informe detallado con el estado de cada tarea. Es responsabilidad del desarrollador analizar ese objeto de respuesta y decidir si el pipeline global debe marcarse como fallido, algo similar a lo que ocurre en una actualización programada en Power BI cuando fallan las credenciales de una sola fuente.
¿Es mejor usar Pipelines de Data Factory o runMultiple?
Depende de la complejidad. Si necesitas una interfaz visual, disparadores por eventos o integración con otras actividades (como Dataflows o Scripts SQL), usa Pipelines. Si buscas un control total mediante código, menor latencia en el arranque de tareas y gestión dinámica de dependencias mediante Python, runMultiple es superior.
¿Cuántos notebooks puedo ejecutar en paralelo realmente?
No hay un número fijo, ya que depende de los núcleos (vCores) disponibles en tu capacidad de Fabric y de cuántos ejecutores solicite cada notebook. Una capacidad F64 permite mucha más concurrencia que una F2. El secreto está en balancear el número de ejecutores por notebook frente al número de notebooks totales.





