Ночной backup, который занял четыре часа

В три часа ночи мне пришло сообщение от мониторинга: «backup job: running, duration 03:47:12». В обычную ночь этот job занимает сорок минут. Я открыла ноутбук с чашкой чая, которая по всем законам жанра должна была быть кофе, но в три часа ночи организм уже сдаётся и просит просто что-то тёплое.

Симптом

Backup делался через pg_dump в директорию на отдельном томе, затем архив копировался в холодное хранилище. В логах не было ошибок — job просто шёл. Медленно. Очень медленно. Первая мысль — диск. Вторая мысль — сеть. Обе оказались частично правы, но не в том смысле, в котором я думала.

bash
iostat -xz 5
# %util на томе с дампом: 97-100% на протяжении всего job
# await: 40-90 ms, что для этого диска ненормально высоко

Что нашлось на самом деле

Диск был перегружен — но не самим бэкапом. Параллельно с job'ом ночью отрабатывал автовакуум на нескольких крупных таблицах, плюс раз в сутки запускался процесс агрегации отчётов, который писал временные файлы на тот же том. Три процесса конкурировали за один и тот же диск, и ни один мониторинг раньше не связывал их вместе — метрики были, но каждая жила в своей отдельной панели.

Главная ошибка

У нас был мониторинг длительности backup job'а и отдельно — мониторинг дисковой нагрузки. Но не было дашборда, который показывал бы оба графика рядом. Проблему увидели только тогда, когда наложили их вручную друг на друга.

Logical и physical backup — не взаимозаменяемые вещи

pg_dump делает логический бэкап: он читает данные через обычные SQL-запросы и сохраняет их в переносимом формате. У этого подхода есть удобства — можно восстановить одну таблицу, можно переносить дамп между версиями PostgreSQL — но есть и цена: дамп большой базы читает по сути всю базу целиком, создавая заметную нагрузку на I/O и CPU на время всей операции.

ПодходЧто этоПлюсыМинусы
Logical (pg_dump)SQL-совместимый дамп данныхПортируемость, восстановление части данныхМедленнее на больших базах, нагружает CPU/I/O
Physical (pg_basebackup)Копия файлов кластера на уровне блоковБыстрое восстановление, меньше нагрузки при снятииТребует ту же версию PostgreSQL, восстановление только целиком
Continuous archiving (WAL)Непрерывная отправка журналов транзакцийPoint-in-time recovery, минимальный RPOТребует места под WAL-архив и отдельной настройки

После этого случая мы не отказались от pg_dump полностью — он всё ещё удобен для регулярных переносимых снимков — но добавили физическое архивирование через pg_basebackup и непрерывную отправку WAL в отдельное хранилище. Это разделило нагрузку по времени и дало возможность point-in-time recovery, а не только «восстановиться на момент последнего дампа».

RPO и RTO — не только на бумаге

До этого случая цифры RPO (recovery point objective — сколько данных мы готовы потерять) и RTO (recovery time objective — как быстро мы обязаны восстановиться) существовали только в документе, который никто не перечитывал полгода. После инцидента мы фактически замерили, сколько времени у нас ушло бы на восстановление, если бы этот backup оказался единственным вариантом — и цифра оказалась заметно больше той, что была записана в документе.

Почему это важно не только для backup

PostgreSQL сам по себе не изолирует I/O между разными своими подсистемами и внешними процессами на том же томе. Если WAL, данные таблиц, временные файлы сортировки и бэкапы делят один диск — при пиковой нагрузке любой из них может «съесть» пропускную способность у остальных. В нашем случае решение было не героическим, а скучным:

  • вынести директорию для дампов на отдельный физический том;
  • сдвинуть окно автовакуума для тяжёлых таблиц на менее конфликтное время через autovacuum_vacuum_cost_delay;
  • перенести агрегацию отчётов на два часа раньше, до старта backup job'а;
  • добавить непрерывную отправку WAL, чтобы не зависеть от единственного ночного окна.

Как теперь проверяем состояние backup

Помимо факта «job завершился успешно», мы стали проверять несколько дополнительных вещей:

ПроверкаЗачем
Длительность job'а относительно скользящего среднего за 14 днейАномальный рост длительности виден раньше, чем таймаут
Размер полученного дампаРезкое падение размера — признак неполного бэкапа
Тестовое восстановление на staging раз в неделюБэкап, который нельзя восстановить, бесполезен
I/O диска во время job'аРанний сигнал конкуренции за ресурсы
Непрерывность цепочки WAL-архивовРазрыв в архиве ломает point-in-time recovery
Напоминание

Backup без проверенного восстановления — это файл, в существование которого вы верите на слово. Мы настроили еженедельный автоматический restore на отдельный staging-инстанс именно после этого случая, и он уже дважды находил проблему раньше, чем она стала бы критичной.

Небольшой скрипт для проверки свежести бэкапа

bash
#!/usr/bin/env bash
LATEST=$(find /backups -name "*.dump" -mtime -1 | wc -l)
if [ "$LATEST" -eq 0 ]; then
  echo "No fresh backup in the last 24 hours" | mail -s "Backup alert" oncall
fi

Ничего революционного — просто проверка, что свежий файл существует и его размер не подозрительно мал. Иногда самые полезные скрипты и есть самые скучные, и это ровно тот случай.

Что я вынесла из этой ночи

  • Мониторинг отдельных метрик — это не мониторинг системы. Метрики нужно уметь смотреть вместе.
  • Диск — общий ресурс, даже если кажется, что у каждого процесса «своя» задача.
  • Backup без регулярного тестового восстановления — это половина решения.
  • RPO и RTO стоит проверять на практике хотя бы раз в квартал, а не только записывать в документ.

К пяти утра job наконец завершился, дамп оказался цел, а я успела написать пол-абзаца в этот блог прежде, чем уснуть за ноутбуком. Собака этого не оценила — по её мнению, в пять утра положено гулять, а не работать.