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

Estas son las afirmaciones que escuché de nuestros desarrolladores. Lo más interesante es que esto resultó ser cierto, dando lugar a una larga investigación. Se tratará de servidores SQL, que giran en nuestro VMware.



En realidad, garantizar que el servidor de producción esté irremediablemente detrás del portátil es fácil. Ejecute (no en tempdb y no en una base con la 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, se ejecuta durante 5 segundos y en el servidor de producción, 28 segundos. Debido a que SQL debe esperar el final físico de la entrada del registro de transacciones, y aquí estamos haciendo transacciones muy cortas. Hablando en términos generales, condujimos un camión grande y poderoso al tráfico de la ciudad, y vemos cómo es famoso por los repartidores de pizza en scooters: el rendimiento no es importante aquí, solo la latencia es importante. Y ni un solo almacenamiento de red, sin importar cuántos ceros tenga en su precio, podrá ganar latencia frente al SSD local.

(en los comentarios resultó que mentí - tuve una durabilidad retrasada en ambos lugares. Sin durabilidad retrasada resulta:
Escritorio - 39 segundos, 15K tr / seg, 0.065ms / io ida y vuelta
PROD - 360 segundos, 1600 tr / seg, 0.6ms
I debería haber prestado atención demasiado pronto)


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

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 para usted, la verificación de la simplicidad del número se llevará a cabo durante 6-7-8 segundos. Así fue en varios servidores. Pero en algunos, el cheque tomó 25-40 segundos. Curiosamente, no había servidores en los que la ejecución llevaría, por ejemplo, 14 segundos: el código funcionó muy rápido o muy lento, es decir, el problema era, digamos, blanco y negro.

¿Qué he hecho? Útil en las métricas de VMware. Todo estaba bien allí: había muchos recursos, Tiempo de preparación = 0, todo fue suficiente, durante la prueba tanto en servidores rápidos como lentos CPU = 100 en una vCPU. Hice una prueba para calcular la cantidad de Pi; la prueba mostró los mismos resultados en cualquier servidor. Olía a magia negra cada vez más.

Habiendo salido a la granja DEV, comencé a jugar como servidores. Resultó que vMotion de host a host puede "curar" el servidor, pero puede y viceversa, convertir un servidor "rápido" en uno "lento". Parece así: algunos hosts tienen un problema ... pero ... no. Algunas máquinas virtuales disminuyeron la velocidad en el host, por ejemplo, A, pero funcionaron rápidamente en el host B. ¡Y la otra máquina virtual, por el contrario, funcionó rápidamente en A y redujo la velocidad en B! ¡Los autos "rápidos" y "lentos" a menudo giraban sobre el anfitrión!

A partir de ese momento, el aire olía claramente a azufre. Después de todo, el problema no puede atribuirse a ninguna máquina virtual (parches de Windows, por ejemplo), porque se convirtió en una "rápida" con vMotion. Pero el problema tampoco podría atribuirse al host, ya que podría tener máquinas "rápidas" y "lentas". Tampoco estaba conectado con la carga: logré obtener una máquina "lenta" en el host, donde no había nada más que eso.

Por desesperación, inicié Process Explorer desde Sysinternals y miré la pila SQL. En máquinas lentas, la línea me llamó la atención inmediatamente:

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


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 mostró una desaceleración aún más brillante: en máquinas "rápidas" muestra 16-18 millones de ciclos por segundo, mientras que en máquinas lentas es un millón y medio, o incluso 700 mil. Es decir, la diferencia es de 10-20 veces (!!!). Esto ya era una pequeña victoria: en cualquier caso, no había ninguna amenaza de quedarse atascado entre el soporte de Microsoft y VMware para que se transfirieran flechas entre sí.

Además, el progreso se detuvo: vacaciones, asuntos importantes, histeria viral y un fuerte aumento de la carga. Muchas veces mencioné un problema mágico a mis colegas, pero a veces parecía que no siempre confiaban en mí: la afirmación de que VMware ralentizó el código 10-20 veces era demasiado monstruosa.

Traté de desenterrar lo que estaba disminuyendo. A veces me pareció que había encontrado una solución: encender y apagar los enchufes en caliente, cambiar el tamaño de la memoria o la cantidad de procesadores que a menudo convertían la máquina en una "rápida". Pero no para siempre. Pero lo que resultó ser cierto es que es suficiente salir y tocar el volante, es decir, cambiar cualquier parámetro de la máquina virtual.

Finalmente, mis colegas estadounidenses de repente encontraron la causa raíz.



¡Los hosts diferían en frecuencia!
  • Esto generalmente no da miedo. Pero: al pasar de un host 'nativo' a un host con una frecuencia 'diferente', VMware debe ajustar el resultado de GetTimePrecise.
  • Como regla, esto no da miedo, a menos que haya una aplicación que solicite el tiempo exacto millones de veces por segundo, como un servidor SQL.
  • Pero esto no da miedo, ya que el servidor SQL no siempre hace esto (vea la Conclusión)

Pero hay casos en que este rastrillo golpea dolorosamente. Y, sin embargo, sí, tocando la rueda (cambiando algo en la configuración de VM) forcé a VMware a 'contar' la configuración, y la frecuencia del host actual se convirtió en la frecuencia 'nativa' de la máquina.

Decisión


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

When you disable virtualization of the TSC, reading the TSC from within the virtual machine returns the physical machine’s TSC value, and writing the TSC from within the virtual machine has no effect. Migrating the virtual machine to another host, resuming it from suspended state, or reverting to a snapshot causes the TSC to jump discontinuously. Some guest operating systems fail to boot, or exhibit other timekeeping problems, when TSC virtualization is disabled. In the past, this feature has sometimes been recommended to improve performance of applications that read the TSC frequently, but performance of the virtual TSC has been improved substantially in current products. The feature has also been recommended for use when performing measurements that require a precise source of real time in the virtual machine.

En resumen, agregue el parámetro

monitor_control.virtual_rdtsc = FALSE

Conclusión


Probablemente tengas una pregunta: ¿para qué llama SQL a GetTimePrecise con tanta frecuencia?

No tengo la fuente del servidor SQL, pero la lógica dice esto. SQL es casi un sistema operativo con concurrencia cooperativa, donde cada hilo tiene que ceder de vez en cuando. ¿Dónde es mejor hacerlo? Donde hay una expectativa natural: bloqueo o IO. Bueno, ¿y si giramos los ciclos computacionales? Entonces, lo obvio y casi el único lugar es en el intérprete (esto no es del todo un intérprete), después de la ejecución del siguiente operador.

Como regla general, el servidor SQL no se usa para clavar cálculos limpios y esto no es un problema. Pero los ciclos de trabajo con todo tipo de tabletas temporales (que se almacenan en caché de inmediato) convierten el código en una secuencia de declaraciones ejecutadas muy rápidamente.

Por cierto, si ajusta la función en NATIVAMENTE COMPILADO, deja de pedir tiempo y su velocidad aumenta en un factor de 10. ¿Pero qué pasa con la multitarea cooperativa? Pero para el código compilado de forma nativa, tuve que hacer MULTITASKING PREEMPTIVE en SQL.

All Articles