En ocasiones nos encontramos situaciones bastante anómalas que requieren análisis “think out of the box” por llamarlos de alguna forma. En este caso vamos a tratar un escenario que hemos llamado optimizaciones «boomerang» donde podemos encontrarnos que tras optimizaciones (indexación, parametrización, reescritura de queries, etc.) el consumo global de CPU disminuye y, contrariamente a lo previsto, los tiempos de ejecución de ciertos procesos aumentan.

En general una recomendación que siempre hacemos para obtener el máximo rendimiento de un SQL Server es configurar las opciones de energía, tanto a nivel de BIOS como de OS, en modo “alto rendimiento”. Esto sacrifica consumo a cambio de obtener frecuencias más elevadas y menores latencias en el acceso a la CPU, evitando pasar los cores por estados “latentes”, el uso de core parking, etc.

En general el objetivo de esta configuración es obtener un “extra” de rendimiento tanto a baja carga como en niveles de carga más elevados. De hecho, si el TDP está ya justo o la refrigeración no es la ideal la diferencia de rendimiento en cargas de CPU elevadas puede ser nula entre ambas configuraciones de energía ya que ocurrirá el llamado «thermal throttling» de forma automática.

Sin embargo, tenemos un escenario especialmente “sangrante” que ocurre en cargas muy bajas. Para mostraros este problema vamos a crear un procedimiento que simula un procesando fila a fila sin paralelismo. Este podría ser un caso de un cursor a nivel de servidor o cliente donde iteramos fila a fila sobre operaciones muy sencillitas (lookups de pocas filas, etc.)

Comenzaremos creando una base de datos con una tabla con 10 millones de registros la cual indexaremos para que los seek sean rápidos:

create database poweroptimization
go
use poweroptimization
go
select top 10000000 row_number() over (order by (select 1)) i into numbers from sys.objects s1, sys.objects s2,sys.objects s3, sys.objects s4, sys.objects s5
go
create index ix_i on numbers (i)
go

A continuación, lanzaremos el siguiente bucle que realiza 1 millón de seeks buscando un valor específico en la tabla:

-- Optimized query, index seek
set nocount on 
go
declare @i int
declare @loop int =1
while (@loop < 1000000)
begin
  select @i=i from numbers where i=@loop
  set @loop=@loop+1
end

El plan de ejecución es extremadamente sencillo, con el 100% del coste de cada iteración asociado a la operación seek:

Vamos a ejecutar este script sobre un servidor que tiene carga de cpu “global” antes de lanzar la carga entre el 0% y el 100%. Al contar con 24 cores, cada core al 100% de uso representa aproximadamente un 4.16% de CPU sobre el total. Todo esto sin tener en cuenta el impacto del uso de hyperthreading que puede hacer que, en cargas especialmente difíciles para el hyperthreading, a poco más del 60% de CPU lógica ya estaremos “al 100%” de uso de recursos físicos y sufriremos ya ralentizaciones muy notorias.

Si nos fijamos en el gráfico todo parece “normal” desde una carga del 15% hasta el 100%. Básicamente el modo high performance tiene un rendimiento mejor que el modo balanceado a lo largo de toda la curva de carga por un margen razonable.

Sin embargo, si nos fijamos en el comportamiento a cargas bajas (entre el 0% y el 15%) en el modo balanceado vemos que tenemos una diferencia enorme en el tiempo de ejecución. La carga en modo balanced tiene duraciones que son peores cuanto menor es la carga, llegando a 22 segundos con un 0% de carga, que es la misma duración que tenemos al 93% de carga lo cual parece algo bastante “ilógico”.

Si comparamos la diferencia de tiempos con el modo de alto rendimiento la situación a muy baja carga es preocupante, ya que pasamos de 8 segundos a 22 segundos (2.75 veces más duración). Nos encontraremos con este tipo de situaciones «efecto boomerang» si hemos llegado a muy bajos consumos de CPU debido a las mejoras realizadas y tenemos configurado un modo de ahorro energético. La consecuencia será que los usuarios experimentarán peores tiempos de respuesta en ciertos procesos como «consecuencia indirecta» de la optimización realizada.

Si añadimos la frecuencia de trabajo del procesador a esta gráfica podremos comprender mejor qué es lo que ocurre en realidad:

Podemos ver que en modo high performance la frecuencia del procesador (E5 2630 v2) se mantiene en los 2900 MHz (por encima de los 2600 MHz de frecuencia base) durante la mayor parte de la curva de carga y es incluso superior, llegando a 3100 MHz (el máximo en modo turbo) cuando la carga es baja.

Sin embargo, en el modo balanceado ocurre lo contrario y cuando la carga es muy baja se reduce muy notablemente la frecuencia del procesador para ahorrar energía, hasta unos 1500 MHz, mientras que a cargas mayores se mantiene como mucho en los 2600 MHz de frecuencia base. Esta bajada de frecuencia a baja carga hace que los tiempos de ejecución de nuestra carga empeoren notablemente al tratarse de un loop/cursor monohilo. En casos reales de clientes hemos visto que la frecuencia en baja carga puede incluso bajar de 1 GHz, a 800 MHz, exacerbando este efecto aún más que en el ejemplo mostrado.

También debemos tener en cuenta que el rendimiento se degrada con el aumento de la carga de CPU, siendo la degradación más temprana en el caso del modo balanceado. En modo balanceado a partir del 33% ya empieza a ir aumentando la duración mientras que en el caso del alto rendimiento no aumenta hasta que superamos el umbral del 60%.

Por tanto, debemos asumir que el rendimiento va a empeorar con el aumento de utilización de la CPU (este es un fallo típico al esperar escalabilidad lineal durante los stress tests). Si queremos tener un sistema que responda de forma “ágil” y sin variaciones sustanciales en sus tiempos de respuesta sería conveniente no mantener niveles de utilización por encima del 50%, manteniéndonos idealmente por debajo del 30% si es posible.

En conclusión, para sacar el máximo rendimiento y evitar situaciones “extrañas” como la que hemos comentado recomendamos que se utilicen los modos de alto rendimiento en la configuración de la BIOS o, si se deja al criterio del operativo, en la configuración del sistema operativo. También debemos tener en cuenta que la multitarea no deja de ser un “espejismo” creado por el sistema operativo, por lo que, si aumentamos la concurrencia, el número de threads, etc. y con ellos el uso de CPU, esos cambios de contexto no son gratis, por lo que tendremos una degradación en la ejecución mayor cuanto mayor sea la carga de CPU.

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)