Так мій старий laptop в кілька разів потужніший, ніж ваш production server

Саме такі претензії я почув від наших девелоперів. Найцікавіше, що це виявилося правдою, давши початок тривалому розслідуванню. Йтиметься про SQL servers, які крутяться у нас на VMware.

Так мій старий laptop в кілька разів потужніший, ніж ваш production server

Власне, домогтися, щоб production server безнадійно відстав від лаптопа легко. Виконайте (не на tempdb і не на базі з увімкненою Delayed Durability) код:

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

На моєму робочому столі він виконується 5 секунд, а на production server - 28 секунд. Тому що SQL повинен чекати фізичного закінчення запису в transaction log, а ми робимо тут дуже короткі транзакції. Грубо кажучи, ми загнали велику потужну вантажівку в міський трафік, і спостерігаємо, як її лихо обганяють доставники піци на скутерах - тут не важливий через путівку, важлива лише latency. А жоден network storage, скільки б нулів не було в його ціні, не зможе виграти по latency у локального SSD.

(У коментах з'ясувалося що я збрехав - у мене в обох місцях затесався delayed durability. Без delayed durability виходить:
Desktop - 39 секунд, 15K tr/sec, 0.065ms /io roundtrip
PROD - 360 секунд, 1600 tr/sec, 0.6ms
Я повинен був звернути увагу, що аж надто швидко)

Однак у разі ми маємо справу з тривіальними нулями зета функції Рімана з тривіальним прикладом. У тому прикладі, що мені принесли девелопери, було інше. Я переконався, що вони мають рацію, і почав вичищати з прикладу всю їхню специфіку, пов'язану з бізнес-логікою. У якийсь момент я зрозумів, що можу повністю викинути їхній код, і написати свій — який демонструє ту саму проблему — на production він виконується в 3-4 рази повільніше:

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

Якщо у вас все добре, то перевірка простоти числа буде виконуватись 6-7-8 секунд. Так було на ряді серверів. Але на деяких перевірка займала 25-40 секунд. Що цікаво, не було серверів, де виконання займало б, скажімо, 14 секунд — код працював або дуже швидко, або повільно, тобто проблема була, скажімо так, чорно білою.

Що я зробив? Поліз у метрики VMware. Там було все добре - ресурсів було в надлишку, Ready time = 0, всього вистачає, під час тесту і на швидких, і на повільних серверах CPU = 100 на одному vCPU. Я взяв тест із розрахунку числа Pi - тест показував однакові результати на будь-яких серверах. Все більше пахло чорною магією.

Вибравшись на DEV ферму, я почав грати серверами. З'ясувалося, що vMotion з хоста на хост може "вилікувати" сервер, але може і навпаки, "швидкий" сервер перетворити на "повільний". Здається ось воно — якісь хости мають проблему… але… ні. Якась віртуалка гальмувала на хості, припустимо, A але працювала швидко на хості B. А інша віртуалка навпаки, працювала швидко на A і гальмувала на B! На хості часто крутилися і "швидкі" і "повільні" машинки!

З цього моменту в повітрі виразно запахло сіркою. Адже проблема не могла бути приписана ні віртуалці (windows patches, наприклад) — вона ж перетворювалася на «швидку» при vMotion. Але проблема також не могла бути приписана хосту - адже на ньому могли бути як швидкі, так і повільні машинки. Також це не було пов'язане з навантаженням - мені вдалося отримати "повільну" машинку на хості, де, крім неї, взагалі не було нічого.

Від розпачу я запустив Process Explorer від Sysinternals і переглянув стек SQL. На повільних машинках мені відразу кинувся в очі рядок:

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

Це вже було щось. Була написана програма:

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

Ця програма демонструвала ще яскравіше уповільнення — на «швидких» машинах вона показує 16-18 мільйонів циклів за секунду, тоді як на повільних — півтора мільйона, а то й 700 тисяч. Тобто різниця складає 10-20 разів (!!!). Це було вже маленькою перемогою: принаймні не було загрози застрягти між Microsoft і VMware support так, щоб вони переводили стрілки один на одного.

Далі прогрес зупинився – відпустка, важливі справи, вірусна істерія та різке зростання навантаження. Я часто згадував магічну проблему колегам, але часом здавалося, що вони навіть не завжди мені вірять — надто жахливою була заява від того, що VMware уповільнює код у 10-20 разів.

Я намагався сам розкопати, що гальмує. Часом мені здавалося, що я знайшов рішення — включення та вимкнення Hot plugs, зміна об'єму пам'яті чи числа процесорів часто перетворювала машинку на «швидку». Але не назавжди. А ось що виявилося правдою — то це те, що достатньо вийти і постукати по колесу — тобто змінити будь параметр віртуалки

Зрештою, мої американські колеги раптом знайшли root cause.

Так мій старий laptop в кілька разів потужніший, ніж ваш production server

Хости відрізнялися частотою!

  • Як правило, це не страшно. Але: при переїзді з 'рідного' хоста на хост з 'іншою' частотою VMware має коригувати результат GetTimePrecise.
  • Як правило це не страшно, якщо тільки не виявляється аплікації, яка просить точний час мільйони разів на секунду, як SQL Server.
  • Але і це не страшно, тому що SQL Server робить це далеко не завжди (див. Висновок)

Але є випадки, коли ці граблі боляче б'ють. І таки так, постукавши по колесу (змінивши щось в налаштуваннях VM) я змушував VMware "перерахувати" конфігурацію, і частота поточного хоста ставала "рідною" частотою машини.

Рішення

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

Якщо ви маєте здатність до virtualization of the TSC, reading the TSC from within virtual machine returns the physical machine's TSC value, and writing the TSC from within virtual machine has no effect. Схема віртуальної машини до іншого host, resuming it from suspended state, або reverting до snapshot causas до TSC до jump discontinuously. Деякі інші операційні системи неспроможні стріляти, або здійснити інші часипроникнення проблем, коли TSC virtualization є неможливим. In the past, this feature has sometimes been recomended to improve performance of applications that read the TSC frequently, Але розробка віртуальної TSC має бути невиправданим substantially in current products. The feature has also been recomended for use when performing measurements that require a precise source of real time in the virtual machine.

Коротше кажучи, треба додати параметр

monitor_control.virtual_rdtsc = FALSE

Висновок

У вас напевно постало питання: а нафіга SQL викликати GetTimePrecise так часто?

У мене немає вихідних SQL Server, але логіка говорить ось що. SQL це майже операційна система з cooperative concurrency, де кожен thread повинен час від часу «поступатися». А де це краще зробити? Там, де є природне очікування – lock чи IO. Добре, а що якщо ми крутимо обчислювальні цикли? Тоді очевидне та майже єдине місце — в інтерпертаторі (це не зовсім інтерпретатор) після виконання чергового оператора.

Як правило, SQL server не використовується для забивання цвяхів чистих обчислень, і це не є проблемою. Але цикли з роботою з будь-якими тимчасовими табличками (які тут же кешуються) перетворюють код на послідовність операторів, що дуже швидко виконуються.

До речі, якщо функцію обернути в NATIVELY COMPILED, то вона перестає вимагати час, і її швидкість збільшується раз на 10. А як же cooperative multitasking? А ось для natively compiled code і довелося SQL зробити PREEMPTIVE MULTITASKING.

Джерело: habr.com

Додати коментар або відгук