MAATRIX / Блог / База замирала на 15 секунд: разбор фонового TRIM на всём разделе

База замирала на 15 секунд: разбор фонового TRIM на всём разделе

MAATRIX

Раз в какое-то время база данных на проде замирала целиком: все запросы зависали примерно на 15 секунд, а потом всё возвращалось к норме, будто ничего не было. Логи самой базы при этом молчали — ни ошибок, ни предупреждений, ни намёка на причину. Такая загадка обычно решается не в логах приложения, а на уровне диска — и в этом случае виновником оказался фоновый TRIM, запускавшийся по расписанию для всего раздела целиком.

Симптом: база то работает, то замирает на 15 секунд

Картина была одинаковой каждый раз. База отвечала нормально, метрики задержек запросов держались в привычном диапазоне — и вдруг все соединения к базе одновременно "зависали". Не отваливались с ошибкой, не рвались по таймауту — просто переставали получать ответ. Через примерно 15 секунд всё возобновлялось само, будто база сделала глубокий вдох и продолжила работать как ни в чём не бывало.

Ключевая деталь, которая в итоге и привела к разгадке: замирания происходили не хаотично, а с определённой регулярностью — не "иногда, когда повезёт", а через похожие интервалы, в примерно одно и то же время суток. Именно повторяемость "то нормально работает, то замирает на одинаковый по ощущениям интервал" наводит на мысль о какой-то запланированной задаче в системе, а не о случайной аномалии нагрузки или сетевой проблеме. Это общий принцип диагностики: если проблема регулярна по времени — ищите cron, systemd-таймер или другой планировщик, а не пытайтесь объяснить её колебаниями трафика.

На графиках мониторинга это выглядело как резкий провал throughput — количество обработанных запросов в секунду падало почти до нуля, а активные соединения к базе копились в состоянии ожидания. При этом CPU базы не был перегружен, память в норме, сетевых потерь не зафиксировано. Всё указывало на то, что база сама ничего не делает — она просто ждёт ответа от диска.

Почему логи базы данных молчали

Первая реакция при таком инциденте — открыть логи базы и искать ошибку. Но логи молчали не потому, что база "не заметила" проблему, а потому что с точки зрения СУБД ничего экстраординарного не произошло: запрос на запись или чтение просто выполнялся дольше обычного. Это не ошибка соединения, не deadlock, не нехватка памяти — это обычная операция ввода-вывода, которая заняла аномально много времени.

Для большинства баз данных (PostgreSQL, MySQL/InnoDB и других) долгий диск — это не повод писать в лог ошибку. В логе медленных запросов (slow query log в MySQL или log_min_duration_statement в PostgreSQL) такие запросы могли попасть, если такое логирование было включено и порог был выставлен достаточно низко — но и тогда лог покажет лишь "запрос выполнялся N секунд", без указания причины. Причина лежит на уровень ниже — на уровне блочного устройства, а СУБД туда не заглядывает.

Более информативную картину дают не логи базы, а системные метрики самого сервера:

iostat -x 1

Во время замирания в выводе iostat видно резкий скачок %util (утилизация устройства близка к 100%) и рост await (среднее время ожидания операции вводом-выводом) — при этом r/s и w/s (число реальных операций чтения/записи в секунду) могут просесть, потому что диск занят чем-то другим, а не обслуживанием обычных запросов приложения. Ещё один полезный индикатор — вывод dmesg и системного журнала: если диск на 15 секунд действительно "завис" для всех процессов, а не только для базы, это уже не про саму СУБД, а про блочное устройство целиком.

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

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

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

Что такое TRIM и зачем он нужен SSD

Чтобы понять корень проблемы, нужно вспомнить, как SSD-накопитель работает изнутри. В отличие от жёсткого диска, SSD не может просто перезаписать блок данных поверх старого — сначала блок нужно очистить (стереть), и только потом в него можно писать заново. Проблема в том, что накопитель на аппаратном уровне не знает, какие блоки данных файловая система считает "удалёнными" — с точки зрения SSD все ранее записанные блоки одинаково заняты, даже если файл, которому они принадлежали, давно удалён пользователем.

Команда TRIM (в спецификации ATA она называется DSM с атрибутом Trim, в NVMe — Deallocate) решает именно эту проблему: она сообщает контроллеру диска, какие блоки данных больше не используются файловой системой и могут быть освобождены. После получения TRIM диск может заранее стереть эти блоки в фоне, до того как в них понадобится что-то записать — это часть внутренней сборки мусора (garbage collection) контроллера SSD.

