lenec ru

← все посты

Бэкап Postgres на S3: Selectel и Yandex Object Storage

16K

База данных без бэкапа — это база, которую вот-вот потеряешь. Я делаю бэкапы Postgres на S3-совместимое хранилище уже несколько лет, и за это время восстанавливалась с них раза три-четыре. Расскажу свою рабочую схему: как делать pg_dump, как заливать в Selectel или Yandex Object Storage, как ротация и проверка.

Контекст: Postgres 16 на VPS, Ubuntu 24.04. Хранилище — Selectel S3 или Yandex Object Storage. Оба S3-совместимы, разница только в endpoint и ключах.

Что бэкаплю

  • Логический дамп через pg_dump. Удобный, переносимый, можно восстанавливать частично. Минус — медленный на больших базах.
  • WAL-архив для восстановления на любую точку времени (PITR). Использую только на ответственных кластерах.
  • Файловая копия данных через pg_basebackup для быстрого восстановления крупного кластера.

Для большинства небольших проектов хватает первого пункта. Расскажу про него подробно, остальные — кратко.

pg_dump раз в день

Самая простая схема: ежедневно дампим всю базу, заливаем в S3. Скрипт /opt/scripts/pg-backup.sh:

#!/bin/bash
set -euo pipefail

DATE=$(date -u +%FT%H%M%SZ)
HOST=${PGHOST:-127.0.0.1}
PORT=${PGPORT:-5432}
USER=${PGUSER:-postgres}
DB=${PGDATABASE:-app}
BUCKET=my-backups
DIR=/tmp/pg-backup

mkdir -p $DIR

# Глобальные объекты (роли, права)
pg_dumpall --globals-only -h $HOST -p $PORT -U $USER > $DIR/globals-$DATE.sql

# Сама база, custom-формат
pg_dump -h $HOST -p $PORT -U $USER -F c -Z 6 -f $DIR/$DB-$DATE.dump $DB

# Чексумма
sha256sum $DIR/$DB-$DATE.dump > $DIR/$DB-$DATE.dump.sha256

# Заливка на S3
aws --endpoint-url https://s3.storage.selcloud.ru \
  s3 cp $DIR/$DB-$DATE.dump s3://$BUCKET/postgres/daily/
aws --endpoint-url https://s3.storage.selcloud.ru \
  s3 cp $DIR/$DB-$DATE.dump.sha256 s3://$BUCKET/postgres/daily/
aws --endpoint-url https://s3.storage.selcloud.ru \
  s3 cp $DIR/globals-$DATE.sql s3://$BUCKET/postgres/daily/

# Локальная очистка (не держим больше двух последних)
find $DIR -name "*.dump" -mtime +2 -delete
find $DIR -name "*.sha256" -mtime +2 -delete
find $DIR -name "*.sql" -mtime +2 -delete

echo "Backup ${DB}-${DATE}.dump uploaded"

Что важно:

  • -F c — custom-формат pg_dump, поддерживает параллельное восстановление и выборочный restore таблиц.
  • -Z 6 — gzip-сжатие на лету. Для текстовых данных даёт x3-5 уменьшение.
  • SHA256-чексумма заливается рядом. На восстановлении первое, что я проверяю.
  • pg_dumpall --globals-only — отдельный файл с ролями и пермишенами. Без него на чистом сервере не получится восстановить пользователей.

S3-credentials и aws CLI

Для Selectel и Yandex используется aws CLI с правильным endpoint. Кладу credentials в файл ~/.aws/credentials для root-юзера, который запускает скрипт:

[default]
aws_access_key_id = <ACCESS_KEY>
aws_secret_access_key = <SECRET_KEY>
region = ru-1

В Selectel ключи генерятся в кабинете S3 → «Сервисный пользователь». В Yandex — через сервисный аккаунт с ролью storage.editor. Endpoint:

  • Selectel: https://s3.storage.selcloud.ru
  • Yandex: https://storage.yandexcloud.net

cron

Запуск раз в сутки в 3 ночи через системный cron:

# /etc/cron.d/pg-backup
0 3 * * * postgres /opt/scripts/pg-backup.sh >> /var/log/pg-backup.log 2>&1

Запускаю от пользователя postgres, чтобы не возиться с паролями к БД (peer-auth подхватит). Логи отдельным файлом, на ротацию ставлю logrotate.

На systemd-таймерах это выглядит чище. Юнит-сервис:

# /etc/systemd/system/pg-backup.service
[Unit]
Description=Postgres backup to S3

[Service]
Type=oneshot
User=postgres
ExecStart=/opt/scripts/pg-backup.sh
# /etc/systemd/system/pg-backup.timer
[Unit]
Description=Daily Postgres backup

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now pg-backup.timer
sudo systemctl list-timers pg-backup.timer

Persistent=true — если сервер был выключен в плановое время, бэкап догонится при старте.

Ротация в S3

Не самой задаче скрипта удалять старые копии. Это делает Lifecycle policy на bucket:

<LifecycleConfiguration>
  <Rule>
    <ID>daily-30days</ID>
    <Status>Enabled</Status>
    <Filter><Prefix>postgres/daily/</Prefix></Filter>
    <Expiration><Days>30</Days></Expiration>
  </Rule>
  <Rule>
    <ID>weekly-90days</ID>
    <Status>Enabled</Status>
    <Filter><Prefix>postgres/weekly/</Prefix></Filter>
    <Expiration><Days>90</Days></Expiration>
  </Rule>
</LifecycleConfiguration>

Применяется через aws CLI:

aws --endpoint-url https://s3.storage.selcloud.ru \
  s3api put-bucket-lifecycle-configuration \
  --bucket my-backups \
  --lifecycle-configuration file://lifecycle.json

Я держу: последние 30 ежедневных, последние 90 еженедельных, последние 12 ежемесячных. Этого достаточно, чтобы откатиться к любой важной дате.

Шифрование

Дамп не должен лежать в S3 в открытом виде. Включаю серверное шифрование bucket-а (SSE-S3 встроено) и дополнительно шифрую сам файл перед загрузкой:

openssl enc -aes-256-gcm -salt \
  -in $DIR/$DB-$DATE.dump \
  -out $DIR/$DB-$DATE.dump.enc \
  -pbkdf2 -pass file:/etc/pg-backup-key

Ключ /etc/pg-backup-key генерится один раз и хранится в защищённом виде (например, в менеджере секретов хостера). Если потерять — бэкапы превратятся в кашу. Я дублирую его в офлайн-копии.

Проверка восстановления

Самое важное и часто пропускаемое. Никогда не верь, что бэкап работает, пока не восстановил его. Я делаю это раз в неделю автоматически:

#!/bin/bash
set -euo pipefail

LATEST=$(aws --endpoint-url https://s3.storage.selcloud.ru \
  s3 ls s3://my-backups/postgres/daily/ | grep '\.dump$' | sort | tail -1 | awk '{print $4}')

aws --endpoint-url https://s3.storage.selcloud.ru \
  s3 cp s3://my-backups/postgres/daily/$LATEST /tmp/$LATEST

# Проверка чексуммы
aws --endpoint-url https://s3.storage.selcloud.ru \
  s3 cp s3://my-backups/postgres/daily/${LATEST}.sha256 /tmp/${LATEST}.sha256
sha256sum -c /tmp/${LATEST}.sha256

# Восстановление в test-базу
sudo -u postgres dropdb --if-exists restore_test
sudo -u postgres createdb restore_test
sudo -u postgres pg_restore -d restore_test -j 4 /tmp/$LATEST

# Простая проверка: есть ли таблицы
rows=$(sudo -u postgres psql -d restore_test -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'")
echo "Restored database has $rows tables"
[ "$rows" -gt 0 ] || (echo "FAIL: empty database"; exit 1)

sudo -u postgres dropdb restore_test
rm /tmp/$LATEST /tmp/${LATEST}.sha256

echo "Backup verified: $LATEST"

Если хоть раз падает — алерт, разбираюсь руками. Проверка восстановления выручала меня дважды: один раз сломалась network на сервере и pg_dump давал битый файл, другой раз неправильно настроили S3 endpoint.

WAL-архив для PITR

Если нужно «откатиться к 14:30 вчера» — обычного дампа мало. Помогает archive_mode + archive_command. В postgresql.conf:

wal_level = replica
archive_mode = on
archive_command = '/opt/scripts/wal-upload.sh %p %f'
restore_command = '/opt/scripts/wal-download.sh %f %p'

Скрипт wal-upload.sh закидывает каждый WAL-файл в S3:

#!/bin/bash
set -e
SRC=$1
NAME=$2
aws --endpoint-url https://s3.storage.selcloud.ru \
  s3 cp $SRC s3://my-backups/postgres/wal/$NAME

WAL-архив занимает существенно больше места, чем дампы. Полезно держать его 1–2 недели и применять только при сложном инциденте.

pgBackRest как альтернатива

Если хочется не возиться руками со скриптами, есть pgBackRest — мощный инструмент для бэкапа Postgres. Он умеет:

  • Полные и инкрементальные бэкапы.
  • Прямую загрузку в S3.
  • PITR с тонким контролем.
  • Параллелизм на чтении и записи.

На больших кластерах он удобнее моих собственных скриптов. На маленьких — overhead. Я для проектов 100+ ГБ беру pgBackRest, для всего остального хватает pg_dump.

Подводные камни

Свободное место на сервере

Перед тем как лить в S3, dump лежит на диске. На 50 ГБ базе нужно 50 ГБ свободно (ну или до 30, если хорошо сжимается). Если место кончится — pg_dump упадёт ровно посередине, оставив битый файл. Я мониторю свободное место как отдельную метрику.

Большие транзакции и lock

pg_dump берёт ACCESS SHARE lock, который не мешает обычной работе, но мешает ALTER TABLE. Если у вас миграция в момент бэкапа — она встанет в очередь. Я планирую ночной бэкап в окно, когда деплои не идут.

Бэкап с реплики

Чтобы не нагружать мастер, можно делать pg_dump с реплики. Это нормально, но имей в виду: реплика может слегка отставать, и бэкап будет слегка устаревшим. Различия обычно секунды, на бэкапах не критично.

Сетевые ошибки

aws CLI умеет retry, но при больших файлах временами всё-таки падает. Я добавляю --cli-read-timeout 0 и проверяю код возврата. На очень больших дампах режу через split и заливаю частями.

Чек-лист, который я повторяю каждый квартал

  • Скрипт работает, последний бэкап сегодня.
  • Lifecycle policy включена и работает (старые объекты удаляются).
  • Verify-скрипт восстанавливает базу без ошибок.
  • Чексумма последнего файла совпадает.
  • Креды S3 не утекли и ещё валидны.
  • Дисковое место под dump не близко к концу.
  • Документация на восстановление актуальна (где какой ключ, кто запускает, как).

Бэкапы — занудство, но именно от них зависит, потеряешь ли ты данные после первого инцидента. На моих проектах эта схема работает годами без вмешательства, и я сплю спокойно. Главное — не настраивать «и забыть», а проверять, что восстановление действительно работает.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.