En los últimos años vamos viendo cada vez más entornos híbridos donde algunos SQL Server se desplazan a la nube. En estos escenarios otras aplicaciones, servicios, ERPs o incluso otras instancias SQL Server siguen viviendo OnPremise en el datacenter original. Esta situación implica que cuando existan conexiones entre ambos entornos, las conexiones tendrán, en comparación con las conexiones que no crucen entre entornos, restricciones de ancho de banda y latencias mayores.

En el pasado hablamos de esta problemática en este blog cuando estas operaciones eran orientadas a fila y cada fila incurría en la penalización de la latencia de red (https://blogs.solidq.com/es/dotnet_es/row-by-agonizing-row-rbar-y-la-latencia-de-red/ ) . Sin embargo, tenemos otras situaciones donde, aunque trabajemos con operaciones orientadas conjuntos de datos, el rendimiento total es menor del esperado. Antes de comenzar vamos a ver el rendimiento “bruto” que tenemos en este caso, en una conexión local, utilizando para ello iPerf3:

Por tanto, partimos de un escenario inicial donde con una latencia de red local alcanzamos algo más de 800 Mbps de media, con picos de 953 Mbps, siendo éstos valores bastante típicos de una red local a 1Gbps. Vamos a utilizar la herramienta Clumsy (https://jagt.github.io/clumsy/download.html) para añadir latencias de red desde 1 ms hasta 100ms:

Si volvemos a lanzar la misma prueba de iPerf3 vemos que la velocidad que, en nuestro entorno de prueba, podemos obtener con una sola conexión desciende de forma brusca a medida que aumenta la latencia. En esta red no se ha realizado ninguna optimización desde el punto de vista de TCP ni de transporte, se están utilizando los valores por defecto de la pila TCP de Windows Server 2016:

Podemos comparar estos datos con datos de referencia públicos como estos (https://docs.microsoft.com/en-us/azure/expressroute/expressroute-troubleshooting-network-performance#latencybandwidth-results ) y vemos que la curva es similar, con caídas muy pronunciadas incluso con pequeñas latencias:

Si comparamos los tiempos de una consulta SQL Server que mueva cierto volumen de datos por red vemos que con el aumento de la latencia el tiempo total va aumentando. Por ejemplo para esta consulta que mueve 5 MB de datos, podemos pasar a tener esperas que van desde 40ms hasta 383 ms.

-- 5 MB 
select top 5000 convert(char(1000),'') a from sys.objects s1, sys.objects s2, sys.objects s3, sys.objects s4, sys.objects s5

El dato en bruto, unas pocas milésimas, puede parecer poco pero tenemos que contextualizarlo del contexto de una red de 1 Gbps, donde podemos mover más de 100 MB/s por lo que 5 Mb/s esperaríamos poderlos mover en 1/20 de segundo (50ms). ¿Qué ocurrirá si en vez de mover 5 MB queremos mover 5 GB? Ocurrirá que en vez de tardar 40 segundos, nos llevará 383 segundos (6 minutos y 23 segundos), lo cual ya puede empezar a causar desviaciones de tiempo sustanciales en paquetes ETL de alta frecuencia de carga. Y si en vez de mover 5 GB fuesen 50 GB (algo tampoco descabellado por lo que vemos en clientes) hablaríamos de pasar de un tiempo de 6 minutos y 40 segundos a más de 1 hora.

Por tanto, cuando el volumen de datos a mover por red es elevado es tan importante conocer el ancho de banda disponible como la latencia que vamos a tener extremo a extremo. En función de dicha latencia si queremos aprovechar el ancho de banda disponible tendremos que rediseñar nuestros procesos para que sean multihilo y combinar los resultados en destino.

Por ejemplo, si estamos hablando de una extracción por fechas y vamos a obtener los datos de un mes, podemos fácilmente dividir la carga por días y realizar N extracciones, una por día y paralelizarlas en M threads concurrentes. Incluso no sería descabellado un thread por día del mes, de forma que tengamos entre 28 y 31 threads concurrentes según los días que tenga cada mes.

La implementación básica sería tan sencilla como añadir un filtro extra a nuestra consulta “where datepart(day,columnaFecha)= 1,2,3,(…),31”. Esta implementación podría servir en muchos casos pero habrá casos en los que no funcione muy bien. Si extraemos datos de un datawarehouse es normal que tenga un diseño físico optimizado para trabajar con rangos de fechas, por lo que probablemente será mejor idea utilizar un filtro con un rango pequeño “where columnaFecha >= ‘20180101’ columnaFecha < ‘20180102’”. En todo caso ayudaría mucho que conociéramos el modelado que se ha hecho, ya que podemos tener por ejemplo particiones por semanas u otro factor que tengamos que tener en cuenta a la hora de seleccionar los rangos idóneos.

¿Qué ocurre si no tenemos ninguna fecha para realizar el reparto? En ese caso nuestra primera opción sería utilizar algún campo que tenga suficiente variabilidad como para generar un reparto justo con una función módulo. Por ejemplo, una columna autonumérica podría ser un candidato ideal, donde pondríamos un filtro de este estilo “where (id % 10)=0,1,2,3,(…),8,9”. Si no disponemos de esa columna podemos utilizar alguna de tipo texto que sea lo más “única” posible por fila. Por ejemplo, sobre un campo de DNI podemos calcularle un CHECKSUM y realizar el módulo sobre él “where ABS(CHECKSUM(dni)) % 10 = 0,1,2,3,(…),8,9”.

Por último, si no tenemos ninguno de esos casos, como la consulta anterior de 5 MB de peso que devolvía solamente cadenas de texto con blancos, podemos utilizar NewId() como fuente de aleatoriedad “where ABS(CHECKSUM(NewId())) % 10 = 0,1,2,3,(…),8,9”. El problema de esta aproximación es que impediría por ejemplo que implementáramos “reintentos parciales” de la carga, ya que cada ejecución nos devolvería unas filas distintas para cada una.

select top 5000 convert(char(1000),'') a, ABS(CHECKSUM(NewId())) % 10 filtro from sys.objects s1, sys.objects s2, sys.objects s3, sys.objects s4, sys.objects s5

 

Finalmente, en aquellos casos donde el coste de la ejecución de la consulta es muy importante (por ejemplo si hay una vista compleja, ordenaciones pesadas, agrupaciones, etc.) es posible que no sea asumible lanzar 10 peticiones concurrentes y acabemos empeorando la situación por saturar la fuente de datos. En esos casos es posible que nos interese materializar el resultado global, por ejemplo en una tabla de staging o temporal global en el mismo origen de datos (para que no sufra el impacto de la red), y posteriormente ya leer de esa tabla plana con el filtro correspondiente.

En conclusión, cuando nos planteemos ir a un entorno híbrido necesitamos realizar pruebas que simulen las características de la red adecuadas. Desgraciadamente en general el dimensionamiento queda en una mera comparación de anchos de banda donde, siendo superior a la utilización media actual, se acepta como válido. El impacto de la latencia está bastante estudiado pero la técnica más apropiada para reducir el impacto cada caso es algo que tenemos que analizar caso a caso individualmente, lo cual puede tener un impacto importante cuando el número de aplicaciones, ETLs, etc. es elevado.

 

Rubén Garrigós

Rubén Garrigós is an expert in high-availability enterprise solutions based on SQL Server design, tuning, and troubleshooting. Over the past fifteen years, he has worked with Microsoft data access technologies in leading companies around the world. He currently is a Microsoft SQL Server and .NET applications architect with SolidQ. Ruben is certified by Microsoft as a Solution Expert on the Microsoft Data Platform (MSCE: Data Platform) and as a Solution Expert on the Microsoft Private Cloud (MSCE: Private Cloud). As a Microsoft Certified Trainer (MCT), Ruben has taught multiple official Microsoft courses as well as other courses specializing in SQL Server. He has also presented sessions at official events for various Microsoft technologies user groups.

Latest posts by Rubén Garrigós (see all)