Da, vechiul meu laptop este de câteva ori mai puternic decât serverul tău de producție.

Acestea sunt exact plângerile pe care le-am auzit de la dezvoltatorii noștri. Cel mai interesant lucru este că acest lucru s-a dovedit a fi adevărat, dând naștere unei lungi anchete. Vom vorbi despre serverele SQL care rulează pe VMware.

Da, vechiul meu laptop este de câteva ori mai puternic decât serverul tău de producție.

De fapt, este ușor să ne asigurăm că serverul de producție se află fără speranță în spatele laptopului. Executați (nu pe tempdb și nu pe o bază de date cu Durabilitate întârziată activată) codul:

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

Pe desktopul meu durează 5 secunde, iar pe serverul de producție durează 28 de secunde. Deoarece SQL trebuie să aștepte sfârșitul fizic al intrării din jurnalul de tranzacții, iar aici facem tranzacții foarte scurte. Aproximativ vorbind, am condus un camion mare și puternic în traficul orașului și am privit cum a fost depășit în mod amețitor de cei care livreau pizza pe scutere - debitul nu este important aici, doar latența este importantă. Și nicio stocare în rețea, indiferent câte zerouri ar fi în prețul său, nu poate învinge SSD-ul local din punct de vedere al latenței.

(în comentarii s-a dovedit că am mințit - am avut durabilitate întârziată în ambele locuri. Fără durabilitate întârziată rezultă:
Desktop - 39 de secunde, 15K tr/sec, 0.065 ms/io dus-întors
PROD - 360 secunde, 1600 tr/sec, 0.6 ms
Ar fi trebuit să observ că a fost prea rapid)

Totuși, în acest caz avem de-a face cu zerouri triviale ale funcției zeta Riemann cu un exemplu banal. În exemplul pe care mi l-au adus dezvoltatorii, a fost diferit. Eram convins că au dreptate și am început să scot din exemplu toate specificul lor legat de logica de business. La un moment dat mi-am dat seama că aș putea să arunc complet codul lor și să-l scriu pe al meu - ceea ce demonstrează aceeași problemă - în producție funcționează de 3-4 ori mai lent:

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

Dacă totul este în regulă, atunci verificarea primarității unui număr va dura 6-7-8 secunde. Acest lucru s-a întâmplat pe mai multe servere. Dar la unii, verificarea a durat 25-40 de secunde. Interesant este că nu existau servere unde execuția să dureze, să zicem, 14 secunde - codul a funcționat fie foarte repede, fie foarte lent, adică problema era, să spunem, alb-negru.

Ce am facut? S-au folosit valorile VMware. Totul a fost bine acolo - a existat o abundență de resurse, Ready time = 0, a fost suficient de toate, în timpul testului atât pe servere rapide, cât și pe cele lente CPU = 100 pe un vCPU. Am făcut un test pentru a calcula numărul Pi - testul a arătat aceleași rezultate pe orice server. Mirosul magiei negre devenea din ce în ce mai puternic.

Odată ajuns la ferma DEV, am început să joc cu serverele. S-a dovedit că vMotion de la gazdă la gazdă poate „vindeca” un server, dar poate și transforma un server „rapid” într-unul „lent”. Se pare că asta este - unele gazde au o problemă... dar... nu. O mașină virtuală a fost lentă pe gazdă, să zicem A, dar a funcționat rapid pe gazda B. Și o altă mașină virtuală, dimpotrivă, a funcționat rapid pe A și a încetinit pe B! Atât mașinile „rapide” cât și „lente” se învârteau adesea pe gazdă!

Din acel moment, în aer se simțea un miros distinct de sulf. La urma urmei, problema nu a putut fi atribuită mașinii virtuale (patch-uri Windows, de exemplu) - la urma urmei, s-a transformat în „rapid” cu vMotion. Dar nici problema nu a putut fi atribuită gazdei - la urma urmei, ar putea avea atât mașini „rapide”, cât și „lente”. De asemenea, acest lucru nu a fost legat de încărcare - am reușit să obțin o mașină „lentă” pe gazdă, unde nu era nimic în afară de ea.

Din disperare, am lansat Process Explorer de la Sysinternals și m-am uitat la stiva SQL. Pe mașinile lente, linia mi-a atras imediat atenția:

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

Asta era deja ceva. Programul a fost scris:

    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);
                }
            }
        }
    }

