Sì, il mio vecchio laptop è molte volte più potente del vostro server di produzione

Ho sentito proprio queste lamentele dai nostri sviluppatori. La cosa interessante è che si è rivelata vera, dando inizio a un'indagine prolungata. Parleremo dei server SQL che girano su VMware.

Sì, il mio vecchio laptop è molte volte più potente del vostro server di produzione

In effetti, è facile far sì che un server di produzione sia chiaramente più lento di un laptop. Esegui (non su tempdb e non su un database con Delayed Durability attivato) il seguente codice:

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

Sul mio desktop ci vuole 5 secondi, mentre sul server di produzione 28 secondi. Questo perché SQL deve attendere il completamento fisico della scrittura nel transaction log, mentre noi qui facciamo transazioni molto brevi. In altre parole, abbiamo messo un grande camion potente nel traffico cittadino e osserviamo come i fattorini della pizza con i loro scooter lo superano facilmente — qui non conta il throughput, ma solo la latenza. E nessuno storage di rete, per quanto costoso, può eguagliare la latenza di un SSD locale.

(nei commenti è emerso che ho mentito — in entrambi i luoghi si è infilato il delayed durability. Senza delayed durability otteniamo:
Desktop — 39 secondi, 15K tr/sec, 0.065ms/io roundtrip
PROD — 360 secondi, 1600 tr/sec, 0.6ms
Dovevo notare che è davvero troppo veloce)

Tuttavia, in questo caso stiamo trattando zero triviali della funzione di Riemann con un esempio banale. Nell'esempio che mi hanno portato gli sviluppatori, la situazione era diversa. Mi sono assicurato che avessero ragione e ho iniziato a ripulire l'esempio da tutta la loro specificità legata alla logica aziendale. A un certo punto, ho capito che potevo gettare via completamente il loro codice e scrivere il mio — che dimostra lo stesso problema — in produzione è eseguito 3-4 volte più lentamente:

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 tutto va bene, il controllo della primalità di un numero verrà eseguito in 6-7-8 secondi. È stato così in vari casi server. Ma in alcuni casi, il controllo richiedeva 25-40 secondi. È interessante notare che non c'erano server in cui l'esecuzione richiedesse, diciamo, 14 secondi — il codice funzionava o molto velocemente o molto lentamente, quindi il problema era, per così dire, bianco o nero.

Cosa ho fatto? Sono andato a controllare le metriche di VMware. Tutto sembrava a posto: c'erano molte risorse, il tempo di preparazione = 0, c'era tutto il necessario, e durante il test, su entrambi i server veloci e lenti, CPU=100 su un vCPU. Ho effettuato un test sul calcolo di Pi — il test mostrava risultati identici su qualsiasi server. L'aria cominciava a profumare di magia nera.

Una volta arrivato nella farm DEV, ho iniziato a sperimentare con i server. Ho scoperto che il vMotion da un host all'altro può "guarire" un server, ma può anche avere l'effetto opposto, trasformando un server "veloce" in uno "lento". Sembra proprio che ci fosse qualche host con un problema… ma… no. Una certa macchina virtuale rallentava sull'host, diciamo, A, ma funzionava rapidamente sull'host B. E un'altra macchina virtuale, al contrario, funzionava rapidamente su A e rallentava su B! Su quell'host giravano frequentemente sia macchine "veloci" che "lente"!

Da questo momento nell'aria si avvertì chiaramente un odore di zolfo. Infatti, il problema non poteva essere attribuito alla macchina virtuale (ad esempio, patch di Windows) — poiché essa si trasformava in "veloce" durante il vMotion. Ma il problema non poteva neanche essere attribuito all'host — poiché su di esso potevano esserci sia macchine "veloci" che "lente". Inoltre, non era correlato al carico — sono riuscito a ottenere una macchina "lenta" su un host dove non c'era niente altro.

Dalla disperazione, ho avviato Process Explorer di Sysinternals e ho esaminato lo stack SQL. Sulle macchine lente mi è subito saltata all'occhio la seguente linea:

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

Era già qualcosa. Era stato scritto un programma:

    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  0)
                {
                    Console.WriteLine("{0}", counter);
                }
            }
        }
    }

Questo programma ha mostrato un rallentamento ancora più marcato: su macchine "veloci" mostra tra i 16 e i 18 milioni di cicli al secondo, mentre su quelle più lente si arriva a un milione e mezzo, o addirittura a 700 mila. Quindi la differenza è di 10-20 volte (!!!). Questo era già un piccolo successo: in ogni caso, non c'era il rischio di rimanere bloccati tra il supporto Microsoft e VMware, che si passano la responsabilità l'uno con l'altro.

Poi il progresso si fermò: ferie, impegni importanti, isteria virale e un aumento improvviso del carico. Ho spesso menzionato il problema magico ai miei colleghi, ma a volte sembrava che non mi credessero del tutto — era davvero mostruoso affermare che VMware rallentasse il codice di 10-20 volte.

Ho cercato di capire da solo cosa stesse causando il rallentamento. A volte mi sembrava di aver trovato la soluzione — accendere e spegnere i Hot plugs, modificare la memoria o il numero di processori spesso rendeva la macchina 'veloce'. Ma solo temporaneamente. Quello che si è rivelato vero è che bastava uscire e dare un colpetto alla ruota — cioè cambiare qualunque il parametro della macchina virtuale.

Finalmente, i miei colleghi americani hanno trovato improvvisamente la causa principale.

Sì, il mio vecchio laptop è molte volte più potente del vostro server di produzione

Gli host differivano per frequenza!

  • Di norma, ciò non è grave. Ma: quando si migra da un host 'nativo' a un host con una frequenza 'diversa', VMware deve correggere il risultato di GetTimePrecise.
  • Di norma, questo non è un problema, a meno che non ci sia un'applicazione che richiede l'ora esatta milioni di volte al secondo, come il server SQL.
  • Ma anche questo non è grave, poiché il server SQL non lo fa sempre (vedi Conclusione).

Ci sono però casi in cui queste trappole colpiscono duramente. E in effetti, battendo su una ruota (cambiando qualcosa nelle impostazioni della VM), costringevo VMware a 'ricontare' la configurazione, e la frequenza dell’host attuale diventava la 'frequenza nativa' della macchina.

Soluzione

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

Quando disabiliti la virtualizzazione del TSC, la lettura del TSC dalla macchina virtuale restituisce il valore del TSC fisico della macchina, e scrivere il TSC dalla macchina virtuale non ha effetto. Migrando la macchina virtuale su un altro host, riprendendola dallo stato sospeso o tornando a uno snapshot causa un salto discontinuo del TSC. Alcuni sistemi operativi guest non riescono ad avviarsi o presentano altri problemi di gestione del tempo quando la virtualizzazione del TSC è disabilitata. In passato, questa funzione è stata talvolta raccomandata per migliorare le prestazioni delle applicazioni che leggono frequentemente il TSC., ma le prestazioni del TSC virtuale sono state migliorate sostanzialmente nei prodotti attuali. La funzione è stata anche raccomandata per l'uso quando si effettuano misurazioni che richiedono una fonte precisa di tempo reale nella macchina virtuale.

In breve, è necessario aggiungere il parametro

monitor_control.virtual_rdtsc = FALSE

Conclusione

Probabilmente ti stai chiedendo: perché chiamare SQL per GetTimePrecise così spesso?

Non ho le sorgenti di SQL server, ma la logica dice questo. SQL è quasi un sistema operativo con concorrenza cooperativa, dove ogni thread deve, di tanto in tanto, "cedere il passo". E dove sarebbe meglio farlo? Dove ci sono attese naturali — lock o IO. Va bene, e se stiamo eseguendo cicli computativi? Allora il luogo ovvio e quasi unico è nell'interprete (non è proprio un interprete), dopo l'esecuzione dell'operatore successivo.

Di norma, SQL server non viene utilizzato per svolgere calcoli puri e questo non è un problema. Ma i cicli che lavorano con varie tabelle temporanee (che sono subito memorizzate nella cache) trasformano il codice in una sequenza di operatori molto rapidamente eseguibili.

A proposito, se si incapsula la funzione in NATIVELY COMPILED, essa smette di richiedere tempo e la sua velocità aumenta di dieci volte. E come sta la concorrenza cooperativa? Ecco perché per il codice nativamente compilato in SQL è stato realizzato il PREEMPTIVE MULTITASKING.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster