Недавно я писал статью - что такое 50% cpu? На системах с hyperthreading 50% cpu по метрикам означает, что большая часть ресурсов сервера уже использована. То есть cpu>50% - это уже "желтая зона", и мы ожидаем замедление всего, чего можно. Но я никогда не думал до экспериментов, что падение производительности может быть столь катастрофическим.
Для экспериментов я использую MSSQL. Если вы не связаны с базами, прочитайте первую часть по диагонали до выводов.
MSSQL: inserts на максималках
Давайте создадим табличку:
create table ins (id int identity primary key, dt datetime,
spid int, guid uniqueidentifier, str varchar(100))
и напишем процедуру, которая будет вставлять записи по одной:
create procedure DoIns
@n int
as
set nocount on
while @n>0 begin
insert into ins (dt,spid,guid,str)
select getdate(),@@spid,newid(),'this is a string for me and for you'
set @n=@n-1
end
GO
Вставка по одной записи является крайне неудобной для SQL server, так как каждая запись вставляется в отдельной транзакции, а они записываются синхронно. На старых HDD, вращающихся дисках вы могли получить скорость вставки в 50-70 записей. К счастью, на моем домашнем SSD получается вставить 6240 записей в секунду, а на тестовом сервере в Яндекс облаке - 15 тысяч. Здесь мы упираемся в latency между сервером и хранилищем и в производительность самого хранилища.
Так как это меня сейчас не интересует я поставлю опцию базы Delayed Durability = FORCED, сделав запись транзакций асинхронной. Скорость вставки сразу возросла до 24500, а на сервере в облаке до 90K в секунду.
Теперь будем вставлять записи во много потоков - в 1,2,3,..8 потоков одновременно. Здесь ограничивающим фактором будет следующее: табличка имеет identity, которые возрастают, следовательно, все потоки вставляют записи в конец, в последнюю страницу. Только один процесс может менять страницу, это критическая секция, которая защищается ожиданием PAGELATCH (не путать с PAGEIOLATCH, которая говорит об ожидании физического ввода вывода).
PAGELATCH довольно редко оказывается узким местом, но если оно оказывается таковым, то Microsoft рекомендует включить специальный флаг для индекса, в данном случае это:
ALTER INDEX PK__ins__3213E83FC16B68B2 ON dbo.ins
SET (OPTIMIZE_FOR_SEQUENTIAL_KEY = ON);
Эксперимент с разным числом потоков
Итак, варьируем число потоков и получаем:
Я не могу объяснить провал на 1 потоке у меня на домашнем компьютере, на сервере в облаке его нет, но в остальном картинка довольно логична: чем больше потоков, там больше они мешают друг другу, насыщая вставку, и опция OPTIMIZE_FOR_SEQUENTIAL_KEY немного помогает.
Пока все логично. Но...
Добавим нагрузку по CPU
Просто тупую расчетную нагрузку, которая никуда не пишет и не читает. Например, такую функцию:
create function [dbo].[isPrimeN] (@n bigint)
returns int
WITH NATIVE_COMPILATION, SCHEMABINDING
as
BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'us_english')
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
Обращаю внимание, что это natively compiled функция, с обычной трюк может не пройти. Дальше будем просто гонять ее в бесконечном цикле:
declare @n int while 1=1 select @n=dbo.isPrimeN(1000000000000037)
Вставку будем вести 4 потоками. На моем компе 8vcpu = 4cpu HT
Я приведу результаты в виде таблицы:
Число паразитных пожирателей CPU |
Скорость inserts / sec |
0 |
74635 |
1 |
74502 |
2 |
36448 |
3 |
33161 |
4 |
32894 |
5 |
212 |
6 |
550 |
Последние две строки вызывают шок. На сервере в облаке числа были другие, самые плохие были в районе 1500. Но число потоков варьировались: иногда система срывалась в крутое пике на 3 потребителях cpu, иногда на 2, а бывали периоды, когда она тупила так сама по себе. А что еще ожидать от маленького сервера с 8 cpu, который наверняка разделяет хост с большим числом шумных соседей? Именно поэтому и я стал тестировать это на домашней машине, у которой тоже 8 cpu, 4 cpu hyperthreaded.
Анализ
Как такое могло произойти? Итак, что меняется после появления 4 процессов, непрерывно жрущих CPU? Очевидно, задействуются Hyperthreaded половины процессоров (Windows достаточно умна чтобы вначале раскидывать потребителей CPU на cpu 'через один'). Но мы помним, что технология HT дает дополнительные 20-30%. Да, вторые 50% cpu, как я писал в статье, это "ненастоящие" 50%, и мы ожидаем просадки по производительности, но ведь не в 100 раз, правда?
Я попробую поразмышлять. Итак, писатель выставляет заглушку PAGELATCH и входит в критическую секцию, в которой он спокойно модифицирует страницу в BUFFER POOL. По окончании он снимает замок. Процессы, которые уперлись в PAGELATCH уходят ждать (как мне удалось выяснить, это все таки не spinlock а долговременный latch).
Теперь представим, что на одном ядре работает и тупой пожиратель CPU и процесс, который модифицирует страницу. Мы помним, что технология HT позволяет одному конвейеру двигаться, пока второй ушел за памятью. Но пожиратель CPU крутится в крошечном цикле и за памятью никуда не ходит, а вот процесс, который модифицирует страницу только и делает, что в эту память пишет! То есть это идеальная комбинация, когда партнеры полностью противоположны, и один процесс страдает.
Дальше все просто - процесс модификации движется очень медленно, критическая секция висит максимально долго, все безумно тормозит. Если заменить natively compiled функцию на обычную, то она будет ходить к scheduler на каждой своей строке и ситуация не доходит до такой крайности. Вполне вероятно, что CPU-intensive процессы вне SQL server точно также тормозили бы его, как тормозят в облаке соседи по хосту. Ничего себе так, сосед по хосту может замедлить ваш процессинг в 100 раз!
Для меня открытием стало то, что в мире HT никто не обещал честного деления ядра. И я построил пример где это разделение максимально нечестно.
Эти размышления могут быть и неверными, мне будет интересно услышать ваше мнение.
Комментарии (8)
ky0
22.11.2024 15:03Реквестирую следующую часть - "я выдал виртуалкам меньше ядер, и стало не медленнее, а быстрее", в главной роли vsphere cpu scheduler.
Tzimie Автор
22.11.2024 15:03Я знаю этот эффект, работал на VMware. Но я не хозяин Яндекс клауда, они темнилы и я не знаю, с каким недовесом они продают ядра
LeVoN_CCCP
22.11.2024 15:03На выделенных SQL я тоже как-то пришёл к выводу, что HT надо отключать. Но немного с другой логикой - пытая МС насчёт лицензирования (там тоже толком не могли нормально ответить, а гайд написан в максимально далёких от самой системы терминах наподобие Virtual processors - Virtual cores что похоже одно и то же) и распределения нагрузки под 70%+, выяснилось что получаем систему которая начинает подтормаживать, но платим мы как за честные ядра.
kovserg
22.11.2024 15:03Это давно известный факт. Особенно для процессоров intel. Проблема вызывается делением, т.к. блоков целочисленного деления в них мало. Если хотите ускорить вычисления уберите деление из вычислительных потоков которые исполняются на одном ядре или разнесите их на разные ядра, комбинируя с потоками которые не используют целочисленное деление.
poige
про hyper-threading нужно в основном помнить, что эффект от него очень специфичен для конкретных задач и их комбинации; и бывает, что он ускоряет даже больше, чем на 20–30 %. Поэтому рекомендация обычно крайне простая: не хочется сюрпризов, отключать его. Хочется, но приятных — тестировать.
Что касается «честного деления», то многое будет зависеть от планировщика. Поэтому запускать «паразитную» нагрузку в том же движке RDBMS, это основной подозрительный момент. Понятно, что так имеет смысл проверять тоже, но прежде, чем говорить за весь hyper-threading, было бы неплохо запустить её «снаружи». И сравнить.
Tzimie Автор
Попробую попозже, отпишусь