Sí, mi vieja computadora portátil es varias veces más poderosa que su servidor de producción.

Estas son exactamente las quejas que escuché de nuestros desarrolladores. Lo más interesante es que esto resultó ser cierto, dando lugar a una larga investigación. Hablaremos de servidores SQL que se ejecutan en VMware.

Sí, mi vieja computadora portátil es varias veces más poderosa que su servidor de producción.

En realidad, es fácil asegurarse de que el servidor de producción esté irremediablemente detrás de la computadora portátil. Ejecute (no en tempdb ni en una base de datos con Durabilidad retardada habilitada) el código:

set nocount on
create table _t (v varchar(100))
declare @n int=300000
while @n>0 begin 
  insert into _t select 'What a slowpoke!'
  delete from _t
  set @n=@n-1
  end
GO
drop table _t

En mi escritorio, tarda 5 segundos y en el servidor de producción, 28 segundos. Porque SQL debe esperar el final físico de la entrada del registro de transacciones, y aquí estamos realizando transacciones muy cortas. En términos generales, condujimos un camión grande y potente hacia el tráfico de la ciudad y vimos cómo los repartidores de pizzas en scooters lo adelantaban apresuradamente; el rendimiento no es importante aquí, solo la latencia es importante. Y ningún almacenamiento en red, por muchos ceros que tenga en su precio, puede superar al SSD local en términos de latencia.

(En los comentarios resultó que mentí: había retrasado la durabilidad en ambos lugares. Sin demora la durabilidad resulta:
Computadora de escritorio: 39 segundos, 15 0.065 tr/s, XNUMX ms/io ida y vuelta
PROD - 360 segundos, 1600 tr/s, 0.6 ms
Debería haber notado que era demasiado rápido)

Sin embargo, en este caso estamos tratando con ceros triviales de la función zeta de Riemann con un ejemplo trivial. En el ejemplo que me trajeron los desarrolladores, fue diferente. Me convencí de que tenían razón y comencé a eliminar del ejemplo todos los detalles específicos relacionados con la lógica empresarial. En algún momento, me di cuenta de que podía desechar completamente su código y escribir el mío propio, lo que demuestra el mismo problema: en producción se ejecuta entre 3 y 4 veces más lento:

create function dbo.isPrime (@n bigint)
returns int
as
  begin
  if @n = 1 return 0
  if @n = 2 return 1
  if @n = 3 return 1
  if @n % 2 = 0 return 0
  declare @sq int
  set @sq = sqrt(@n)+1 -- check odds up to sqrt
  declare @dv int = 1
  while @dv < @sq 
    begin
	set @dv=@dv+2
	if @n % @dv = 0 return 0
	end
  return 1
  end
GO
declare @dt datetime set @dt=getdate()
select dbo.isPrime(1000000000000037)
select datediff(ms,@dt,getdate()) as ms
GO

Si todo está bien, comprobar la primalidad de un número tardará entre 6, 7 y 8 segundos. Esto sucedió en varios servidores. Pero en algunos, la comprobación tardó entre 25 y 40 segundos. Curiosamente, no había servidores donde la ejecución tomaría, digamos, 14 segundos; el código funcionaba muy rápido o muy lentamente, es decir, el problema era, digamos, en blanco y negro.

¿Qué he hecho? Métricas de VMware utilizadas. Todo estaba bien allí: había una gran cantidad de recursos, tiempo de preparación = 0, había suficiente de todo, durante la prueba en servidores rápidos y lentos CPU = 100 en una vCPU. Hice una prueba para calcular el número Pi; la prueba mostró los mismos resultados en cualquier servidor. El olor a magia negra se hizo cada vez más fuerte.

Una vez que llegué a la granja DEV, comencé a jugar con los servidores. Resultó que vMotion de un host a otro puede "curar" un servidor, pero también puede convertir un servidor "rápido" en uno "lento". Parece que esto es todo: algunos hosts tienen un problema... pero... no. Alguna máquina virtual era lenta en el host, digamos A, pero trabajaba rápidamente en el host B. ¡Y otra máquina virtual, por el contrario, trabajaba rápidamente en A y se ralentizaba en B! ¡Tanto las máquinas “rápidas” como las “lentas” a menudo giraban en el host!

A partir de ese momento, hubo un claro olor a azufre en el aire. Después de todo, el problema no se podía atribuir a la máquina virtual (parches de Windows, por ejemplo); después de todo, se volvió "rápido" con vMotion. Pero el problema tampoco se puede atribuir al host; después de todo, podría haber máquinas tanto "rápidas" como "lentas". Además, esto no estaba relacionado con la carga: logré tener una máquina "lenta" en el host, donde no había nada más que eso.

Desesperado, inicié Process Explorer desde Sysinternals y miré la pila SQL. En máquinas lentas, la frase me llamó inmediatamente la atención:

ntoskrnl.exe!KeSynchronizeExecution+0x5bf6
ntoskrnl.exe!KeWaitForMultipleObjects+0x109d
ntoskrnl.exe!KeWaitForMultipleObjects+0xb3f
ntoskrnl.exe!KeWaitForSingleObject+0x377
ntoskrnl.exe!KeQuerySystemTimePrecise+0x881 < — !!!
ntoskrnl.exe!ObDereferenceObjectDeferDelete+0x28a
ntoskrnl.exe!KeSynchronizeExecution+0x2de2
sqllang.dll!CDiagThreadSafe::PxlvlReplace+0x1a20
... saltado
sqldk.dll!SystemThread::MakeMiniSOSThread+0xa54
KERNEL32.DLL!BaseThreadInitThunk+0x14
ntdll.dll! RtlUserThreadStart + 0x21

