Tak, mój stary laptop jest znacznie mocniejszy niż twój serwer produkcyjny.

Takie właśnie zarzuty usłyszałem od naszych deweloperów. Co ciekawe, okazało się to prawdą, co dało początek długiemu śledztwu. Mowa będzie o serwerach SQL, które działają u nas na VMware.

Tak, mój stary laptop jest znacznie mocniejszy niż twój serwer produkcyjny.

Właściwie, aby produkcyjny serwer był wyraźnie wolniejszy od laptopa, jest to łatwe do osiągnięcia. Wykonaj (nie na tempdb i nie na bazie z włączoną Delayed Durability) kod:

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

Na moim komputerze stacjonarnym trwa to 5 sekund, a na serwerze produkcyjnym — 28 sekund. Ponieważ SQL musi czekać na fizyczne zakończenie zapisu w dzienniku transakcji, a my robimy bardzo krótkie transakcje. Mówiąc wprost, wpakowaliśmy dużą ciężarówkę w miejski ruch i obserwujemy, jak nadjeżdżają zwinni dostawcy pizzy na skuterach — nie liczy się przepustowość, liczy się tylko opóźnienie. A żadne magazyny sieciowe, niezależnie od tego, ile zer mają w cenie, nie wygrają z lokalnym SSD pod względem opóźnienia.

(w komentarzach okazało się, że skłamałem — w obu miejscach miałem włączoną delayed durability. Bez delayed durability wychodzi:
Desktop — 39 sekund, 15K tr/s, 0.065ms /io roundtrip
PROD — 360 sekund, 1600 tr/s, 0.6ms
Powinienem był zwrócić uwagę, że zbyt szybko)

Jednak w tym przypadku mamy do czynienia z trywialnymi zerami funkcji Riemanna z trywialnym przykładem. W przykładzie, który przynieśli mi programiści, było co innego. Upewniłem się, że mają rację, a następnie zacząłem czyścić ich przykład z całej specyficzności związanej z logiką biznesową. W pewnym momencie zrozumiałem, że mogę całkowicie wyrzucić ich kod i napisać własny — który demonstruje ten sam problem — na produkcji działa 3-4 razy wolniej:

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

Jeśli wszystko u Ciebie w porządku, to sprawdzanie, czy liczba jest pierwsza, zajmować będzie 6-7-8 sekund. Tak było w kilku przypadkach serwerów. Ale w niektórych przypadkach sprawdzenie zajmowało 25-40 sekund. Co ciekawe, nie było serwerów, na których wykonanie zajmowałoby, powiedzmy, 14 sekund — kod działał albo bardzo szybko, albo bardzo wolno, co sprawia, że problem był, powiedzmy, czarno-biały.

Co zrobiłem? Zajrzałem do metryk VMware. Wszystko wyglądało dobrze — zasobów było pod dostatkiem, czas gotowości = 0, wszystko wystarczało, podczas testu zarówno na szybkich, jak i wolnych serwerach CPU=100 na jednym vCPU. Wziąłem test obliczania liczby Pi — test pokazywał te same wyniki na różnych serwerach. Coraz bardziej zaczęło pachnieć czarną magią.

Kiedy dotarłem na farmę DEV, zacząłem eksperymentować z serwerami. Okazało się, że vMotion z hosta na host może „wyleczyć” serwer, ale może też odwrotnie, zamienić „szybki” serwer w „wolny”. Wydaje się, że to jest to — niektóre hosty mają jakiś problem... ale... nie. Jakieś wirtualne maszyny zwalniały na hoście, powiedzmy A, ale działały szybko na hoście B. Z kolei inna wirtualna maszyna działała szybko na A, a zwalniała na B! Na hoście często krążyły zarówno „szybkie”, jak i „wolne” maszyny!

Od tego momentu w powietrzu wyraźnie poczułem zapach siarki. Problem nie mógł być przypisany żadnej wirtualce (łatki Windows, na przykład) — bo stawała się „szybka” podczas vMotion. Ale problem nie mógł być też przypisany do hosta — na nim mogły być zarówno „szybkie”, jak i „wolne” maszyny. Również nie miało to związku z obciążeniem — udało mi się uzyskać „wolną” maszynę na hoście, na którym nie było nic innego.

Z rozpaczy uruchomiłem Process Explorer od Sysinternals i spojrzałem na stos SQL. Na wolnych maszynach od razu rzuciła mi się w oczy linia:

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

To już było coś. Program został napisany:

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

Ten program wykazał jeszcze bardziej znaczące spowolnienie — na 'szybkich' maszynach osiąga 16-18 milionów cykli na sekundę, podczas gdy na wolnych — półtora miliona, a czasem nawet 700 tysięcy. Oznacza to różnicę wynoszącą od 10 do 20 razy (!!!). To była już mała wygrana: w każdym razie, nie groziło mi utknięcie pomiędzy wsparciem Microsoft i VMware, tak aby przesyłali piłeczkę jeden do drugiego.

Następnie postęp się zatrzymał — urlop, ważne sprawy, wirusowa histeria i nagły wzrost obciążenia. Często wspominałem o magicznym problemie kolegom, ale czasami wydawało się, że nawet mi nie wierzą — było to zbyt przerażające, aby uwierzyć w to, że VMware spowalnia kod 10-20 razy.

Próbowałem samodzielnie odkryć, co właściwie spowalnia. Czasami wydawało mi się, że znalazłem rozwiązanie — włączenie i wyłączenie Hot plugs, zmiana ilości pamięci lub liczby procesorów często przekształcała maszynę w 'szybką'. Ale nie na zawsze. A to, co okazało się prawdą — to że wystarczyło wyjść i stuknąć w koło — czyli zmienić przez kogokolwiek parametr wirtualnej maszyny

W końcu moi amerykańscy koledzy nagle znaleźli przyczynę problemu.

Tak, mój stary laptop jest znacznie mocniejszy niż twój serwer produkcyjny.

Hosty różniły się częstotliwością!

  • Zasadniczo nie jest to straszne. Ale: przy przeniesieniu z 'rodzinnego' hosta na host z 'inną' częstotliwością, VMware musi poprawić wyniki GetTimePrecise.
  • Zasadniczo nie jest to straszne, chyba że pojawi się aplikacja, która żąda dokładnego czasu miliony razy na sekundę, jak SQL server.
  • Ale i to nie jest straszne, ponieważ SQL server nie robi tego wcale zawsze (zob. Podsumowanie)

Jednak są przypadki, gdy te pułapki boleśnie uderzają. I rzeczywiście, stukając w koło (zmieniając coś w ustawieniach VM), wymuszałem na VMware 'przeliczenie' konfiguracji, a częstotliwość bieżącego hosta stawała się 'rodzimą' częstotliwością maszyny.

Rozwiązanie

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

Kiedy wyłączasz wirtualizację TSC, odczyt TSC z poziomu maszyny wirtualnej zwraca wartość TSC fizycznej maszyny, a zapisywanie TSC z poziomu maszyny wirtualnej nie ma efektu. Migracja maszyny wirtualnej na inny host, wznowienie jej ze stanu zawieszenia lub powrót do momentu przywrócenia powoduje skok TSC w sposób nieciągły. Niektóre systemy operacyjne gości nie uruchamiają się, lub wykazują inne problemy z pomiarem czasu, kiedy wirtualizacja TSC jest wyłączona. W przeszłości ta funkcja była czasami zalecana do poprawy wydajności aplikacji, które często odczytują TSC, ale wydajność wirtualnego TSC została znacznie poprawiona w obecnych produktach. Funkcję tę również zalecano do pomiarów, które wymagają precyzyjnego źródła rzeczywistego czasu w maszynie wirtualnej.

Krótko mówiąc, trzeba dodać parametr

monitor_control.virtual_rdtsc = FALSE

Podsumowanie

Z pewnością zastanawiasz się, po co wywoływać GetTimePrecise tak często w SQL?

Nie posiadam źródeł SQL Server, ale logika podpowiada mi to: SQL to prawie system operacyjny z współbieżnością współdzieloną, gdzie każdy wątek musi od czasu do czasu „ustępować”. Gdzie to zrobić? Tam, gdzie występuje naturalne oczekiwanie — lock lub IO. Dobrze, ale co jeśli wykonujemy obliczeniowe pętle? Wtedy oczywiste i prawie jedyne miejsce to w interpreterze (to nie do końca interpreter), po wykonaniu kolejnego operatora.

Zazwyczaj SQL Server nie jest używany do załatwiania czystych obliczeń i nie stanowi problemu. Jednak pętle z operacjami na różnych tymczasowych tabelach (które natychmiast są buforowane) zamieniają kod w sekwencję bardzo szybko wykonywanych operatorów.

Swoją drogą, jeśli funkcję owinąć w NATIVELY COMPILED, przestaje ona żądać czasu, a jej prędkość wzrasta dziesięciokrotnie. A co z współdzielonym multitaskingiem? A więc dla kodu natively compiled w SQL wprowadzono PREEMPTIVE MULTITASKING.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster