Тысяча снапшотов и диск, который кончился в один день
Сервер с базой данных работал стабильно почти три года, и вдруг за одни сутки диск заполнился под ноль: PostgreSQL перестал принимать записи, приложение посыпалось пятисотыми ошибками, а разбор ситуации по горячим следам ничего не находил — таблицы не выросли, логи в норме, бэкапы штатные. Виновником оказались снапшоты тома с данными: их накопилась почти тысяча штук, и ни один никогда не удалялся. Рассказываю, как мы это нашли и почему инкрементальные снапшоты — штука коварная именно в долгую.
Содержание
- Как накопилась тысяча снапшотов без единого разговора о лимите
- День, когда диск закончился без предупреждения
- Гипотезы, которые отбросили
- Реальная причина: как устроены инкрементальные снапшоты
- Диагностика и разгребание: сколько места съели снапшоты и как его вернули
- Что изменили: политика хранения и раздельный мониторинг снапшотов
Как накопилась тысяча снапшотов без единого разговора о лимите
Три года назад перед рискованным обновлением схемы кто-то настроил простой cron-скрипт: раз в сутки в 03:00 он снимал ZFS-снапшот датасета с данными PostgreSQL — zfs snapshot pool/pgdata@auto-$(date +%F). Идея была здравой: если миграция пойдёт не так, откатиться на снапшот быстрее, чем поднимать бэкап. Обновление прошло нормально, снапшот тогда так и не понадобился — но и скрипт из cron никто не убрал.
Дальше он просто работал сам по себе. Никто не писал вторую половину — скрипт, который удаляет снапшоты старше N дней. В планах это, конечно, «доделать позже» значилось, но команда сменилась, задача потерялась в бэклоге, а cron исправно тикал каждую ночь. За неполные три года набежало 1071 снапшотов — мы посчитали точно, когда разбирались:
$ zfs list -t snapshot -o name -H | grep '^pool/pgdata@auto-' | wc -l
1071
Каждый снапшот в отдельности почти ничего не весил. Свежий снапшот сразу после создания показывает единицы мегабайт занятого места — ровно потому, что в момент снятия он не хранит копию данных, а только точку отсчёта, от которой считаются последующие изменения. Именно эта мнимая дешевизна и убаюкивает: если бы кто-то раз в месяц открывал zfs list -t snapshot и видел «40 МБ, 38 МБ, 45 МБ» в столбце USED, тревогу бить не стал бы. Настоящая цена копится не в одном снапшоте, а в их количестве и возрасте, а увидеть это можно только сложив все строки, а не глядя на них по одной.
День, когда диск закончился без предупреждения
Спусковым крючком стала плановая доработка: разработчики добавили в основную таблицу заказов новую колонку с индексом и запустили backfill-скрипт, который построчно проставлял значения для нескольких десятков миллионов уже существующих записей. С точки зрения приложения — рутинная миграция, которую тестировали на стейджинге, и там всё прошло гладко (там снапшотов было всего три).
На проде backfill стартовал утром и уже к обеду df -h показывал стремительный рост занятого места на разделе с ZFS-пулом — на несколько процентов в час, хотя обычно прирост данных за сутки укладывался в десятые доли процента. К 15:00 пул был заполнен на 100%, PostgreSQL начал писать в лог could not extend file: No space left on device, а сам backfill-скрипт завис на середине с открытой транзакцией.
Первая реакция — посмотреть, что именно раздулось. Но du -sh /var/lib/postgresql/* и размер самой базы через \l+ в psql показывали рост в разумных пределах, никак не объясняющий внезапную нехватку десятков гигабайт свободного места за несколько часов. Диск как будто съел сам себя.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГипотезы, которые отбросили
Прежде чем дошли до снапшотов, проверили несколько версий, каждая по 10-20 минут:
- WAL раздулся из-за долгой транзакции. Посмотрели
pg_wal— каталог действительно подрос, но не настолько, чтобы объяснить разницу. К тому же после отката транзакции WAL вернулся к обычному размеру, а свободное место не появилось. - Backfill-скрипт писал временные файлы. Проверили
pg_stat_activityиtemp_files/temp_bytesизpg_stat_database— операция была построчной, без больших сортировок, temp-файлов почти не было. - Логи приложения ушли в бесконечный цикл. Быстро проверили
journalctlи логи в/var/log— размер обычный, ротация logrotate отработала как всегда. - Раздулся индекс или bloat таблицы. Посмотрели
pg_total_relation_sizeдля основной таблицы — размер вырос, но на объём, соответствующий добавленной колонке и новому индексу, не в разы больше.
Ни одна версия не объясняла масштаб: пропавшие десятки гигабайт были явно больше, чем сумма всех изменений, которые видело само приложение. Это и стало зацепкой — раз видимые данные выросли не так сильно, но место всё равно кончилось, значит, куда-то уходит что-то, чего приложение не видит вообще.
Реальная причина: как устроены инкрементальные снапшоты
Большинство систем снапшотов на уровне блочного хранилища — ZFS, LVM thin-provisioning, снапшоты дисков у облачных провайдеров — работают по инкрементальному принципу через copy-on-write. Снапшот не копирует данные целиком в момент создания. Вместо этого он фиксирует состояние на текущий момент, а дальше, когда данные под ним меняются, старая версия блока не перезаписывается на месте, а сохраняется — новая версия пишется в новое место, а снапшот продолжает ссылаться на старую. Поэтому только что созданный снапшот действительно занимает считаные мегабайты: изменений с момента его создания ещё не было.
Ловушка в том, что происходит дальше. Если снапшоты никогда не удаляются, каждый из них продолжает держать все блоки, которые были актуальны на момент его создания и с тех пор были перезаписаны хоть раз. Чем дольше снапшот живёт и чем больше данных меняется под ним, тем больше уникальных «замороженных» блоков он на себе тащит. А поскольку снапшотов было больше тысячи, растянутых на три года, суммарно они держали блоки практически от всей истории изменений таблиц за это время — включая давно удалённые и многократно перезаписанные строки, которые в живых данных давно не существуют ни в каком виде.
Backfill, который переписал десятки миллионов строк за один день, резко увеличил объём изменений именно в тот день. Каждая перезаписанная строка означала: новый блок данных плюс необходимость сохранить старую версию блока ради ближайших снапшотов, которые на неё ссылались. При обычном темпе изменений в 100-200 МБ в день пул несколько лет балансировал у разумного порога заполнения. Но массовая перезапись за одни сутки создала объём изменений, сопоставимый с неделями обычной работы — и именно этого запаса свободного места, съеденного годами копившихся снапшотов, не хватило.
Ключевая мысль здесь простая и именно её стоит унести из этой истории: количество снапшотов без политики очистки растёт неограниченно, и суммарный объём, который они удерживают, зависит не от того, сколько «весит» база сейчас, а от того, сколько данных изменилось за всё время их жизни. Это накопленный долг, который молчит до тех пор, пока не случится день с аномально большим объёмом изменений — и тогда счёт предъявляется сразу.
Диагностика и разгребание: сколько места съели снапшоты и как его вернули
Первым делом требовалось понять пропорцию: сколько места в пуле занимают реально живые данные, а сколько — снапшоты. У ZFS для этого есть отдельные свойства датасета, которые считают именно это:
$ zfs get -o value usedbydataset,usedbysnapshots pool/pgdata
47.2G
612.8G
Живые данные — 47 ГБ, снапшоты — почти 613 ГБ, при общем размере пула в 660 ГБ. То есть 93% занятого места было историей изменений, которая никому не была нужна. Дальше отсортировали снапшоты по возрасту и по размеру, чтобы понять, откуда конкретно набрался объём:
$ zfs list -t snapshot -o name,used,creation -s creation pool/pgdata | head -5
NAME USED CREATION
pool/pgdata@auto-2023-09-14 8.90G Thu Sep 14 3:00 2023
pool/pgdata@auto-2023-09-15 340M Fri Sep 15 3:00 2023
pool/pgdata@auto-2023-09-16 298M Sat Sep 16 3:00 2023
Самые старые снапшоты держали больше всего уникальных блоков — что логично, они пережили самую длинную историю изменений под собой. Удалять их нужно было по порядку от старых к новым, а не абы как: пространство освобождается только тогда, когда блок больше не нужен ни одному живому снапшоту, а из-за цепочки ссылок это иногда становится понятно только после удаления предыдущего звена. Чтобы не удалять по одному 1071 раз, использовали диапазонное удаление ZFS:
$ zfs destroy pool/pgdata@auto-2023-09-14%auto-2024-06-30
Синтаксис с % удаляет весь непрерывный диапазон снапшотов от первого до второго включительно за одну операцию. После первой такой чистки пул отдал около 200 ГБ, PostgreSQL смог снова писать на диск, зависшую транзакцию backfill-скрипта откатили и перезапустили порциями — уже с явным контролем свободного места между батчами. Оставшиеся снапшоты чистили аналогичными диапазонами, оставив только последние 30 дней.
Что изменили: политика хранения и раздельный мониторинг снапшотов
Экстренная чистка вернула место, но не решала главного — без правил снапшоты снова начнут копиться с той же скоростью. Дописали то, что нужно было сделать три года назад:
- Явная политика хранения. Договорились на конкретной схеме: почасовые снапшоты хранятся 48 часов, дневные — 30 дней, недельные — 12 недель. Старше — удаляются автоматически без исключений. Такую схему ротации типа «дед-отец-сын» проще всего доверить готовому инструменту, а не самописному скрипту с cron — мы перешли на
sanoid, который берёт на себя и создание снапшотов по расписанию, и их вычищение по правилам retention из одного конфига. - Мониторинг места, занятого именно снапшотами, отдельно от данных. Раз в час systemd-таймер снимает
usedbydatasetиusedbysnapshotsи пишет их как отдельные метрики в мониторинг, с алертом, если доля снапшотов превышает 40% от занятого места пула. До инцидента мониторили только общий процент заполнения диска — а он рос настолько медленно на фоне общего размера пула, что не пересекал привычные пороги в 80-85% почти до самого конца. - Проверка свободного места перед массовыми операциями. Для миграций и backfill-скриптов, которые переписывают заметную долю таблицы, теперь обязателен предварительный расчёт: сколько данных предстоит изменить и есть ли в пуле физический запас с учётом текущих снапшотов, а не только общий процент свободного места.
- Ревизия остальных серверов. Проверили все хосты с похожими cron-скриптами снапшотов — нашли ещё два места с той же болезнью: снапшоты создавались, но не удалялись. Перевели оба на ту же схему с
sanoid.
Отдельно проговорили с командой, что снапшоты — не замена бэкапу, а рабочий инструмент отката, который требует такого же явного жизненного цикла, как и любые другие данные на диске. Мы уже разбирали, во сколько выливается забытая цепочка снапшотов в статье про снапшоты, которые копятся годами, а вопрос, сколько снапшотов вообще разумно держать и во что обходится каждый следующий, подробно раскрыт в материале про цену снапшота в IOPS и месте. Если у вас на серверах LVM, а не ZFS, механика та же самая, только риски и команды другие — про них есть отдельный разбор LVM-снапшотов и рисков их расширения. А базовую настройку алертов по свободному месту, без которой вся эта история осталась бы незамеченной ещё дольше, можно взять из статьи про мониторинг диска на VPS.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему zfs list -t snapshot не сразу показал масштаб проблемы?
Он показывал всё честно — просто 1071 строка с небольшими цифрами в столбце USED не производит впечатления, пока не сложить их вместе или не посмотреть на агрегированное свойство датасета usedbysnapshots. Отдельные снапшоты выглядят безобидно именно потому, что каждый из них хранит только свои уникальные блоки, а не полную копию.
Можно ли было удалить только самые старые снапшоты, не трогая остальные, и решить проблему быстрее?
Да, и именно так и делали — диапазонное удаление от самых старых снапшотов освобождает больше всего места, потому что они держат самую длинную историю изменений. Но частичная чистка без изменения политики хранения лечит симптом на один раз, а не убирает причину — без ротации счётчик снапшотов снова пойдёт вверх.
Такая же проблема бывает с LVM thin-provisioning или облачными снапшотами дисков, а не только с ZFS?
Да, принцип инкрементального хранения через copy-on-write общий для большинства систем снапшотов — LVM thin-снапшоты, снапшоты дисков в облаках, снапшоты виртуальных машин в гипервизорах. Конкретные команды диагностики и удаления отличаются, но логика «снапшот без ротации растёт бесконечно» одна и та же везде.
Сколько снапшотов — это уже много?
Единого числа нет, всё зависит от интенсивности изменений данных под ними и от объёма свободного места в пуле. Ориентир, а не точная цифра: если снапшотов больше нескольких десятков и часть из них старше пары недель без явной причины, стоит проверить долю места, которую они занимают, и завести политику ротации, даже если пока свободного места достаточно.
Помогло бы просто увеличить диск заранее, не разбираясь в причине?
Временно — да, но проблема осталась бы нерешённой: без политики хранения снапшоты продолжали бы копиться с той же скоростью, и через какое-то время та же ситуация повторилась бы уже на большем диске. Больший диск покупает время на то, чтобы навести порядок, но не заменяет саму политику ротации.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →