Últimamente nos hemos enfrentado a una situación algo complicada relacionada con el rendimiento y el uso de memoria de Integration Services. Se trata de, ante dos orígenes de datos que devuelven información a diferente velocidad, la necesidad de realizar un proceso de carga incremental. Esto requiere una operación parcialmente bloqueante (es decir, necesita parte del conjunto de datos para empezar a añadir datos al flujo) como es un Merge Join para comparar las filas que vienen de ambos orígenes y determinar cuál es nueva, cuál modificada y cuál hay que eliminar.

 

Figura 1 – Esquema

 

El código en C# (lo siento por los amantes de VB) para cada componente utilizado para simular los orígenes de datos a diferentes velocidades es muy sencillo, sólo modificamos el método CreateNewOutputRows() que el propio script tiene ya preparado cuando lo abrimos para sobrescribir. Para el componente rápido:

public override void CreateNewOutputRows() 
{ 
long limite = 100000000; 
for (int i = 0; i < limite; i++) 
{ 
/*añadimos valores a las diferentes filas, simplemente porque no estén a null*/ 
Salida0Buffer.AddRow(); 
Salida0Buffer.Columna = i; 
Salida0Buffer.Columna1 = i + 1; 
Salida0Buffer.Columna2 = i + 1; 
} 
} 

Para el componente lento:

public override void CreateNewOutputRows() 
{ 
long limite = 100000000; 
for (int i = 0; i < limite; i++) 
{ 
for (int j = 0; j < 200000; j++) 
{ 
/*iteramos para ralentizar la salida de filas al buffer*/ 
} 
/*añadimos valores a las diferentes filas, simplemente porque no estén a null*/ 
Salida0Buffer.AddRow(); 
Salida0Buffer.Columna = i; 
Salida0Buffer.Columna1 = i + 1; 
Salida0Buffer.Columna2 = i + 1; 
} 
}

Se trata de un recurso muy sencillo pero efectivo. Utilizamos un bucle previo a la adición de cada fila para ralentizarla y provocar que el buffer tarde más en llenarse. Ambos componentes personalizados son iguales en todo lo demás (ordenados por la primera columna, ambos configurados como orígenes de datos, mismo número de columnas de salida).

Nos encontramos que, al tener diferentes velocidades, muchísimas filas quedan a la espera en el componente Merge Join hasta que las filas del otro origen llegan, se produce la comparación y SSIS libera los buffers que ya no necesita y que estaban ocupados para alojar las filas en el flujo de datos.

Figura 2 – Ejecución con buffers de 10000 filas

 

En el momento que se ilustra con la Figura 2 todas las filas que ya han salido del origen rápido menos las que han salido hacia el «Destino dummy» están alojadas en memoria RAM.

Esto provoca una situación de carga de memoria puede llegar a ser muy problemática en casos de cargas con grandes volúmenes de datos, ya que podemos quedarnos sin memoria y que SSIS empiece a hacer swapping a disco (utilizar ficheros temporales en el disco duro para almacenar los buffers que no le caben en memoria RAM).

Esto da lugar a una ralentización enorme del proceso, e incluso si el disco que tenemos asignado para swapping es pequeño podemos encontrarnos con que el proceso entero falla porque también agota el espacio en disco duro.

¿Qué podemos hacer entonces? En principio, la solución más adecuada sería intentar redefinir el modelo de carga para adaptarlo a la máquina sobre la que se ejecuta. Es decir, si no tenemos suficiente memoria o disco para soportar tanto volumen de datos simultáneamente, dividir la carga en diferentes pasos, por ejemplo.

Pero como medida de emergencia mientras no tengamos tiempo o recursos para evitar la situación de falta de memoria, podemos jugar con el tamaño de los buffers en SSIS. Mediante la variable DefaultBufferMaxRows que se encuentra en las propiedades de nuestro DataFlow podemos establecer el tamaño máximo de nuestro buffer (en filas). Por defecto SSIS lo establece a 10.000, pero podemos modificarla.

Figura 3 – Variable modificada

 

Si bajamos el número de filas máximas que puede contener un buffer, SSIS tardará menos en llenarlo y por lo tanto lo liberará antes hacia la operación semi-bloqueante, realizará las comparaciones pertinentes y liberará antes los buffers. Esto provocará que no cargue tanto la memoria pues deberá esperar menos antes de lidiar con cada buffer. Si bien es cierto que esto presiona más la CPU ya que tiene que generar más buffers y tratar con ellos, reducimos la carga de memoria y probablemente evitaremos el fallo por falta de recursos.

Figura 4 – Ejecución con buffer de 4000 filas

 

Podemos ver que para una cantidad muy similar de filas leídas del origen rápido ha leído casi el triple de filas del origen lento, ha trabajado con ellas en el merge join y las ha mandado al destino dummy que tenemos para simular uno real. Esto implica que ha liberado el triple de buffers que por lo tanto no están ocupando memoria, evitando en gran medida las situaciones de falta de recursos que hemos descrito antes.

Para un análisis pormenorizado de uso de recursos (tanto desde el administrador de tareas como utilizando Performance Counters) os invito a que implementéis este sencillo ejemplo y juguéis con los tamaños de buffer para observar las evoluciones de la memoria y la carga de trabajo de la CPU. Seguro que os da ideas para alguna solución en vuestros proyectos.

Pau Sempere

Mentor at SolidQ
I work as a Mentor at SolidQ, participating in BI, SQL Server, Big Data and Data Science projects. I hold a master's degree in Computer Science by the University of Alicante (2005-2011).
Pau Sempere