В три часа ночи мне пришло сообщение от мониторинга: «backup job: running, duration 03:47:12». В обычную ночь этот job занимает сорок минут. Я открыла ноутбук с чашкой чая, которая по всем законам жанра должна была быть кофе, но в три часа ночи организм уже сдаётся и просит просто что-то тёплое.
Симптом
Backup делался через pg_dump в директорию на отдельном томе, затем архив копировался в холодное хранилище. В логах не было ошибок — job просто шёл. Медленно. Очень медленно. Первая мысль — диск. Вторая мысль — сеть. Обе оказались частично правы, но не в том смысле, в котором я думала.
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-инстанс именно после этого случая, и он уже дважды находил проблему раньше, чем она стала бы критичной.
Небольшой скрипт для проверки свежести бэкапа
#!/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 наконец завершился, дамп оказался цел, а я успела написать пол-абзаца в этот блог прежде, чем уснуть за ноутбуком. Собака этого не оценила — по её мнению, в пять утра положено гулять, а не работать.