Бэкап MySQL, на который можно положиться, - это не одна задача, а три отдельные. Первая - копия, которая создаётся по расписанию. Вторая - копия, которая лежит не там же, где база. Третья - регулярное, скучное доказательство того, что из второй копии можно собрать рабочую базу. На RE:NODE первое делается в панели: расписание, которое создаёт backup. Второе и третье - это mysqldump, запущенный по cron на вашей собственной машине, выгрузка в S3 storage и скрипт, который загружает вчерашний файл во временный MySQL и проверяет его. В этой статье мы собираем все три в том порядке, в котором они должны запускаться, и с таким временем, чтобы они не мешали друг другу.
Если вам нужны только флаги самого mysqldump, их по очереди объясняет mysqldump backup and restore, а общий приём с передачей дампа в S3 для трёх движков описан в database dumps to S3 on a schedule. Эта статья уже: одна база MySQL, одно расписание и проверка восстановления, которую вы действительно будете запускать.
План: три копии, три расписания#
Каждый слой защищает от своей беды и запускается в своё время, чтобы не конкурировать за одну и ту же базу.
| Слой | От чего защищает | Где лежит |
|---|---|---|
| Слот бэкапа в панели | Плохая миграция, удалённая таблица, быстрый откат | Вне машины с базой, в слоте панели |
Файл mysqldump в S3 | Удалённый сервер, потерянный аккаунт, переезд на другой хостинг | Ваш bucket, отдельный сервис |
| Проверка восстановления | Бэкап есть, но не загружается | Временный MySQL, который потом удаляется |
Бэкап панели - быстрый. Панель RE:NODE хранит бэкапы в слотах на каждом тарифе, создаёт их по запросу или по расписанию, восстанавливает кнопкой, позволяет скачать и позволяет заблокировать бэкап от ротации. Они хранятся вне той машины, которую защищают. Чего мы не публикуем - это срок хранения сверх ротации, ограничение по размеру и то, где физически лежат копии вне машины. Поэтому считайте слот удобной первой линией, а не единственной копией. Число слотов зависит от уровня; для MySQL оно от одного до четырёх, так что проверьте свой тариф. Удаление сервера удаляет и его бэкапы, в том числе заблокированные, и это самый сильный довод в пользу второго слоя.
Файл mysqldump переносим. Это обычный SQL, поэтому его можно загрузить на другой хостинг, в более новый MySQL или на ноутбук. Проверку восстановления пропускают чаще всего, и именно она решает, засчитываются ли остальные два слоя.
Разумное расписание для базы, которая тише всего ранним утром: бэкап панели в 03:00, дамп в 03:30, выгрузка сразу после него и проверка восстановления раз в неделю, в воскресенье в 06:00. Порядок важнее промежутков. Бэкап панели и дамп - оба читатели, поэтому могут перекрываться, но долгий дамп держит открытым snapshot, и два таких одновременно на маленьком тарифе вам не нужны.
Где запускается задача дампа#
mysqldump - клиент. Он подключается к базе по сети, как любое приложение, и не требует shell на машине с базой. Это удачно, потому что тариф базы данных - не место для установки собственных инструментов. На RE:NODE сервер MySQL выдаёт сгенерированный пароль root, базу приложения и пользователя приложения, доступные на host и port тарифа. Shell на самом сервере базы мы не описываем, так что считайте, что mysqldump запускается в другом месте, и планируйте исходя из этого.
Варианты для исполнителя, по убыванию предпочтения:
- Небольшой VDS или любая Linux-машина, которая у вас уже есть, с cron и клиентскими инструментами MySQL. Это обычный ответ.
- Сервер приложения, если это машина, где можно ставить пакеты и править crontab. Осторожно: удалённый сервер приложения уносит с собой и задачу бэкапа.
- Ваш собственный компьютер или домашний сервер, у которого есть достоинство - он находится в другом месте и относительно базы, и относительно bucket.
Вкладка Schedules в панели может запускать на сервере консольную команду, backup или power action. Для задачи backup и для перезапусков это именно то, что нужно. Консольная команда вводится в консоль сервера, а это не универсальный shell, поэтому не ждите, что она запустит mysqldump против базы. Используйте расписание панели для того, что делает панель, а для дампа - cron на вашей машине.
Версия клиента должна совпадать с версией сервера. В статье имеется в виду MySQL 8.4 LTS, поэтому поставьте на исполнителя клиентские инструменты 8.4. mysqldump гораздо более старого выпуска может споткнуться об параметры, которые знает текущий сервер, а дамп клиентом MariaDB против MySQL для бэкапа лучше не использовать. Проверьте, что у вас есть:
$ mysqldump --version$ mysql --versionШаг первый: расписание бэкапов в панели#
Откройте сервер в панели, перейдите в Schedules и создайте расписание. У расписания есть cron-выражение и упорядоченный список задач, каждая из которых может ждать заданную задержку после предыдущей. Для базы полезна форма с одной задачей, создающей backup. Если вы ни разу не писали cron-выражение, cron expressions explained читается за пять минут.
Ежедневный backup в 03:00 выглядит так:
0 3 * * *Пять полей - это минута, час, день месяца, месяц и день недели. Прежде чем выбирать час, проверьте, какой часовой пояс использует вкладка Schedules, потому что часы панели и часы вашего исполнителя не обязательно одни и те же. Пояс для расписаний мы не указываем, поэтому посмотрите, как вкладка показывает время следующего запуска, а если отображение неясно, создайте одно пробное расписание.
Затем подумайте о слотах. При одном-четырёх слотах на тарифе MySQL ежедневный бэкап быстро проходит по кругу. Помогают две привычки:
- Держите второе, еженедельное расписание, только если на тарифе остаются свободные слоты, и воспринимайте слоты как короткое окно недавних состояний, а не как историю.
- Блокируйте бэкап, который хотите сохранить, например снятый перед рискованной миграцией. Заблокированные бэкапы исключаются из ротации, так что помните, что они занимают слот, пока вы их не разблокируете.
Надёжность бэкапа из панели не узнать. После первого запуска по расписанию откройте Backups и убедитесь, что запись появилась в ожидаемое время и размер правдоподобен. Страница guides разбирает вкладку Schedules, а раздел о бэкапах - вкладку Backups.
Шаг второй: пользователь для mysqldump#
Не запускайте задачу cron от root. Пароль root - ключ ко всему серверу, а среда cron - плохое место для его хранения. Пользователь приложения, которого генерирует RE:NODE, лучше, потому что он касается только вашей базы. Если вы можете создать отдельную учётную запись, дайте ей только то, что нужно дампу. Для одной базы это выглядит так:
CREATE USER 'backup'@'%' IDENTIFIED BY RANDOM PASSWORD;GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT ON appdb.* TO 'backup'@'%';IDENTIFIED BY RANDOM PASSWORD один раз выводит сгенерированный пароль; сразу перенесите его в файл учётных данных ниже. Можно ли на вашем тарифе создавать дополнительных пользователей - это вопрос к вашему аккаунту в панели, так что проверьте, прежде чем на это рассчитывать; если нельзя, сгенерированного пользователя приложения достаточно, чтобы сделать дамп его собственной базы. Если --routines падает с ошибкой привилегий, MySQL users and privileges объясняет привилегию SHOW_ROUTINE, которая это исправляет.
Положите учётные данные в файл, чтобы они не попадали ни в список процессов, ни в историю shell:
[client]host = db.example.netport = 30412user = backuppassword = paste-the-generated-password-hereЗамените host и port на те, что панель показывает для вашей базы. Затем закройте файл:
$ sudo mkdir -p /etc/backup$ sudo chown root:root /etc/backup && sudo chmod 700 /etc/backup$ sudo chmod 600 /etc/backup/my.cnfЕсли задача запускается от обычного пользователя, сделайте владельцем каталога его. Смысл в том, чтобы больше никто на исполнителе не мог файл прочитать. Проверьте подключение, прежде чем писать любой скрипт:
$ mysql --defaults-extra-file=/etc/backup/my.cnf -e 'SELECT VERSION(), NOW();'Шаг третий: скрипт дампа#
У конвейера mysqldump | gzip > file есть известный недостаток: если mysqldump умирает на полпути, компрессор всё равно завершается чисто и оставляет маленький, на вид корректный файл. Скрипт ниже обходит это с помощью pipefail, пишет во временное имя и присваивает окончательное только после того, как докажет, что файл полный. Полный файл mysqldump заканчивается строкой комментария, начинающейся с -- Dump completed; в оборванном её нет, поэтому это дешёвая и честная проверка полноты.
#!/usr/bin/env bashset -euo pipefailDB=appdbWORK=/var/backups/mysqlBUCKET=s3://db-dumps/mysql/$DBPROFILE=storeKEEP_LOCAL_DAYS=3STAMP=$(date -u +%F_%H%M)NAME="$DB-$STAMP.sql.zst"mkdir -p "$WORK"cd "$WORK"mysqldump --defaults-extra-file=/etc/backup/my.cnf \ --single-transaction --routines --events --triggers --hex-blob \ --set-gtid-purged=OFF --no-tablespaces "$DB" \ | zstd -q -T0 > "$NAME.part"zstd -t -q "$NAME.part"zstd -dc "$NAME.part" | tail -n 1 | grep -q '^-- Dump completed'mv "$NAME.part" "$NAME"sha256sum "$NAME" > "$NAME.sha256"aws s3 cp "$NAME" "$BUCKET/$NAME" --profile "$PROFILE" --only-show-errorsaws s3 cp "$NAME.sha256" "$BUCKET/$NAME.sha256" --profile "$PROFILE" --only-show-errorsfind "$WORK" -name "$DB-*.sql.zst*" -mtime +"$KEEP_LOCAL_DAYS" -deleteecho "ok $NAME $(stat -c %s "$NAME") bytes"Разберём, что он делает, потому что у каждой строки есть причина:
set -euo pipefailостанавливает скрипт на первой ошибке, на необъявленной переменной и на сбое любой стадии конвейера. Безpipefailстатусом выхода конвейера дампа был бы статус компрессора.--single-transactionчитает все таблицы InnoDB из одного согласованного snapshot, не блокируя писателей.--routines --events --triggersкладут в файл хранимые процедуры, запланированные события и триггеры.--hex-blobбезопасно записывает двоичные столбцы.--set-gtid-purged=OFFи--no-tablespacesизбавляют от двух ошибок, которые чаще всего останавливают дамп обычного пользователя.zstd -tпроверяет целостность сжатого файла. Следующая строка распаковывает его и проверяет последнюю строку, что доказывает: сам дамп завершился нормально, а не просто сжатие осталось целым.- Имя
.partозначает, что наполовину записанный файл никогда не примут за бэкап. Окончательное имя получает только файл, прошедший обе проверки. - Контрольная сумма выгружается рядом с дампом. Проверка восстановления использует её, чтобы обнаружить повреждённую загрузку или неудачную выгрузку.
- Локальные копии удаляются через три дня. Долгую историю хранит bucket; исполнителю нужны лишь последние несколько для быстрого восстановления.
Установите инструменты и сделайте файл исполняемым. Названия пакетов ниже - для Debian и Ubuntu:
$ sudo apt install mysql-client zstd awscli$ sudo chmod 755 /usr/local/bin/mysql-backup.sh$ sudo /usr/local/bin/mysql-backup.shЗапустите его вручную один раз и прочитайте вывод, прежде чем ставить на расписание. Запишите размер файла и то, сколько это заняло. Оба числа - ваша точка отсчёта.
Шаг четвёртый: S3 storage как внешняя копия#
Выгрузка использует AWS CLI, потому что он есть на каждой платформе, а S3 storage RE:NODE с ним работает. Каждый S3-сервер поставляется со сгенерированными для него access key и secret key и уже созданным первым bucket, с адресацией path-style и любым названием региона. Endpoint отвечает по обычному HTTP на своём порту, а HTTPS идёт через proxy slot тарифа. Для дампа это имеет практическое следствие: файл идёт по сети, поэтому либо используйте адрес HTTPS, либо шифруйте файл до того, как он покинет исполнителя. Дамп содержит всех пользователей, все хэши и все адреса вашего приложения.
Настройте профиль на исполнителе один раз. Endpoint ниже - заглушка вместо адреса, который показывает ваш тариф:
[profile store]region = us-east-1endpoint_url = https://s3.example.comrequest_checksum_calculation = when_requiredresponse_checksum_validation = when_requireds3 = addressing_style = path[store]aws_access_key_id = YOUR_ACCESS_KEYaws_secret_access_key = YOUR_SECRET_KEYПроверьте списком. Первый вызов громко падает, если endpoint, ключи или стиль адресации неверны:
$ aws s3 ls s3://db-dumps/ --profile storeS3 storage with the AWS CLI подробнее рассказывает про endpoint и настройки checksum. Если вы предпочитаете rclone, равнозначный remote есть в rclone with S3 storage.
Для шифрования проще всего поставить age между компрессором и файлом: замените конец строки дампа на | zstd -q -T0 | age -r YOUR_PUBLIC_KEY > "$NAME.part" и поправьте имя и проверки, а это значит, что проверка полноты теперь должна сначала расшифровать, приватным ключом, который не стоит оставлять на исполнителе. Из-за этого противоречия многие в первой версии выбирают HTTPS и закрытый bucket, а шифрование добавляют позже. Что бы вы ни выбрали, храните ключ расшифровки там, где он переживёт потерю исполнителя.
Тариф S3 storage хранит одну копию на NVMe в одном месте, без репликации. Это второй дом для дампов, а не третий. Если данные важны, действует идея 3-2-1 из S3 storage sizing and the 3-2-1 rule: бэкап панели, дамп в bucket и время от времени скачивание куда-нибудь совсем в другое место. Размер bucket считайте от политики. Тридцать ежедневных дампов по 300 МБ в сжатом виде - это около 9 ГБ, что едва помещается на тариф в 10 ГБ, так что перед выбором тарифа измерьте свой первый файл.
Шаг пятый: расписание в cron#
Cron - планировщик на исполнителе. Отредактируйте crontab пользователя, которому принадлежат учётные данные:
$ crontab -eMAILTO=you@example.com30 3 * * * flock -n /tmp/mysql-backup.lock /usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1Подставьте в MAILTO свой адрес; cron отправляет по почте вывод задачи, не перенаправленный никуда, хотя здесь его принимает лог. 30 3 * * * - это 03:30 каждый день в часовом поясе исполнителя, который можно проверить через timedatectl. flock -n отказывается запускать вторую копию, если предыдущая ещё работает, а именно так на растущей базе появляются два перекрывающихся дампа. Лог должен существовать и быть доступным для записи пользователю задачи:
$ sudo touch /var/log/mysql-backup.log$ sudo chown "$USER" /var/log/mysql-backup.logCron работает с минимальным PATH, поэтому скрипт, который работает в вашем shell и падает из cron, почти всегда просто не находит aws или zstd. Либо выполните command -v aws zstd mysqldump и впишите каталоги в строку PATH= в начале crontab, либо используйте полные пути в скрипте. Проверьте лог наутро после первого запуска, а не через неделю.
Задача, которая перестала работать, выглядит точно так же, как работающая, поэтому добавьте сигнализацию о тишине. Многие бесплатные сервисы мониторинга дают URL; скрипт обращается к нему только в конце успешного запуска, а вы получаете оповещение, когда этого обращения нет. Добавьте одну строку последней командой скрипта:
curl -fsS -m 10 --retry 3 "https://hc.example.com/ping/your-uuid" > /dev/nullСледите и за строкой с размером, которую скрипт пишет в лог. Дамп, внезапно сжавшийся до нескольких килобайт, сломался так, что коды выхода этого не поймали, например потому что пользователь приложения лишился привилегий.
Шаг шестой: проверка восстановления#
Бэкап, который никто не восстанавливал, - гипотеза. Проверка восстановления делает из неё факт, и её стоит автоматизировать, потому что вручную она случается один раз. Идея проста: взять из bucket самый новый дамп, проверить контрольную сумму, загрузить его во временный MySQL той же версии, который существует только для проверки, выполнить несколько запросов, успешных только на настоящих данных, вывести, сколько это заняло, и всё удалить.
Docker - самый простой временный сервер, и он держит проверку подальше от вашей настоящей базы. Закрепите ту же версию, что у сервера, здесь 8.4:
#!/usr/bin/env bashset -euo pipefailDB=appdbBUCKET=s3://db-dumps/mysql/$DBPROFILE=storeCTR=restore-test-$$EXPECT_TABLES=12WORK=$(mktemp -d)cleanup() { docker rm -f "$CTR" > /dev/null 2>&1 || true; rm -rf "$WORK"; }trap cleanup EXITcd "$WORK"LATEST=$(aws s3 ls "$BUCKET/" --profile "$PROFILE" \ | awk '{print $4}' | grep '\.sql\.zst$' | sort | tail -n 1)[ -n "$LATEST" ] || { echo "no dump found"; exit 1; }aws s3 cp "$BUCKET/$LATEST" . --profile "$PROFILE" --only-show-errorsaws s3 cp "$BUCKET/$LATEST.sha256" . --profile "$PROFILE" --only-show-errorssha256sum -c "$LATEST.sha256"docker run -d --name "$CTR" -e MYSQL_ROOT_PASSWORD=scratch \ -e MYSQL_DATABASE="$DB" mysql:8.4 > /dev/nulluntil docker exec "$CTR" mysql -h127.0.0.1 -uroot -pscratch -e 'SELECT 1' \ > /dev/null 2>&1; do sleep 2; doneSTART=$(date +%s)zstd -dc "$LATEST" | docker exec -i "$CTR" mysql -uroot -pscratch "$DB"END=$(date +%s)TABLES=$(docker exec "$CTR" mysql -uroot -pscratch -N -e \ "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB'")echo "tables: $TABLES (expected at least $EXPECT_TABLES)"[ "$TABLES" -ge "$EXPECT_TABLES" ]docker exec "$CTR" mysql -uroot -pscratch -N "$DB" -e \ "SELECT COUNT(*) AS orders, MAX(created_at) AS newest FROM orders"echo "restored $LATEST in $((END - START)) s"Здесь важны несколько деталей. Официальный образ MySQL при инициализации запускает временный сервер, а затем перезапускается. Этот временный сервер не слушает TCP, поэтому цикл ожидания подключается с -h127.0.0.1 и проходит только тогда, когда поднялся настоящий сервер; обычный mysqladmin ping через сокет может сработать слишком рано, и импорт наткнётся на перезапуск. EXPECT_TABLES - число, которое вы задаёте вручную, то есть количество таблиц, которое должно быть в вашей базе, и проверка падает, если в восстановленной копии их меньше. Последний запрос нужно адаптировать: возьмите любую таблицу и столбец с временем, который ваше приложение пишет при каждом изменении, чтобы с первого взгляда видеть, что самая новая строка - вчерашняя, а не прошлого месяца.
trap удаляет контейнер и временный каталог независимо от того, прошла проверка или упала, так что сбой ничего не оставляет. Одноразовый пароль root scratch безвреден, потому что контейнер не публикует ни одного порта.
В первый раз запустите вручную и прочитайте, что он говорит. Затем поставьте в cron раз в неделю, вне окна дампа:
0 6 * * 0 /usr/local/bin/mysql-restore-test.sh >> /var/log/mysql-restore-test.log 2>&1Число, которое стоит сохранить после первого запуска, - время восстановления. Дамп в несколько сотен мегабайт на ноутбуке загружается за минуты, но загрузка заново строит все индексы, и восстановление нередко в несколько раз медленнее, чем был дамп. Если это число больше простоя, который вы готовы терпеть, ответ - не более быстрый скрипт, а параллельные дамп и загрузка через MySQL Shell, описанные в mysqldump backup and restore, или базы поменьше. Test a backup restore: a 30-minute drill превращает ту же идею в рутину для команды.
Восстановление по-настоящему#
Когда день придёт, у вас два пути, и какой быстрее, решает панель. Для ошибки последних нескольких дней первым стоит попробовать slot backup: восстановите его кнопкой на вкладке Backups. Прежде чем нажимать, помните, что восстановление заменяет текущее состояние, так что если в базе остались изменения, которые вам нужны, сначала снимите свежий бэкап или скачайте слот, поверх которого собираетесь восстанавливать.
Когда слота уже нет или нет сервера, используйте дамп. Создайте базу на новом сервере и загрузите файл прямо из bucket:
$ aws s3 cp s3://db-dumps/mysql/appdb/appdb-2026-10-23_0330.sql.zst - --profile store \ | zstd -dc \ | mysql --defaults-extra-file=/etc/backup/my.cnf appdbВосстанавливайте в пустую базу. Дамп удаляет и заново создаёт таблицы, которые в нём есть, но другие таблицы не трогает, поэтому загрузка «поверх» живой базы даёт смесь. Если приложение не должно видеть наполовину загруженную базу, восстановите в базу с новым именем и переключите строку подключения, когда всё будет готово. Если вы переезжаете на совсем другой хостинг, migrating MySQL to a new host разбирает пользователей, definer и переключение.
Что идёт не так#
Сбои группируются вокруг нескольких причин, и большинство из них тихие.
- Задача ни разу не запускалась. У cron минимальный
PATH, crontab принадлежит другому пользователю или машину пересобрали. Отсутствие обращения к сервису мониторинга - то, как вы узнаете об этом за день. Без него вы узнаете при восстановлении. - Access denied для tablespaces. Начиная с MySQL 8.0.21, дамп информации о табличных пространствах требует глобальной привилегии
PROCESS. Скрипт передаёт--no-tablespaces, что и нужно дампу приложения. - Дамп крошечный. Пользователь подключается, но ничего не видит, или имя базы неверно.
mysqldumpможет успешно завершиться на пустой базе. Держите в логе строку с размером и сравнивайте со вчерашней. - Нет строки `Dump completed`. Соединение оборвалось посреди дампа, обычно при долгом запуске по ненадёжному каналу. Скрипт не повышает такой файл до бэкапа, вы видите ошибку в логе, а нового объекта в bucket нет.
- Выгрузка падает с ошибкой checksum или адресации. Две настройки checksum в профиле и
addressing_style = path- обычное решение для S3-совместимых endpoint. - Восстановление неверно так, что ни одна проверка этого не ловит. Число строк совпадает, самая новая метка времени верна, но хранимой процедуры нет. Это означает, что в команде дампа нет
--routinesи--events, и поэтому они есть в скрипте. - Дамп пересекается с миграцией.
--single-transactionне защищает от изменений схемы посреди дампа. Не выкатывайте миграции между 03:25 и временем, когда дамп обычно заканчивается.
Если база небольшая, вся рутина использует несколько сотен мегабайт хранилища и несколько минут времени исполнителя за ночь. Если большая, думать придётся о времени восстановления, и выяснять это правильно в воскресной проверке, а не во время аварии.
FAQ#
Можно ли запускать mysqldump из вкладки Schedules в панели?
Для дампа базы - нет. Расписание выполняет на сервере консольную команду, backup или power action, а консольная команда уходит в консоль сервера, а не в универсальный shell. Используйте расписание для бэкапов и перезапусков, а mysqldump запускайте по cron с отдельной машины.
Сколько бэкапов панели хранить?
Столько, сколько позволяют слоты вашего тарифа, воспринимая их как короткое окно недавних состояний. У тарифа MySQL от одного до четырёх слотов в зависимости от уровня. Блокируйте тот, который нельзя потерять, перед рискованным изменением, а длинную историю храните как дампы в S3.
Как часто запускать проверку восстановления?
Для важной базы раз в неделю - хорошее значение по умолчанию, а также после любого изменения скрипта дампа, версии MySQL или учётных данных. Проверка стоит нескольких минут запасной машины, а первый сбой, который она поймает, окупит все запуски без происшествий.
Нужно ли шифровать дампы?
Да, если файл идёт по сети без HTTPS или ключи bucket могут утечь, потому что дамп содержит всех пользователей и хэши базы. Endpoint S3 отвечает по обычному HTTP на своём порту, так что используйте адрес HTTPS за proxy slot тарифа или шифруйте инструментом вроде age до выгрузки и храните ключ отдельно от исполнителя.
Что делать, если база слишком велика для одного файла mysqldump?
Примерно после 20-50 ГБ однопоточный дамп и особенно однопоточное восстановление становятся медленными. Утилиты dump и load из MySQL Shell работают параллельно и обычно становятся следующим шагом. Сначала измерьте время восстановления, потому что именно это число решает, нужно ли что-то менять.


