En nuestro día a día nos encontramos muy habitualmente cuestiones respecto a la escalabilidad de cargas SQL Server. Más concretamente a cómo se espera que la carga se comporte ante un aumento de carga y/o ante una mejora en el hardware (escalado vertical). No son respuestas sencillas ya que existen muchos factores a considerar y en este artículo me gustaría comentar algunos de ellos para que nos ayuden a no caer (o reafirmarnos) en ciertas ideas preconcebidas.

Uno de los primeros conceptos que debemos tener claro es que, salvo honrosas excepciones, no existe una escalabilidad lineal de cargas SQL Server respecto al hardware empleado. La idea que nos encontramos preconcebida en muchos clientes es que si tenemos un servidor con 16 cores, 32 GB de RAM y 10 discos dedicados y pasamos a un servidor con 32 cores, 64 GB de ram y 20 discos dedicados podemos duplicar la capacidad de proceso (throughput) con un tiempo de respuesta (latencia) muy similar a la actual.

Desafortunadamente la realidad es muy diferente por muchos motivos. Podríamos extendernos muchísimo si revisamos todos factores que hacen que esto no vaya a ser así pero vamos a centrarnos en algunos de los más comunes que nos encontramos desde el punto de vista de CPU. Si nos fijamos en las características de los procesadores podemos ver que en general existe una relación inversa entre el número de cores que disponen y las frecuencias de trabajo. La razón es que tanto aumentar el número de cores como la frecuencia de trabajo aumenta la cantidad de calor que tenemos que disipar. La mayoría de servidores trabajan en un entorno estable y controlado a nivel de temperatura pero también muy habitualmente soportan cargas 24×7 lo cual requiere niveles adicionales de tolerancia para evitar que se produzcan fallos hardware. Soluciones de refrigeración muy complejas o técnicas de overclocking agresivas (típicas de PCs de entusiastas) no son generalmente aplicables en estos entornos. Cierto es que existen innovaciones a nivel de refrigeración que podrían dar el salto a estos entornos pero aún tienen que probar su fiabilidad a largo plazo.

Respecto a la frecuencia de trabajo por core (en realidad la frecuencia multiplicada por el número de operaciones por ciclo) nosotros solemos decir que no es «sustituible». Es decir, si queremos mantener la latencia en el tiempo de respuesta y aumentar la capacidad no podemos hacerlo escalando el número de cores a cambio de sacrificar frecuencia de trabajo. Hace unos años publiqué en este blog un artículo más detallado mostrando diferencias de rendimiento para distintas operaciones modificando con el número de cores y frecuencias de trabajo de un mismo procesador (Optimización vs mejora en hardware). Únicamente mostraré aquí el impacto que genera en una consulta no paralela el aumento de la frecuencia de un core, desde 1.5 GHz hasta 4.5 GHz:

duracionyfrecuencia

En esta consulta cuyo coste mayoritario inicialmente es la CPU podemos ver cómo aumentar la frecuencia produce una reducción del total de milisegundos de CPU consumidos y de la duración total. También podemos ver como a medida que aumenta la frecuencia aumenta la diferencia entre tiempo de CPU y duración, indicando que existen otros factores que están añadiendo tiempo fijo a la consulta o que están comenzando a actuar de cuello de botella (por ejemplo la transferencia por red de los datos):

Frecuencia CPU CPU (ms) Duración (ms) Reducción CPU % Reducción Duración %
1500 10718 10953
2500 7082 7286 33.92% 33.48%
3500 5055 5177 52.84% 52.73%
4500 3791 4173 64.63% 61.90%

Además del factor de la frecuencia de trabajo, hay otros factores a tener en cuenta, como la cantidad de caché L2/L3 por core (que puede ser menor en procesadores con más cores) o el impacto que tiene compartir elementos como el controlador de memoria por un número mayor de cores. También hay que tener en cuenta la generación del procesador, ya que, por poner un ejemplo, los aumentos en rendimiento de un procesador E5 de Intel V1, respecto a un E5 v2 equivalente o desde un E5 v2 a un  E5 v3 similar pueden ser de hasta un 30%.

Respecto a la cantidad de memoria L3 hay un factor importante que nos afecta que es el «working set» en el que habitualmente se muevan nuestras consultas. Cuando mayor sea éste o menor sea nuestra caché L3 más fácil es que tengamos que recurrir a accesos a «lenta» memoria RAM. Sé que alguien puede pensar que acceder a la memoria RAM no debería ser costoso, y no lo es si lo comparamos con acceder a disco, pero aun así es muchísimo más costoso que acceder a la caché L3. Por ejemplo las latencias típicas de acceso a los distintos niveles de caché en un procesador Intel moderno es de menos de 2ns para la L1, alrededor de 4ns para la L2 y unos 6ns para la L3. Un acceso a memoria, contando que ello implica un fallo al buscar en el resto de cachés, puede significar alrededor de 60ns, es decir, resulta 10 veces más lento que el acceso a la caché más lenta, la L3.

Y en qué repercute esto a nuestra carga SQL Server os preguntaréis. Si medimos los tiempos por ejemplo de una consulta que realiza cálculos sobre un conjunto de filas vemos que el coste de procesado por fila tiene un punto «óptimo» en lo que respecta a coste por fila procesada. Es aquella zona en la cual podemos encontrar la información necesaria dentro de las cachés del procesador y donde el coste fijo de arrancar la ejecución de la consulta se divide entre un número suficiente de filas a procesar:

CPU por fila

Cuando desplegamos un nuevo servidor es recomendable realizar algunas pruebas de rendimiento sintéticas. Normalmente estas pruebas comprueban el funcionamiento de la memoria, la CPU, el disco, etc. de forma aislada. Es importante tener referencias de los valores que podríamos esperar para poder atacar posibles problemas antes de entrar en producción. Por ejemplo es frecuente que configuraciones a nivel de servidor/BIOS hagan que el rendimiento sea subóptimo. También es habitual encontrarnos con problemas de drivers/firmwares no actualizados que generan rendimientos pobres.

Una vez tenemos claro que el hardware está funcionando correctamente también es recomendable realizar algunas pruebas ya directamente sobre la instancia de SQL Server. Idealmente realizaríamos una prueba de carga basada en la carga real que va a soportar el servidor. Desgraciadamente muchas veces esto no es posible, por no existir los recursos apropiados para realizarla, y nos tenemos que conformar con una prueba más genérica. Por ejemplo podemos utilizar para lanzar estas pruebas genéricas HammerDB. Lanzando pruebas de estilo TPC-C podemos encontrarnos patrones curiosos de carga como el siguiente:

hammerdb1

task_manager_1

tpc1

La razón de este patrón es que debido al algoritmo de asignación de conexiones a schedulers en base al factor de carga si generamos un conjunto de conexiones de forma simultánea podemos encontrarnos con situaciones donde el reparto no es lo uniforme que debería ser. En el caso anterior toda la carga la procesa un solo nodo NUMA y debido a la utilización de connection pooling (aunque se abran y cierren a nivel de aplicación) las conexiones seguirán asignadas a la misma conexión física a SQL Server. En este caso concreto una solución puede ser añadir un retardo en la creación inicial de las conexiones de forma que el factor de carga se vaya actualizando y se vayan asignando conexiones a schedulers de los dos nodos NUMA.  Hay que tener en cuenta que este algoritmo se ha modificado en SQL Server 2012+ EE por lo que recomiendo leer el instructivo artículo de Bob Dorr (SQL Server 2012 Database Engine Task Scheduling).

En el caso anterior, pese a utilizar un solo nodo NUMA, obtenemos más de 908K tpm. Al solucionar este problema y poder utilizar el doble de CPU esperaríamos intuitivamente poder doblar esta capacidad pero la realidad es bastante diferente y nos quedamos en 1416K tpm, menos de un 50% de incremento:

task_manager_2

tpc2

Por último también podemos experimentar con máscaras de procesador. Por ejemplo con una máscara que nos limite a la mitad los cores para de esta forma utilizar solamente un core lógico de los dos asociados a cada core físico:

hammerdb3

task_manager_3

 

tpc3

En este caso, con la mitad de cores lógicos en uso pero con los dos nodos NUMA obtenemos 1237K tpm en la prueba. Esta cifra es mayor que la que obtenemos utilizando un nodo NUMA únicamente pero menor que la que obtenemos utilizando todos los cores lógicos y físicos. La lección que aprendemos de estas pruebas es que el uso medio de CPU no es un indicativo estable del rendimiento y que factores como el reparto entre nodos NUMA o entre cores lógicos/fisicos tiene un impacto importante en el throughput. También podemos concluir que, para esta prueba en concreto, la utilización de hyperthreading aporta un mayor rendimiento respecto a no usarlo pero no debemos confiarnos ya que el aumento no es proporcional al consumo de CPU medio.

En conclusión, nuestra recomendación general a la hora de elegir un procesador para SQL Server se basa en intentar equilibrar estos factores:

  • Que tenga la mayor frecuencia de trabajo posible, primando la frecuencia base respecto a la turbo. Aunque el hyperthreading es «gratis» desde el punto de vista de licenciamiento debemos ser cautos con las expectativas de mejora que nos aporta respecto a utilizar solo los cores físicos.
  • Que el sistema cuente con el menor número de procesadores posible que sean suficientes para nuestra carga. No olvidemos que sistemas 2S actuales tienen capacidades superiores a sistemas 4S de hace unos pocos años.
  • Que cuente con la mayor cantidad de cache L2 por core y el mayor ratio de L3 total posible por core. Debemos ser muy cuidadosos y huir de «procesadores en oferta» ya que suelen ser aquellos con poca caché y baja frecuencia.
  • Que encaje dentro del presupuesto existente. El procesador es un componente que rara vez se actualiza en un servidor una vez comprado, por lo que es preferible intentar ahorrar en otros componentes que puedan ampliarse más fácilmente en un futuro, como la memoria RAM. También el modelo de licenciamiento de SQL Server 2012+, basado en core físico en vez de en procesador, debemos tenerlo en cuenta para no excedernos en el número de cores a licenciar.

 

 

Rubén Garrigós