Sim, meu laptop antigo é várias vezes mais poderoso que seu servidor de produção.

Estas são exatamente as reclamações que ouvi de nossos desenvolvedores. O mais interessante é que isso se revelou verdade, dando origem a uma longa investigação. Falaremos sobre servidores SQL executados em VMware.

Sim, meu laptop antigo é várias vezes mais poderoso que seu servidor de produção.

Na verdade, é fácil garantir que o servidor de produção esteja irremediavelmente atrás do laptop. Execute (não em tempdb e nem em um banco de dados com Durabilidade Atrasada habilitada) o 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

No meu desktop leva 5 segundos e no servidor de produção leva 28 segundos. Porque o SQL deve aguardar o final físico da entrada do log de transações e estamos fazendo transações muito curtas aqui. Grosso modo, dirigimos um caminhão grande e potente no trânsito da cidade e observamos como ele foi ultrapassado por entregadores de pizza em scooters - o rendimento não é importante aqui, apenas a latência é importante. E nenhum armazenamento em rede, não importa quantos zeros exista em seu preço, pode superar o SSD local em termos de latência.

(nos comentários descobri que eu menti - atrasei a durabilidade em ambos os lugares. Sem adiar a durabilidade acontece:
Desktop – 39 segundos, 15 mil tr/s, 0.065 ms/io ida e volta
PROD - 360 segundos, 1600 tr/seg, 0.6 ms
Eu deveria ter notado que foi muito rápido)

Porém, neste caso estamos lidando com zeros triviais da função zeta de Riemann com um exemplo trivial. No exemplo que os desenvolvedores me trouxeram foi diferente. Fiquei convencido de que eles estavam certos e comecei a retirar do exemplo todas as suas especificidades relacionadas à lógica de negócios. Em algum momento percebi que poderia jogar fora completamente o código deles e escrever o meu próprio - o que demonstra o mesmo problema - na produção ele roda 3-4 vezes mais devagar:

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

Se tudo estiver bem, a verificação da primalidade de um número levará de 6 a 7 a 8 segundos. Isso aconteceu em vários servidores. Mas em alguns, a verificação demorou de 25 a 40 segundos. Curiosamente, não existiam servidores onde a execução demorasse, digamos, 14 segundos - o código funcionava muito rápido ou muito devagar, ou seja, o problema era, digamos, preto e branco.

O que eu fiz? Métricas VMware usadas. Estava tudo bem lá - havia abundância de recursos, Tempo de prontidão = 0, havia o suficiente de tudo, durante o teste em servidores rápidos e lentos CPU = 100 em uma vCPU. Fiz um teste para calcular o número Pi - o teste mostrou os mesmos resultados em qualquer servidor. O cheiro da magia negra tornou-se cada vez mais forte.

Assim que cheguei ao farm DEV, comecei a brincar com os servidores. Descobriu-se que o vMotion de host para host pode “curar” um servidor, mas também pode transformar um servidor “rápido” em um “lento”. Parece que é isso - alguns hosts têm um problema... mas... não. Alguma máquina virtual ficou lenta no host, digamos A, mas funcionou rapidamente no host B. E outra máquina virtual, ao contrário, funcionou rapidamente em A e ficou lenta em B! Máquinas “rápidas” e “lentas” frequentemente giravam no host!

A partir daquele momento, houve um cheiro distinto de enxofre no ar. Afinal, o problema não poderia ser atribuído à máquina virtual (patches do Windows, por exemplo) – afinal, ficou “rápido” com o vMotion. Mas o problema também não poderia ser atribuído ao host – afinal, ele poderia ter máquinas tanto “rápidas” quanto “lentas”. Além disso, isso não estava relacionado à carga - consegui colocar uma máquina “lenta” no host, onde não havia nada além dela.

Desesperado, iniciei o Process Explorer da Sysinternals e observei a pilha SQL. Em máquinas lentas, a frase imediatamente chamou minha atenção:

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
... pulado
sqldk.dll!SystemThread::MakeMiniSOSThread+0xa54
KERNEL32.DLL!BaseThreadInitThunk+0x14
ntdll.dll! RtlUserThreadStart + 0x21

