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.

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
GOJeś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.

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
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
