Cuando pensamos en general en grandes volúmenes de datos y en entorno analíticos el particionado suele ser un concepto clave para poder tener un rendimiento adecuado. Cuando comparamos los Synapse dedicated SQL pools con SQL Server aparecen diferencias importantes y que nos afectan a cómo abordar este particionado de una forma óptima.

Sistemas MPP, ¿qué son?

Lo primero que tenemos que entender es que Synapse dedicated SQL pool es un sistema MPP (Massive Parallel Processing), una evolución de APS (Microsoft Analytics Platform System) que a su vez lo fue de PDW (SQL Server Parallel Data Warehouse). Los sistemas MPP por su naturaleza nos permiten una escalabilidad mucho mayor que un SMP (Symmetric Multi-Processing) al permitir escalar horizontalmente. A cambio de ello, implican una mayor complejidad y costes de comunicación entre nodos que debemos tener en cuenta.

Desde mi punto de vista, debemos considerar un sistema MPP como alternativa cuando de forma clara se exceda la capacidad de cómputo de un sistema SMP. Suele ser demasiado habitual ir directamente a una solución MPP para volúmenes de datos no demasiado grandes y con necesidades de cómputo perfectamente manejables por un SMP.

También es cierto en algunos casos puede tener sentido ir a MPP por economía de escala, ya que suele ser más barato disponer de 4 nodos con 1-2 sockets y 4 TB de RAM que no un único sistema con 8 sockets y 16 TB de RAM. Esto es también especialmente cierto en entornos virtualizados/cloud donde las máquinas de mayor tamaño tienen un incremento en el precio importante al ser mucho más “nicho” que las más pequeñas. Por tanto al final hablamos de buscar un compromiso entre el total de recursos, coste y complejidad añadida.

Distribuciones en Synapse, un concepto clave

Un concepto clave antes de poder entrar en el particionado sobre Synapse es el concepto de distribución. Una distribución es la unidad básica de almacenamiento y procesamiento para las operaciones paralelas. El número de distribuciones en Synapse es de 60 por lo que, por decirlo de alguna manera, cuando una consulta se ejecuta la estamos dividiendo en 60 pequeñas operaciones que ejecutan en paralelo.

En función del tamaño del dedicated SQL pool, cada nodo de computación manejará entre 1 y 60 distribuciones en los casos más extremos (60 nodos vs 1 único nodo). Si nos fijamos en los tamaños más pequeños únicamente se utiliza 1 compute node y se escala hacia arriba dicho nodo desde 60 GB de memoria hasta 300 GB. Esto en cierta forma apoya la idea que para tamaños pequeños es mejor mantenernos en una configuración de 1 solo nodo, similar a un SMP.

A partir de este punto se comienzan a añadir nodos, cada  uno con 300 GB de memoria, hasta llegar al máximo con el DW30000c con 18 TB de memoria en total:

service Levels

También debemos tener en cuenta que un sistema como Synapse no está pensado para una gran concurrencia y tiene un número limitado de “concurrency slots” que en función de la clasificación que se haga de las operaciones y el tamaño que tengamos nos permitirá ejecutar más o menos consultas de forma concurrente. Según el modelo de clasificación que elijamos (estático o dinámico) el aumentar las DWu nos permitirá mayor concurrencia sobre un mismo volumen de datos (estático) o mayor concurrencia sobre un volumen de datos que aumenta (dinámico).

¿Cómo nos afectan las distribuciones al uso del particionado de datos?

El particionado en Synapse se ejecuta a nivel de distribución, por lo que aplicará sobre un conjunto de 60 distribuciones. Por tanto tenemos que asegurarnos que la volumetría de cada una de esas 60 distribuciones ya resulta ser suficientemente elevada como para que el particionado adicional sobre esta distribución de datos pueda aportarnos ventajas y no causarnos más problemas.

¿Cuál es el caso más habitual de problemas que nos encontraremos? Pues el caso más habitual es que estemos utilizando índices columnares y el volumen tras particionar resulte demasiado pequeño, menos de 1 millón de filas por partición y distribución. Es decir, que si por ejemplo generamos particiones por mes y mantenemos 5 años de datos, tendríamos 60 particiones que multiplicadas por 60 distribuciones nos daría que la volumetría “recomendable” de, al menos, 3600 millones de filas. En muchos casos no tenemos una volumetría tan elevada por lo que lo recomendable sería bajar la granularidad del particionado, por ejemplo 1 partición cada año, de forma que con 300 millones de filas o 60 millones por año ya podamos obtener buenos ratios de compresión con el índice columnar.

Centrándonos en las diferencias al particionar entre SQL Server y Synapse veremos que en Synapse se ha simplificado el particionado y se encuentra estrechamente ligado a la creación de la tabla. Es decir, en Synapse no vamos a tener esquemas de particionado ni funciones de particionado como tenemos en SQL Server, únicamente vamos a tener que indicar la columna por la que particionaremos (por ejemplo la fecha), los puntos ( boundaries) que separan cada una de las particiones y si queremos que los límites estén incluidos en la partición de la izquierda o de la derecha (RANGE LEFT/RIGHT).

Pasando a la acción, SMP vs MPP

Una vez tenemos ya los conceptos básicos claros y sus implicaciones, vamos a realizar algunas pruebas con Synapse y las vamos a comparar con el rendimiento que podamos obtener con un SMP en Azure. Más concretamente vamos a comparar el rendimiento de algunas consultas particionadas y sin particionar en dos escenarios:

  • SQL VM con 8 vCore SQL Server EE -> 47€/h vs Synapse DW500c  -> 6.367€/h
  • SQL VM con 80 vCores SQL Server EE -> 7€/h vs Synapse DW5000c  -> 63.67€/h

Preparando los datos… 1000 millones de filas para desayunar.

Para poder realizar la prueba realizaremos una carga inicial de 1000 millones de filas sobre una tabla con un índice columnar clustered sin particionar. Para realizar la carga utilizaremos 10 threads, cada uno de ellos responsable de insertar 100 millones de filas:

Particionado Synapse VS SQL Server – Parte 1.

Para la carga no hemos planteado ningún tipo de optimización especial, simplemente hemos insertado las filas en batches de 1 millón de filas por lo que los tiempos de carga no serán especialmente buenos.

La estructura de la tabla utilizada es muy sencilla, con un identificador, una columna de texto para darle un poco de volumen y una columna filter que utilizaremos posteriormente para particionar:

CREATE TABLE test_cci (id int identity, payload varchar(4000), filter int)
CREATE CLUSTERED COLUMNSTORE INDEX cci ON test_cci;

Para realizar la carga lo que haremos es insertar valores aleatorios para el payload y el valor del filtro será un valor entre 0 y 9 en base a un cálculo módulo 10 basado en el payload aleatorio de entre 0 y 99 caracteres:

insert into test_cci (payload,filter)
select TOP 1000000 payload, abs(checksum(payload))%10 as filter
FROM
(
SELECT convert(varchar(4000),crypt_gen_random (abs(checksum(newid()))%100),1) as payload
from sys.objects s1,sys.objects s2,sys.objects s3,sys.objects s4,sys.objects s5, sys.objects s6
) a
— 1M rows
 

Siguiendo este esquema hemos necesitado 76 minutos para cargar 1000 millones de filas (13 millones de filas por minuto). La carga se ha hecho directamente al columnar en un formato comprimido y por tanto hemos castigado bastante la CPU al tratarse de una máquina con solo 8 cores que han estado trabajando “a tope” durante esos 76 minutos:

Particionado Synapse VS SQL Server – Parte 1.

Una vez cargados los datos, ¿qué ocurre con la velocidad de lectura tras la carga?

Cuando tenemos un DW uno de los objetivos es poder tener una velocidad de lectura aceptable desde disco para que el rendimiento de los scans no se degrade en exceso.

Por ejemplo, si trabajaramos en on-premise en un portátil con un SSD NVME podemos obtener velocidades típicas de alrededor 2.5 GB/s haciendo un scan de una tabla de SQL Server:

Database-

En el caso de la VM de 8 cores utilizada, de la serie L que está optimizada para estos escenarios de alta demanda de entrada/salida, obtenemos valores de aproximadamente 1.6 GB/s que no está mal para tratarse de una máquina virtual de 8 cores en cloud:

Database

La primera de las consultas que lanzaremos sobre los datos cargados es una consulta que intenta buscar la menor de las cadenas generadas aleatorias:

select min(len(payload)) from test_cci

Esta consulta fuerza la lectura de la columna de texto, su descompresión y su filtrado. Durante este tipo de operación saturamos tanto la CPU antes que la entrada/salida, que queda a aproximadamente 1/3 de su capacidad:

CPU
Particionado Synapse VS SQL Server – Parte 1.

El tiempo total de ejecución ha sido de 991 segundos con un consumo de CPU de 6859 segundos (ratio 7 a 1):

SQL Server Execution Times:
CPU time = 6859202 ms, elapsed time = 991442 ms.

También podemos lanzar la misma consulta con un filtro que seleccionará aproximadamente 1/10 parte del total de filas a procesar por lo que se trabajará sobre aproximadamente 100 millones de filas:

select min(len(payload)) from test_cci where filter=1
Particionado Synapse VS SQL Server – Parte 1.

El tiempo en este caso se reduce a únicamente 1/3 del total y ya el cuello de botella deja de estar en la CPU (ratio 4 a 1) y pasa a estar en la IO:

CPU
SQL Server Execution Times:
CPU time = 1422625 ms, elapsed time = 343770 ms.

Dividiendo el desayuno en bocados más pequeños

A continuación vamos a crear una función de particionado y un esquema de particionado para particionar en 10 particiones de aproximadamente 100 millones de filas cada una:

CREATE PARTITION FUNCTION [fn_filter](int) AS RANGE LEFT FOR VALUES (N‘0’, N‘1’, N‘2’, N‘3’, N‘4’, N‘5’, N‘6’, N‘7’, N‘8’, N‘9’)
CREATE PARTITION SCHEME [sch_filter] AS PARTITION [fn_filter] TO ([PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY])

Crearemos una nueva tabla particionada y copiaremos los 1000 millones de filas partición a partición (las lanzaremos en paralelo):

CREATE TABLE test_cci2 (id int identity, payload varchar(4000), filter int) ON [sch_filter]([filter])
CREATE CLUSTERED COLUMNSTORE INDEX [cci] ON [dbo].[test_cci2] WITH (DROP_EXISTING = OFF, COMPRESSION_DELAY = 0, DATA_COMPRESSION = COLUMNSTORE) ON [sch_filter]([filter])
 
INSERT INTO test_cci2 (payload,filter)
SELECT payload,filter from test_cci where filter=0
 
INSERT INTO test_cci2 (payload,filter)
SELECT payload,filter from test_cci where filter=1
 
()
 
INSERT INTO test_cci2 (payload,filter)
SELECT payload,filter from test_cci where filter=9
query

De nuevo durante esta operación la CPU nos hará de cuello de botella al tener que descomprimir para extraer las filas y tener que volver a comprimir los datos al escribir al nuevo índice columnar particionado:

Particionado Synapse VS SQL Server – Parte 1.

Esto no es algo que nos sorprenda especialmente, ya que los índices columnares ofrecen importantes ventajas de rendimiento pero tienen como contrapartida que el consumo de CPU suele ser elevado. Por decirlo de alguna forma, permiten aplicar “fuerza bruta” a este tipo de operaciones lo cual puede requerir un dimensionamiento entre entrada/salida y CPU distinto al que estábamos acostumbrados con el almacenamiento rowstore.

Evaluando el impacto del particionado sobre SMP en las lecturas

Una vez la carga esté completada el siguiente paso será probar cuanto impacto tiene el particionado cuando filtramos por la columna de particionado.

Para ello volveremos a lanzar la consulta que lanzamos anteriormente sin particionado:

select min(len(payload)) from test_cci2 where filter=1

Podemos ver que en el plan de ejecución se aplica el filtro por partición, seleccionándose la segunda partición (la primera correspondería a filter=0):

partición

Al usar particionado vemos que disminuye la presión que hacemos sobre disco, quedando en alrededor de unos 570 MB/s:

Particionado Synapse VS SQL Server – Parte 1.

Esto es debido a que la CPU queda totalmente saturada al llegarle ya todas las filas “prefiltradas” y teniendo que trabajar más duro procesando sin parar todas las filas versus descartar 9/10 de las filas como hacíamos antes:

Particionado Synapse VS SQL Server – Parte 1.

Debido a esto, el uso de IO y CPU que estamos haciendo es más eficiente y ello repercute en unos mejores tiempos tanto de consumo de CPU (-50%), de IO (-90%) y ello repercute en una menor duración (-70%):

SQL Server Execution Times:
CPU time = 668309 ms, elapsed time = 97213 ms.

¿Qué ocurre con la carga de escritura al escalar nuestro SMP?

El siguiente paso sería realizar un escalado de los recursos de la máquina virtual hasta los 80 cores y volver a testear el comportamiento que tenemos con una máquina mucho más potente.

Particionado Synapse VS SQL Server – Parte 1.
Particionado Synapse VS SQL Server – Parte 1.

Como curiosidad respecto a los distintos problemas que pueden acarrear las cargas, si lanzamos decenas de procesos de carga concurrentes, conseguimos aumentar el uso de CPU y se distribuyen correctamente entre todos los cores:

Particionado Synapse VS SQL Server – Parte 1.

Sin embargo en este caso con el diseño de la tabla basado en un identity nos encontramos con un cuello de botella a la hora de generar dichos valores y realmente en el momento que tenemos más de 16-20 procesos concurrentes ya el rendimiento no mejora prácticamente nada por esta razón. Por tanto, usar un identity no es una buena idea para escalar en rendimiento de escritura.

Particionado Synapse VS SQL Server – Parte 1.

El tiempo de inserción de los 1000 millones de filas se redujo a algo menos de 30 minutos, algo no proporcional al aumento en 10 veces de los recursos de CPU. En este tipo de casos sería recomendable el uso de secuencias y la preasignación en bloques para cada grupo de datos, es decir, solicitarle a la secuencia “dame 1 millón de IDs” y ya usarlos internamente en el proceso como valores a insertar, sin depender de la secuencia ya para la inserción durante el proceso.

Analizando el impacto en las lecturas tras escalar nuestro SMP

Volviendo al tema de las consultas vamos a comprobar cual es el rendimiento tanto en la consulta no particionada como en la particionada.

Durante la ejecución de la consulta sobre la tabla sin particionar vemos que el uso de la CPU se distribuye entre todos los cores de forma bastante equilibrada:

Logical Processors

La velocidad de lectura en este caso sube hasta 4 GB/s

Velocidad de lectura

Debemos tener en cuenta que SQL Server “dispara” el Soft Numa automáticamente al detectar más de 8 cores. En total tendremos 10 nodos numa de 8 cores desde el punto de vista de SQL Server:

SQL Server

Si dejamos el MAXDOP al valor por defecto nos encontramos que la ejecución usando todos los cores no es realmente muy eficiente. Con 80 cores la duración es 1/5 de la duración original (pese a haber aumentado 10X los cores):

select min(len(payload)) from test_cci option (maxdop 80)
 
SQL Server Execution Times:
CPU time = 6536188 ms, elapsed time = 201094 ms

Si bajamos a 40 cores por ejemplo nos encontramos que el rendimiento algo menos de 1/3 del tiempo que teníamos con 8 cores por lo que aumento de cores nos proporciona un aumento del rendimiento aunque no tanto como esperaríamos:

select min(len(payload)) from test_cci option (maxdop 40)
 
SQL Server Execution Times:
CPU time = 6184281 ms, elapsed time = 319295 ms.

 

Si lanzamos la consulta filtrada con un máximo de 80 cores sobre la tabla particionada veremos que la duración se reduce a prácticamente 1/8 del tiempo original:

select min(len(payload)) from test_cci2 where filter=1 option (maxdop 80)
 
SQL Server Execution Times:
CPU time = 565636 ms, elapsed time = 12636 ms.
 

Si limitamos a 40 cores vemos que la duración se reduce prácticamente en la misma medida:

select min(len(payload)) from test_cci2 where filter=1 option (maxdop 40)
 
SQL Server Execution Times:
CPU time = 546222 ms, elapsed time = 13781 ms.

Si limitamos a 20 cores ya el tiempo se degrada más notablemente aunque sigue siendo 1/3 del tiempo original (debemos tener en cuenta que no solamente aumenta la CPU al escalar, también vemos como la IO en lecturas de la tabla nos sube de 1.6 GB/s hasta 4.5 GB/s):

select min(len(payload)) from test_cci2 where filter=1 option (maxdop 20)
 
SQL Server Execution Times:
CPU time = 544297 ms, elapsed time = 27338 ms.
Database

Con estos resultados podemos ver que en este tipo de configuraciones SMP el escalado vertical es posible que venga bastante condicionado tanto al particionado usado así como la configuración de paralelismo que realicemos. También debemos considerar la capacidad que tenga el hardware subyacente (el host de la máquina virtual en este caso) de ser escalado de forma homogénea y suficiente. Es decir, es habitual que los escalados verticales solo nos permitan aumentar recursos en algunos aspectos pero no en todos (por ejemplo no en ancho de banda a memoria o no en ancho de banda de entrada/salida). Debido a ello y a las propias limitaciones de un sistema SMP, del impacto del NUMA, etc. lo habitual es encontrarnos con escalabilidades no lineales con el aumento de recursos en un SMP.

Conclusión

En esta primera parte hemos visto qué rendimiento podemos esperar de un sistema SMP y su comportamiento ante el particionado y el escalado vertical de recursos. Es cierto que estos resultados usando sistemas especializados SMP para DW podrían ser mejorados, pero hemos querido utilizar como ejemplo un sistema que estén al alcance de muchos bolsillos y esté disponible en un entorno de cloud público, que no requiera de la compra de hardware especializado.

Para aquellos que quieran ampliar información y estén interesados en sistemas SMP especializados para DW les recomiendo que accedan a la web de TPC y en el apartado de TPC-H analicen los reportes completos de las configuraciones hardware y software utilizadas en las pruebas. Los costes totales de estas soluciones no están al alcance de la mayoría de las organizaciones ya que son del orden de varios cientos de miles de euros. Por poner algún ejemplo, tenemos esta configuración de DELL con un coste de hardware más software de 500K: http://www.tpc.org/results/fdr/tpch/dell~tpch~3000~dell_poweredge_r7525~fdr~2021-07-03~v01.pdf o este de HP de 1 millón de euros: http://www.tpc.org/results/fdr/tpch/hpe~tpch~10000~hp_proliant_dl385_gen10_plus~fdr-2021-04-02~v01.pdf)

¿Necesitas sacar más partido a tus datos para impulsar tu proyecto?

Acelera tus procesos de Business Analytics. Toma mejores decisiones, optimiza tu Datawarehouse y el proceso de generación de informes en tus proyectos de analítica con nuestra consultoría en Business Intelligence.

Quiero Más Información
0 Shares:
Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

You May Also Like
Leer más

Hilando fino en SSAS multidimensional

El equipo de SolidQ ha estado buscando la mejor manera de implementar una jerarquía padre-hijo de cuentas contables con un operador unitario que tuviera un buen rendimiento, a pesar de la gran cantidad de datos a la que tenía que enfrentarse. Veremos cómo aplanar la jerarquía, cómo implementarlo con SSAS, con una alternativa MDX, cómo añadir ordenación a las cuentas basadas en otro atributo, Time Balance Average y algún otro truco de tuning.