Acest program a demonstrat o încetinire și mai pronunțată - pe mașinile „rapide” arată 16-18 milioane de cicluri pe secundă, în timp ce pe mașinile lente arată un milion și jumătate, sau chiar 700 de mii. Adică diferența este de 10-20 de ori (!!!). Aceasta a fost deja o mică victorie: în orice caz, nu exista nicio amenințare de a rămâne blocat între suportul Microsoft și VMware, astfel încât să întoarcă săgeți unul asupra celuilalt.

Apoi progresul s-a oprit - vacanțe, chestiuni importante, isterie virală și o creștere bruscă a volumului de muncă. Le-am menționat adesea colegilor mei problema magică, dar uneori părea că nici măcar nu mă credeau întotdeauna - afirmația că VMware încetinește codul de 10-20 de ori a fost prea monstruoasă.

Am încercat să descopăr singur ceea ce mă încetinește. Uneori mi s-a părut că am găsit o soluție - pornirea și dezactivarea hot plug-urilor, schimbarea cantității de memorie sau a numărului de procesoare transforma adesea mașina într-una „rapidă”. Dar nu pentru totdeauna. Dar ceea ce s-a dovedit a fi adevărat este că este suficient să ieși și să bati la volan - adică să schimbi orice parametrul mașinii virtuale

În cele din urmă, colegii mei americani au găsit brusc cauza principală.

Da, vechiul meu laptop este de câteva ori mai puternic decât serverul tău de producție.

Gazdele diferă ca frecvență!

  • De regulă, aceasta nu este mare lucru. Dar: atunci când trece de la o gazdă „nativă” la o gazdă cu o frecvență „diferită”, VMware trebuie să ajusteze rezultatul GetTimePrecise.
  • De regulă, aceasta nu este o problemă, cu excepția cazului în care există o aplicație care solicită ora exactă de milioane de ori pe secundă, cum ar fi serverul SQL.
  • Dar acest lucru nu este înfricoșător, deoarece serverul SQL nu face întotdeauna acest lucru (vezi Concluzie)

Dar există cazuri când această greblă lovește puternic. Și totuși, da, apăsând pe roată (modificând ceva în setările VM) am forțat VMware să „recalculeze” configurația, iar frecvența gazdei curente a devenit frecvența „nativă” a mașinii.

decizie

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

Când dezactivați virtualizarea TSC, citirea TSC din interiorul mașinii virtuale returnează valoarea TSC a mașinii fizice, iar scrierea TSC din interiorul mașinii virtuale nu are niciun efect. Migrarea mașinii virtuale la o altă gazdă, reluarea acesteia din starea suspendată sau revenirea la un instantaneu face ca TSC să sară discontinuu. Unele sisteme de operare invitate nu reușesc să pornească sau prezintă alte probleme de cronometrare atunci când virtualizarea TSC este dezactivată. În trecut, această caracteristică a fost uneori recomandată pentru a îmbunătăți performanța aplicațiilor care citesc frecvent TSC, dar performanța TSC-ului virtual a fost îmbunătățită substanțial în produsele actuale. Caracteristica a fost, de asemenea, recomandată pentru utilizare atunci când se efectuează măsurători care necesită o sursă precisă de timp real în mașina virtuală.

Pe scurt, trebuie să adăugați parametrul

monitor_control.virtual_rdtsc = FALSE

Concluzie

Probabil aveți o întrebare: de ce apelează SQL atât de des la GetTimePrecise?

Nu am codul sursă al serverului SQL, dar logica spune asta. SQL este aproape un sistem de operare cu concurență cooperativă, în care fiecare fir trebuie să „cedeze” din când în când. Unde este cel mai bun loc pentru a face asta? Unde există o așteptare naturală - blocare sau IO. Bine, dar dacă facem bucle de calcul? Apoi, locul evident și aproape singurul este în interpret (acesta nu este chiar un interpret), după executarea următoarei instrucțiuni.

În general, serverul SQL nu este folosit pentru unghii de calcul pur și aceasta nu este o problemă. Dar buclele care funcționează cu tot felul de tabele temporare (care sunt imediat memorate în cache) transformă codul într-o secvență de instrucțiuni executate foarte rapid.

Apropo, dacă înfășurați funcția în NATIVELY COMPILED, atunci nu mai cere timp, iar viteza acesteia crește de 10 ori.Ce zici de multitasking cooperativ? Dar pentru codul compilat nativ a trebuit să facem PREEMPTIVE MULTITASKING în SQL.

Sursa: www.habr.com

Adauga un comentariu