MAATRIX / Блог / Митигации Spectre съедали 30% производительности базы

Митигации Spectre съедали 30% производительности базы

MAATRIX

В конце августа 2026 года мы разбирали инцидент, который две недели маскировался под «что-то с диском» или «соседи по гипервизору мешают». База отвечала на треть медленнее, чем неделю назад, при той же нагрузке — и никто из команды не связывал это с плановым обновлением безопасности, которое прошло чуть раньше. История ниже — не абстрактная теория про Spectre и Meltdown, а конкретный путь от «стало медленнее» до «вот почему», с командами, которые можно повторить на своём сервере.

Симптомы: рост задержек без видимых изменений нагрузки

Всё началось буднично: мониторинг показал, что p95 задержка запросов к PostgreSQL выросла примерно вдвое от привычного уровня, а часть тяжёлых аналитических запросов стала укладываться в SLA с трудом. При этом:

  • количество запросов в секунду не изменилось — по логам приложения нагрузка была ровно такой же, как в предыдущие недели;
  • размер базы и объём данных росли предсказуемо, без скачков;
  • диск не показывал роста await или %util в iostat — узкое место было явно не в I/O;
  • сеть между приложением и базой не менялась, задержка на уровне TCP оставалась прежней.

Единственное, что бросалось в глаза в top и vmstat — заметно выросла доля %sys (системное время ядра) относительно %user. Раньше на этом сервере system time редко превышал 8-10%, а тут держался в районе 25-30% почти постоянно. Это и стало первой настоящей зацепкой, хотя тогда её ещё не восприняли всерьёз — списали на «фоновые процессы ОС».

Важная деталь для дальнейшего разбора: незадолго до появления симптомов на сервере отработало плановое обновление пакетов безопасности, включая ядро. Обновление прошло без ошибок, сервис перезапустился штатно, никто не связал два события — до тех пор, пока не начали методично отбрасывать альтернативы.

Первые подозрения: диск, сеть и шумные соседи

Первая версия — деградация диска. NVMe-накопители могут терять производительность из-за износа, переполнения SLC-кэша при постоянной записи или проблем с TRIM. Проверили smartctl -a на предмет реаллоцированных секторов и счётчиков ошибок — всё чисто. Прогнали короткий fio тест на соседнем разделе того же диска в те же часы — показатели укладывались в норму для этой модели накопителя (сравнивали не с абсолютными цифрами из паспорта, а с собственными замерами месячной давности на этом же сервере).

Вторая версия — виртуализация и шумные соседи, если сервер работает не на голом железе, а на VPS с общим гипервизором. Здесь помогает штатный счётчик steal time: в top это колонка st, отдельная от us и sy. Мы подробно разбирали, как отличить «сосед ест ваш CPU» от прочих проблем, в статье про steal time — в данном случае st был около нуля, то есть гипервизор честно отдавал выделенные такты, и версия с соседями отпала. Сервер, к слову, был выделенным (bare metal), так что эта причина изначально была маловероятна, но её всё равно стоило исключить формально, а не на словах.

Третья версия — сеть между приложением и базой, лишние hop'ы, деградация MTU. Проверили mtr и ping за длительный период — стабильно, без потерь и роста RTT.

К этому моменту стало ясно: узкое место не во внешних по отношению к CPU ресурсах. Общий подход к поиску причины высокой нагрузки на процессор — с чего начинать и какие инструменты запускать по порядку — мы описывали в статье про диагностику нагрузки на процессор. В этом инциденте именно такой чек-лист и увёл разбор в сторону ядра, а не приложения.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Что показали метрики: system time и лавина syscall

Следующий шаг — заглянуть внутрь %sys, а не просто зафиксировать его рост. Здесь пригодился perf top (пакет linux-tools-common / linux-tools-$(uname -r) в Debian/Ubuntu):

sudo perf top -p $(pgrep -d, -f postgres)

Наверху списка неожиданно оказались функции, связанные с обработкой прерываний и переключением контекста между режимами ядра и пользователя — то, что обычно занимает доли процента, здесь стабильно держало заметную часть верхних строчек профиля. Это уже прямой намёк на накладные расходы, связанные не с логикой приложения, а с самим механизмом системных вызовов.

Дальше посчитали частоту syscall на процесс:

sudo strace -c -p <pid_postgres_worker> -f -T

(запускали на короткое время, 10-15 секунд, чтобы не давить процесс трассировкой) — и увидели, что PostgreSQL действительно генерирует очень много мелких системных вызовов: read, write, fsync, semop для блокировок, работа с shared memory. Это нормально для СУБД — она устроена так по архитектуре, много данных гоняется через страничный кэш и WAL. Но именно syscall-тяжёлые нагрузки — базы данных, интерпретаторы вроде PHP-FPM с высокой частотой запросов, сетевые прокси — сильнее всего чувствуют накладные расходы, если у каждого системного вызова внезапно выросла «цена».

Дополнил картину vmstat 1, где столбец cs (context switches per second) оказался заметно выше обычного при той же нагрузке на запросы — это косвенно подтверждало версию про возросшую стоимость переключений между пользовательским и ядерным режимом.

Гипотезы, которые отбросили

Прежде чем дойти до правильного ответа, разобрали и закрыли ещё несколько версий, каждая из которых по отдельности звучала правдоподобно:

  • Раздутая база и деградация автовакуума. Проверили pg_stat_user_tables на предмет мёртвых строк (n_dead_tup) и время последнего автовакуума — цифры были в пределах нормы, ничего похожего на классическую проблему с автовакуумом, которую мы разбирали отдельно.
  • Устаревшая статистика планировщика. Прогнали ANALYZE вручную, сравнили планы через EXPLAIN (ANALYZE, BUFFERS) для нескольких «тяжёлых» запросов — планы не изменились, индексы использовались как и раньше.
  • Рост конкуренции за блокировки. Проверили pg_locks в моменты пиковой нагрузки — очередей на блокировки не было, деградация была равномерной по всем типам запросов, а не концентрировалась вокруг конкретных таблиц.
  • Проблема с параметрами shared_buffers / work_mem. Сравнили текущий postgresql.conf с версией до инцидента через diff по бэкапу конфига — файл не менялся, настройки памяти были теми же.
  • Изменения в коде приложения. Подняли git log сервисов, обращающихся к базе, за последние недели — ничего, что могло бы объяснить рост именно системного времени на стороне СУБД.

Каждая из этих версий по отдельности могла бы объяснить рост задержек, но ни одна не объясняла главного маркера — стабильно высокий %sys при неизменном профиле запросов. Это и заставило вернуться к единственному изменению, которое реально произошло на сервере в нужный промежуток времени: обновлению ядра.

Настоящая причина: retpoline и KPTI после планового обновления

Проверка оказалась простой, если знать, куда смотреть:

grep -r . /sys/devices/system/cpu/vulnerabilities/

Вывод показал по каждой уязвимости (Spectre v1, Spectre v2, Meltdown, MDS и другие, в зависимости от поколения CPU) статус митигации — что-то вроде Mitigation: Retpolines для Spectre v2 и Mitigation: PTI для Meltdown. Сравнение с состоянием до обновления (по старым логам dmesg и заметкам в системе управления конфигурацией) показало: до апдейта часть митигаций либо отсутствовала, либо ядро было собрано/сконфигурировано иначе, а после планового обновления безопасности они включились в полную силу.

Механика проблемы в двух словах:

  • KPTI (Kernel Page Table Isolation) — защита от Meltdown, которая разделяет таблицы страниц ядра и пользователя. Каждый переход между пользовательским и ядерным режимом (то есть каждый syscall, каждое прерывание) требует дополнительной работы с таблицами страниц и TLB. Чем чаще процесс делает системные вызовы, тем больше суммарные накладные расходы.
  • Retpoline — программная защита от Spectre v2 через замену косвенных переходов (indirect branch) на специальную последовательность инструкций, которая мешает спекулятивному исполнению угадывать адрес перехода. Это дороже обычного jmp/call и особенно заметно в коде с большим количеством виртуальных вызовов и переходов по указателям — а ядро Linux и рантайм СУБД именно так и устроены.

Для нагрузок, которые почти не выходят в ядро (чистые вычисления в userspace, отрисовка, часть ML-инференса), это может пройти почти незаметно. А вот для PostgreSQL, который активно работает с диском, WAL и блокировками через syscall, — накопительный эффект от KPTI и retpoline вместе вполне способен дать замер в районе 20-40% деградации на syscall-тяжёлых операциях в зависимости от поколения CPU и конкретной комбинации митигаций. Это ориентир из общей картины по индустрии, а не гарантированная цифра — на вашем железе и с вашей версией ядра результат может быть заметно другим, поэтому измерять нужно на своих данных, а не полагаться на чужие проценты.

Отдельно стоит сказать про поколение процессора. На старых CPU без аппаратной поддержки части митигаций (например, без IBRS в железе) всё приходится компенсировать программно, и накладные расходы выше. Более новые поколения Intel и AMD получили аппаратные механизмы (enhanced IBRS и аналоги), которые снижают цену защиты. Разницу между платформами по этому и другим параметрам для баз данных мы разбирали в статье про выбор процессора для баз данных — она пригодилась и здесь, когда встал вопрос «что делать дальше».