Esto ya era algo. El programa fue escrito:

    class Program
    {
        [DllImport("kernel32.dll")]
        static extern void GetSystemTimePreciseAsFileTime(out FILE_TIME lpSystemTimeAsFileTime);

        [StructLayout(LayoutKind.Sequential)]
        struct FILE_TIME
        {
            public int ftTimeLow;
            public int ftTimeHigh;
        }

        static void Main(string[] args)
        {
            for (int i = 0; i < 16; i++)
            {
                int counter = 0;

                var stopwatch = Stopwatch.StartNew();

                while (stopwatch.ElapsedMilliseconds < 1000)
                {
                    GetSystemTimePreciseAsFileTime(out var fileTime);
                    counter++;
                }

                if (i > 0)
                {
                    Console.WriteLine("{0}", counter);
                }
            }
        }
    }

Este programa demostró una desaceleración aún más pronunciada: en máquinas "rápidas" muestra entre 16 y 18 millones de ciclos por segundo, mientras que en máquinas lentas muestra un millón y medio, o incluso 700 mil. Es decir, la diferencia es de 10 a 20 veces (!!!). Esto ya era una pequeña victoria: en cualquier caso, no había amenaza de quedar atrapado entre el soporte de Microsoft y VMware de modo que se atacaran mutuamente.

Luego el progreso se detuvo: vacaciones, asuntos importantes, histeria viral y un fuerte aumento de la carga de trabajo. A menudo mencioné el problema mágico a mis colegas, pero a veces parecía que ni siquiera siempre me creían: la afirmación de que VMware ralentiza el código entre 10 y 20 veces era demasiado monstruosa.

Intenté descubrir por mí mismo qué era lo que me frenaba. A veces me parecía que había encontrado una solución: encender y apagar los enchufes en caliente, cambiar la cantidad de memoria o la cantidad de procesadores a menudo convertían la máquina en una "rápida". Pero no para siempre. Pero lo que resultó ser cierto es que basta con salir y tocar el volante, es decir, cambiar cualquier parámetro de la máquina virtual

Finalmente, mis colegas estadounidenses encontraron de repente la causa fundamental.

Sí, mi vieja computadora portátil es varias veces más poderosa que su servidor de producción.

¡Los anfitriones diferían en frecuencia!

  • Como regla general, esto no es gran cosa. Pero: al pasar de un host "nativo" a un host con una frecuencia "diferente", VMware debe ajustar el resultado de GetTimePrecise.
  • Como regla general, esto no es un problema, a menos que haya una aplicación que solicite la hora exacta millones de veces por segundo, como el servidor SQL.
  • Pero esto no da miedo, ya que el servidor SQL no siempre hace esto (ver Conclusión)

Pero hay casos en los que este rastrillo golpea con fuerza. Y, sin embargo, sí, al tocar la rueda (cambiando algo en la configuración de la VM) obligué a VMware a "recalcular" la configuración, y la frecuencia del host actual se convirtió en la frecuencia "nativa" de la máquina.

Solución

www.vmware.com/files/pdf/techpaper/Timekeeping-In-VirtualMachines.pdf

Cuando deshabilita la virtualización del TSC, leer el TSC desde la máquina virtual devuelve el valor de TSC de la máquina física y escribir el TSC desde la máquina virtual no tiene ningún efecto. Migrar la máquina virtual a otro host, reanudarla desde el estado suspendido o volver a una instantánea hace que el TSC salte de forma discontinua. Algunos sistemas operativos invitados no arrancan o presentan otros problemas de cronometraje cuando la virtualización TSC está desactivada. En el pasado, a veces se recomendaba esta característica para mejorar el rendimiento de las aplicaciones que leen el TSC con frecuencia., pero el rendimiento del TSC virtual se ha mejorado sustancialmente en los productos actuales. También se ha recomendado el uso de la función al realizar mediciones que requieren una fuente precisa de tiempo real en la máquina virtual.

En resumen, necesitas agregar el parámetro.

monitor_control.virtual_rdtsc = FALSO

Conclusión

Probablemente tenga una pregunta: ¿por qué SQL llama a GetTimePrecise con tanta frecuencia?

No tengo el código fuente del servidor SQL, pero la lógica dice esto. SQL es casi un sistema operativo con concurrencia cooperativa, donde cada hilo debe "ceder" de vez en cuando. ¿Cuál es el mejor lugar para hacer esto? Donde hay una espera natural: bloqueo o IO. Bien, pero ¿qué pasa si estamos girando bucles computacionales? Entonces el lugar obvio y casi único es en el intérprete (esto no es realmente un intérprete), después de ejecutar la siguiente declaración.

Generalmente, el servidor SQL no se utiliza para operaciones informáticas puras y esto no es un problema. Pero los bucles que funcionan con todo tipo de tablas temporales (que se almacenan inmediatamente en caché) convierten el código en una secuencia de declaraciones ejecutadas muy rápidamente.

Por cierto, si envuelves la función en COMPILACIÓN NATURAL, deja de pedir tiempo y su velocidad aumenta 10 veces. ¿Qué pasa con la multitarea cooperativa? Pero para el código compilado de forma nativa tuvimos que realizar MULTITAREA PREVENTIVA en SQL.

Fuente: habr.com

Añadir un comentario