Без TRIM SSD со временем не понимает, что часть занятого пространства на самом деле свободна, и при новой записи вынужден выполнять более медленный цикл "прочитать блок — стереть — записать" вместо простой записи в уже подготовленный чистый блок. Это одна из причин деградации скорости записи на SSD по мере заполнения и старения — механизм связан с тем же wear leveling, о котором можно почитать подробнее в статье про то, как SSD умирает медленно. Так что сама по себе идея TRIM полезна и правильна — вопрос в том, как именно он запускается.

Периодический TRIM для всего раздела vs непрерывный TRIM

Здесь и кроется ключевое различие, которое объясняет инцидент. Есть два принципиально разных способа сообщать диску о свободных блоках:

Периодический (batched/scheduled) TRIM — команда fstrim запускается по расписанию (обычно через cron или systemd-таймер fstrim.timer) и обрабатывает весь раздел целиком за один проход. Такой подход распространён именно потому, что он предсказуем и прост в администрировании: запустили одну команду — получили полностью "почищенный" раздел.

Непрерывный (continuous) TRIM — вместо разовой большой операции файловая система или драйвер накопителя отправляет команды TRIM малыми порциями постоянно, сразу по мере удаления файлов. В Linux это включается опцией монтирования discard (или, в более новых версиях, асинхронный вариант discard=async в ext4/XFS), при которой каждая операция удаления сама по себе триггерит небольшой TRIM для освобождённых блоков — без единой массивной операции по всему разделу.

Разница на практике огромная. Периодический TRIM для маленького раздела на быстром NVMe может отработать почти незаметно. Но для большого раздела (сотни гигабайт или несколько терабайт), особенно если на нём давно копились изменения, единоразовый проход fstrim по всему разделу может занять заметное время — и всё это время диск занят обработкой TRIM, а не обычными запросами чтения/записи от приложений. Реальная задержка обычных операций в этот момент растёт, потому что очередь команд накопителя забита служебной работой.

Способ TRIMКак работаетРиск для продакшена
Периодический (fstrim, весь раздел)Разовый проход по расписанию, обрабатывает весь диск целикомДиск занят TRIM продолжительное время, обычные I/O-запросы ждут
Непрерывный (discard/discard=async)Малые порции TRIM сразу при удалении файловНагрузка размазана во времени, резких провалов почти нет

Именно поэтому база данных, которая почти всегда что-то пишет (WAL, журналы транзакций, обновления индексов), ощущает периодический полный TRIM как настоящее зависание: диск физически занят и реально медленнее отвечает на запросы именно в этот интервал.

Хронология расследования: от закономерности к cron

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

crontab -l
sudo crontab -l -u root
ls /etc/cron.d/
systemctl list-timers --all

Проверка списка таймеров и cron-заданий обнаружила именно то, что искали: периодический запуск fstrim для всего раздела, время срабатывания которого совпадало с моментами замирания базы — с точностью до минуты. Это и была искомая закономерность: не совпадение, а прямая причинно-следственная связь.

systemctl status fstrim.timer
systemctl cat fstrim.timer

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

sudo fstrim -v /var/lib/mysql
iostat -x 1

Если во время ручного запуска fstrim наблюдается та же картина — рост %util и await при просадке отклика от базы, — причина подтверждена. В этом инциденте именно так и произошло: ручной запуск fstrim на разделе с данными базы воспроизвёл симптом один в один, включая длительность зависания.

Корневая причина: batched fstrim на весь раздел разом

Корневая причина инцидента — это не какая-то поломка и не деградация диска, а конфигурационное решение: TRIM был настроен как периодическая (batched/scheduled) операция для всего раздела целиком, вместо непрерывного (continuous) TRIM, встроенного в саму файловую систему или драйвер и выполняемого малыми порциями постоянно.

Периодический TRIM сам по себе не является ошибкой — это распространённый и часто рекомендуемый способ настройки, особенно там, где непрерывный discard считался рискованным для производительности на старых версиях ядра или конкретных контроллерах SSD. Проблема возникает конкретно тогда, когда:

  • раздел достаточно большой, и полный проход TRIM занимает заметное время;
  • на разделе работает сервис, чувствительный к задержкам ввода-вывода (в первую очередь — база данных);
  • расписание TRIM не согласовано с реальными паттернами нагрузки на этот сервис.

В данном случае все три условия совпали: большой раздел с данными базы, сама база активно и постоянно пишет (журналы транзакций, обновления страниц), а расписание fstrim было настроено без оглядки на то, что именно на этом разделе крутится продакшен-нагрузка.