Что изменили: подтверждение, тюнинг и осознанный выбор

Чтобы не полагаться на догадки, гипотезу проверили экспериментально — но не на проде. Подняли идентичный по конфигурации тестовый сервер, прогнали одинаковый набор запросов через pgbench дважды: один раз с ядром как есть, второй раз временно с параметром загрузки mitigations=off, добавленным в GRUB (GRUB_CMDLINE_LINUX_DEFAULT="mitigations=off" с последующим update-grub и перезагрузкой). Разница в пропускной способности на этом конкретном сервере оказалась заметной и укладывалась в ту же вилку, что и наблюдения на проде — это подтвердило гипотезу и закрыло вопрос «а точно ли дело в этом».

Дальше — самое важное решение инцидента: отключать митигации на проде мы не стали. Это осознанный выбор, а не техническое ограничение — mitigations=off реально возвращает часть производительности, но открывает сервер для атак класса Spectre/Meltdown, а это способ кражи данных из памяти других процессов (в том числе других виртуальных машин на том же хосте, если сервер не выделенный). Для одиночного bare metal сервера без недоверенных соседей риск ниже, чем в мультитенантной среде, но это всё равно решение о приемлемом риске, которое должна принимать команда осознанно, а не по умолчанию через флаг ядра.

Что сделали вместо отключения митигаций:

  1. Снизили syscall-нагрузку архитектурно. Часть чтений вынесли через пул соединений (PgBouncer в режиме transaction для коротких запросов), что снизило накладные расходы на установление соединений и часть системных вызовов на их обслуживание.
  2. Пересмотрели checkpoint и WAL-параметры, чтобы снизить частоту fsync там, где это допустимо по требованиям к надёжности — меньше syscall на диск при той же логической нагрузке.
  3. Зафиксировали новый baseline в мониторинге: %sys, context switches, p95/p99 задержки — теперь это отдельные панели с алертами, а не только общий CPU load.
  4. Добавили проверку в процесс обновлений: перед плановым патчингом ядра на проде теперь прогоняется контрольный pgbench-бенчмарк на staging той же конфигурации — если деградация выходит за согласованный порог, обновление сначала обсуждается с командой, а не просто накатывается по расписанию. Общий процесс, что делать, если плановое обновление всё же положило прод, у нас описан в статье про откат после неудачного обновления.
  5. Заложили апгрейд железа в план — на более новом поколении CPU с аппаратной поддержкой части митигаций та же нагрузка обходится дешевле по накладным расходам. Это не молниеносное решение инцидента, а пункт в дорожной карте на следующий цикл обновления серверов.

Отдельный урок — про мониторинг регрессий безопасности. Обновления безопасности обязательны и откладывать их — плохая идея по понятным причинам, но именно поэтому нужен процесс, который отделяет «обновление применилось» от «обновление ничего не сломало по производительности». Мы отдельно разбирали похожие грабли планового патчинга в статье про частые ошибки автообновлений безопасности — Spectre-митигации это лишь один из сценариев, где «тихое» обновление меняет профиль производительности без единой ошибки в логах.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Как быстро проверить, включены ли митигации Spectre/Meltdown на моём сервере?

Выполните grep -r . /sys/devices/system/cpu/vulnerabilities/ — вывод покажет статус по каждой уязвимости и включённый механизм защиты (или его отсутствие, если CPU не подвержен конкретной проблеме).

Стоит ли отключать митигации ради производительности базы данных?

Только на bare metal сервере, где вы полностью контролируете, какой код выполняется, и осознанно принимаете риск. На виртуализированных средах с недоверенными соседями отключать митигации не рекомендуется — это открывает потенциальный канал утечки данных между виртуальными машинами.

Почему именно базы данных страдают от митигаций больше других сервисов?

Потому что СУБД активно работает через системные вызовы: чтение/запись на диск, fsync для WAL, семафоры для блокировок, работа с разделяемой памятью. Каждый переход в ядро становится немного дороже из-за KPTI и retpoline, а у СУБД таких переходов очень много в единицу времени.

Можно ли снизить влияние митигаций без их отключения?

Да — за счёт архитектурных изменений: пулинг соединений, снижение частоты fsync там, где это допустимо, батчинг операций, укрупнение транзакций. Также помогает переход на CPU с аппаратной поддержкой части защитных механизмов — на них накладные расходы программных митигаций ниже.

Как не пропустить такую деградацию в будущем?

Ведите отдельный мониторинг %sys и context switches, а не только общий CPU load, и прогоняйте контрольный бенчмарк на staging перед каждым плановым обновлением ядра на проде — это дешевле, чем потом две недели искать причину постфактум.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →