Přesně tyto stížnosti jsem slyšel od našich vývojářů. Nejzajímavější na tom je, že se to ukázalo jako pravda, což vedlo k dlouhému vyšetřování. Budeme mluvit o SQL serverech, které běží na VMware.
Ve skutečnosti je snadné zajistit, aby produkční server byl beznadějně za notebookem. Spusťte (ne na tempdb a ne na databázi s povolenou zpožděnou trvanlivostí) kód:
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
Na mém desktopu to trvá 5 sekund a na produkčním serveru to trvá 28 sekund. Protože SQL musí čekat na fyzický konec záznamu transakčního protokolu a my zde provádíme velmi krátké transakce. Zhruba řečeno, vjeli jsme do městského provozu velkým, výkonným náklaďákem a sledovali, jak ho svižně předjíždějí doručovatelé pizzy na skútrech – propustnost zde není důležitá, důležitá je pouze latence. A žádné síťové úložiště, bez ohledu na to, kolik nul je v jeho ceně, nemůže porazit místní SSD z hlediska latence.
(v komentářích se ukázalo, že jsem lhal - měl jsem zpožděnou trvanlivost na obou místech. Bez opožděné trvanlivosti se ukazuje:
Desktop – 39 sekund, 15 kB tr/s, 0.065 ms/io zpáteční cesta
PROD - 360 sekund, 1600 tr/s, 0.6 ms
Měl jsem si všimnout, že to bylo příliš rychlé)
V tomto případě však máme co do činění s triviálními nulami Riemannovy zeta funkce s triviálním příkladem. V příkladu, který mi vývojáři přinesli, to bylo jiné. Byl jsem přesvědčen, že mají pravdu, a začal jsem z příkladu odstraňovat všechna jejich specifika související s obchodní logikou. V určitém okamžiku jsem si uvědomil, že bych mohl úplně zahodit jejich kód a napsat svůj vlastní - což ukazuje stejný problém - ve výrobě to běží 3-4krát pomaleji:
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
Pokud je vše v pořádku, bude kontrola primality čísla trvat 6-7-8 sekund. Stalo se to na několika serverech. Ale u některých trvala kontrola 25-40 sekund. Zajímavé je, že nebyly žádné servery, kde by provedení trvalo řekněme 14 sekund – kód fungoval buď velmi rychle, nebo velmi pomalu, to znamená, že problém byl řekněme černobílý.
Co jsem udělal? Použité metriky VMware. Všechno tam bylo v pořádku - zdrojů dostatek, Ready time = 0, všeho bylo dost, při testu na rychlých i pomalých serverech CPU = 100 na jednom vCPU. Udělal jsem test na výpočet čísla Pi - test ukázal stejné výsledky na jakémkoli serveru. Vůně černé magie byla stále silnější.
Jakmile jsem se dostal na farmu DEV, začal jsem hrát se servery. Ukázalo se, že vMotion z hostitele na hostitele může „vyléčit“ server, ale může také změnit „rychlý“ server na „pomalý“. Zdá se, že je to tak - někteří hostitelé mají problém... ale... ne. Některý virtuální stroj byl pomalý na hostiteli, řekněme A, ale pracoval rychle na hostiteli B. A jiný virtuální stroj naopak pracoval rychle na A a zpomalil na B! Na hostiteli se často točily „rychlé“ i „pomalé“ stroje!
Od té chvíle byl ve vzduchu cítit zřetelný zápach síry. Koneckonců, problém nelze připsat virtuálnímu stroji (například záplaty Windows) - koneckonců se s vMotion změnil na „rychlý“. Problém však také nelze připsat hostiteli - koneckonců mohl mít „rychlé“ i „pomalé“ stroje. Také to nesouviselo se zátěží - podařilo se mi dostat „pomalý“ stroj na hostitele, kde kromě něj nebylo vůbec nic.
Ze zoufalství jsem spustil Process Explorer od Sysinternals a podíval se na SQL stack. Na pomalých strojích mě linka okamžitě zaujala:
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
... přeskočeno
sqldk.dll!SystemThread::MakeMiniSOSThread+0xa54
KERNEL32.DLL!BaseThreadInitThunk+0x14
ntdll.dll! RtlUserThreadStart + 0x21
Tohle už bylo něco. Program byl napsán:
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);
}
}
}
}
Tento program předvedl ještě výraznější zpomalení - na „rychlých“ strojích ukazuje 16-18 milionů cyklů za sekundu, zatímco na pomalých strojích ukazuje jeden a půl milionu nebo dokonce 700 tisíc. To znamená, že rozdíl je 10-20krát (!!!). To už bylo malé vítězství: každopádně nehrozilo, že se mezi Microsoftem a podporou VMware zaseknete tak, že by si navzájem točily šipky.
Pak se pokrok zastavil – dovolené, důležité záležitosti, virová hysterie a prudký nárůst pracovní zátěže. Často jsem se o magickém problému zmiňoval svým kolegům, ale občas se zdálo, že mi ani vždy nevěřili - prohlášení, že VMware zpomaluje kód 10-20krát, bylo příliš obludné.
Snažil jsem se vydolovat to, co mě zpomalovalo. Občas se mi zdálo, že jsem našel řešení – zapínání a vypínání Hot plugs, změna velikosti paměti nebo počtu procesorů často proměnila stroj v „rychlý“. Ale ne navždy. Co se ale ukázalo jako pravda, stačí vyjet a zaklepat na volant – tedy převléknout každý parametr virtuálního stroje
Konečně moji američtí kolegové náhle našli hlavní příčinu.
Domácí se lišili ve frekvenci!
- Zpravidla nejde o nic velkého. Ale: při přechodu z „nativního“ hostitele na hostitele s „jinou“ frekvencí musí VMware upravit výsledek GetTimePrecise.
- Zpravidla to není problém, pokud neexistuje aplikace, která požaduje přesný čas milionkrát za sekundu, jako je SQL server.
- Ale to není děsivé, protože SQL server to vždy nedělá (viz Závěr)
Jsou ale případy, kdy toto hrábě zasáhne tvrdě. A přesto ano, klepnutím na kolečko (změnou něčeho v nastavení VM) jsem přinutil VMware „přepočítat“ konfiguraci a frekvence aktuálního hostitele se stala „nativní“ frekvencí stroje.
rozhodnutí
Když zakážete virtualizaci TSC, čtení TSC z virtuálního stroje vrátí hodnotu TSC fyzického stroje a zápis TSC z virtuálního stroje nemá žádný vliv. Migrace virtuálního počítače na jiného hostitele, jeho obnovení z pozastaveného stavu nebo návrat ke snímku způsobí, že TSC bude přerušovaně přeskakovat. Některé hostované operační systémy se nespustí nebo vykazují jiné problémy s měřením času, když je virtualizace TSC zakázána. V minulosti byla tato funkce někdy doporučována pro zlepšení výkonu aplikací, které často čtou TSC, ale výkon virtuálního TSC byl u současných produktů podstatně vylepšen. Tato funkce byla také doporučena pro použití při provádění měření, která vyžadují přesný zdroj reálného času ve virtuálním počítači.
Stručně řečeno, musíte přidat parametr
monitor_control.virtual_rdtsc = FALSE
Závěr
Pravděpodobně máte otázku: proč SQL volá GetTimePrecise tak často?
Nemám zdrojový kód SQL serveru, ale logika říká toto. SQL je téměř operační systém s kooperativní souběžností, kde se každé vlákno musí čas od času „poddat“. Kde je nejlepší místo k tomu? Kde je přirozené čekání - zámek nebo IO. Dobře, ale co když točíme výpočetní smyčky? Pak je zřejmé a téměř jediné místo v tlumočníku (ve skutečnosti to není tlumočník), po provedení dalšího příkazu.
Obecně se SQL server nepoužívá pro čistě výpočetní přibíjení a to není problém. Ale smyčky, které pracují s nejrůznějšími dočasnými tabulkami (které se okamžitě ukládají do mezipaměti), mění kód na sekvenci velmi rychle provedených příkazů.
Mimochodem, pokud funkci zabalíte do NATIVNĚ KOMPILOVANÉ, tak přestane žádat o čas a její rychlost se zvýší 10x. Co kooperativní multitasking? Ale pro nativně zkompilovaný kód jsme museli provést PREEMPTIVNÍ MULTITASKING v SQL.
Zdroj: www.habr.com