Что делать: практические шаги

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

1. Рассмотреть переход на непрерывный TRIM там, где это поддерживается. Вместо периодического полного fstrim можно включить опцию монтирования discard (или более безопасный асинхронный вариант discard=async, если версия ядра и файловая система его поддерживают) — тогда TRIM будет выполняться малыми порциями сразу при удалении файлов, а не одним массивным блоком по расписанию:

# пример строки в /etc/fstab
UUID=xxxx-xxxx /var/lib/mysql ext4 defaults,discard=async 0 2

После изменения опций монтирования раздел нужно перемонтировать (mount -o remount /var/lib/mysql) или применить после следующей перезагрузки — и обязательно проверить, что накопитель и драйвер контроллера действительно поддерживают такой режим без побочных эффектов на конкретном железе.

2. Если периодический TRIM всё же нужен — планировать его на время минимальной нагрузки на базу. Не оставлять расписание по умолчанию "как есть", а явно перенести запуск fstrim.timer (через OnCalendar= в unit-файле или override) на окно, когда база данных получает минимум трафика — например, глубокой ночью по данным реального графика нагрузки, а не наугад:

sudo systemctl edit fstrim.timer
[Timer]
OnCalendar=
OnCalendar=*-*-* 04:30:00

3. Тестировать влияние конкретной настройки TRIM на реальную задержку критичных сервисов до внедрения в продакшен, а не после обнаружения проблемы. Прежде чем менять политику TRIM на боевом сервере, стоит воспроизвести нагрузку и замерить, как именно диск реагирует на fstrim под похожей рабочей нагрузкой — на тестовом окружении или в окно обслуживания, с параллельным снятием метрик iostat и задержек самой базы. Здесь пригодится честный замер диска инструментом fio, о котором подробнее рассказано в статье про замер диска fio — тот же подход к тестированию применим и для проверки поведения при TRIM, а не только для базового бенчмарка.

Отдельно стоит настроить мониторинг, который будет ловить резкие провалы отклика диска до того, как об этом сообщат пользователи — простое отслеживание await и %util из iostat в системе мониторинга сервера закрывает большую часть таких сюрпризов на будущее. Если нужен общий разбор того, почему скорость диска вообще так критична именно для баз данных, это подробно разобрано в статье про важность скорости диска для баз данных.

Похожий по формату разбор регулярной, на первый взгляд необъяснимой блокировки базы — с другой корневой причиной — можно посмотреть в статье про базу, которая вставала на ровном месте: полезно сравнить, как по-разному может выглядеть на графиках, казалось бы, один и тот же симптом "база зависла".

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

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

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

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

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

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

Как отличить зависание базы из-за TRIM от обычной блокировки (lock/deadlock) внутри самой СУБД?

Блокировка внутри базы обычно видна в системных представлениях самой СУБД — например, в pg_stat_activity у PostgreSQL или в SHOW ENGINE INNODB STATUS у MySQL можно увидеть конкретные заблокированные запросы и кто их блокирует. Если же зависают вообще все запросы одновременно, включая простые чтения, не связанные друг с другом, а системные представления не показывают конкретной блокирующей транзакции — это повод смотреть не внутрь базы, а на диск и его метрики (iostat, dmesg).

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

Да, TRIM — это не опциональная "оптимизация для галочки", а механизм, без которого производительность записи на SSD со временем деградирует, потому что контроллер накопителя не понимает, какие блоки на самом деле свободны. Вопрос не в том, нужен ли TRIM, а в том, каким способом он выполняется — периодически большим блоком или непрерывно малыми порциями.

Как проверить, поддерживает ли конкретный SSD и файловая система непрерывный TRIM (discard)?

Можно посмотреть поддержку TRIM на уровне блочного устройства командой lsblk --discard (там же видны параметры DISC-GRAN и DISC-MAX, отличные от нуля при поддержке), а факт применения опции монтирования проверить через findmnt -no options /var/lib/mysql — там должно быть видно discard или discard=async, если опция реально включена и применилась.

Если сервер арендованный (VPS), а не физический — можно ли вообще управлять расписанием TRIM?

На большинстве VPS с виртуальными дисками расписание fstrim/fstrim.timer настраивается точно так же, как на физическом сервере, — это конфигурация гостевой операционной системы, а не гипервизора. Но конкретную поддержку discard на уровне виртуального диска стоит уточнить у площадки, где арендован сервер, потому что она зависит от того, как виртуальный диск проброшен на уровне гипервизора.

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

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

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