Isso já era alguma coisa. O programa foi 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 demonstrou uma desaceleração ainda mais pronunciada - em máquinas “rápidas” mostra 16-18 milhões de ciclos por segundo, enquanto em máquinas lentas mostra um milhão e meio, ou mesmo 700 mil. Ou seja, a diferença é de 10 a 20 vezes (!!!). Esta já foi uma pequena vitória: em qualquer caso, não havia ameaça de ficar preso entre o suporte da Microsoft e da VMware para que eles atacassem um ao outro.

Então o progresso parou - férias, assuntos importantes, histeria viral e um aumento acentuado na carga de trabalho. Muitas vezes mencionei o problema mágico aos meus colegas, mas às vezes parecia que eles nem sempre acreditavam em mim - a afirmação de que o VMware retarda o código em 10 a 20 vezes era muito monstruosa.

Tentei descobrir sozinho o que estava me atrasando. Às vezes parecia-me que tinha encontrado uma solução - ligar e desligar Hot plugs, alterar a quantidade de memória ou o número de processadores muitas vezes transformavam a máquina em uma máquina “rápida”. Mas não para sempre. Mas o que se revelou verdade é que basta sair e bater no volante - ou seja, mudar qualquer parâmetro da máquina virtual

Finalmente, os meus colegas americanos encontraram subitamente a causa raiz.

Sim, meu laptop antigo é várias vezes mais poderoso que seu servidor de produção.

Os anfitriões diferiam em frequência!

  • Via de regra, isso não é grande coisa. Mas: ao passar de um host 'nativo' para um host com frequência 'diferente', o VMware deve ajustar o resultado do GetTimePrecise.
  • Via de regra, isso não é um problema, a menos que exista uma aplicação que solicite a hora exata milhões de vezes por segundo, como o SQL Server.
  • Mas isso não é assustador, pois o SQL Server nem sempre faz isso (veja Conclusão)

Mas há casos em que esse rake bate forte. E ainda assim, sim, tocando na roda (alterando algo nas configurações da VM) forcei o VMware a 'recalcular' a configuração, e a frequência do host atual tornou-se a frequência 'nativa' da máquina.

Solução

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

Quando você desativa a virtualização do TSC, a leitura do TSC na máquina virtual retorna o valor do TSC da máquina física e a gravação do TSC na máquina virtual não tem efeito. Migrar a máquina virtual para outro host, retomá-la do estado suspenso ou reverter para um instantâneo faz com que o TSC salte de forma descontínua. Alguns sistemas operacionais convidados falham na inicialização ou apresentam outros problemas de cronometragem quando a virtualização TSC está desativada. No passado, esse recurso às vezes era recomendado para melhorar o desempenho de aplicativos que leem o TSC com frequência., mas o desempenho do TSC virtual foi melhorado substancialmente nos produtos atuais. O recurso também tem sido recomendado para uso na realização de medições que exigem uma fonte precisa de tempo real na máquina virtual.

Resumindo, você precisa adicionar o parâmetro

monitor_control.virtual_rdtsc=FALSO

Conclusão

Você provavelmente tem uma pergunta: por que o SQL chama GetTimePrecise com tanta frequência?

Não tenho o código-fonte do SQL Server, mas a lógica diz isso. SQL é quase um sistema operacional com simultaneidade cooperativa, onde cada thread deve “ceder” de tempos em tempos. Qual é o melhor lugar para fazer isso? Onde há uma espera natural - bloqueio ou IO. Ok, mas e se estivermos girando em loops computacionais? Então o lugar óbvio e quase único é no intérprete (este não é realmente um intérprete), após executar a próxima instrução.

Geralmente, o servidor SQL não é usado para computação pura e isso não é um problema. Mas os loops que funcionam com todos os tipos de tabelas temporárias (que são imediatamente armazenadas em cache) transformam o código em uma sequência de instruções executadas muito rapidamente.

A propósito, se você agrupar a função em NATIVAMENTE COMPILADA, ela para de pedir tempo e sua velocidade aumenta 10 vezes. E quanto à multitarefa cooperativa? Mas para código compilado nativamente, tivemos que fazer MULTITASKING PREEMPTIVE em SQL.

Fonte: habr.com

Adicionar um comentário