- Автор темы
- Администратор
- Модер.
- Команда форума
- #1
Скрипт backup mysql 8.4 для freebsd 14
mydatabase_2026-03-11_03-00-00.sql.tmp
и только после успешного завершения переименовывается в .sql. Это защищает от битых полуготовых файлов.
--quick
Для крупных баз это правильнее: mysqldump выгружает строки потоком и меньше нагружает RAM.
Cron
Лучше убрать пароль из скрипта вообще и использовать ~/.my.cnf для пользователя cron:
Права:
chmod 600 ~/.my.cnf
Тогда из скрипта можно удалить:
и заменить вызовы на такие:
Если хочешь избежать ситуации, когда лог растёт бесконечно, можно вынести логирование в syslog через logger вместо файла. Тогда скрипт будет ещё аккуратнее для production.
- set -eu
- защитой от параллельного запуска через lockf
- trap для очистки временного файла
- проверкой доступности MySQL
- проверкой доступа к конкретной БД
- проверкой свободного места
- ротацией по количеству файлов
- аккуратным логированием
Bash:
#!/bin/sh
set -eu
# ===== НАСТРОЙКИ =====
DB_NAME="mydatabase"
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_USER="backup_user"
DB_PASS="strong_password"
BACKUP_DIR="/var/backups/mysql"
LOG_FILE="/var/log/mysql_backup.log"
LOCK_FILE="/var/run/mysql_backup_${DB_NAME}.lock"
KEEP_FILES=7
# Минимум свободного места в KB.
# Например 1048576 = 1 GB
MIN_FREE_KB=1048576
DATE="$(date +"%Y-%m-%d_%H-%M-%S")"
TMP_FILE=""
FINAL_FILE=""
ARCHIVE_FILE=""
MYSQL="/usr/local/bin/mysql"
MYSQLADMIN="/usr/local/bin/mysqladmin"
MYSQLDUMP="/usr/local/bin/mysqldump"
GZIP="/usr/bin/gzip"
DF="/bin/df"
AWK="/usr/bin/awk"
LS="/bin/ls"
TAIL="/usr/bin/tail"
RM="/bin/rm"
MKDIR="/bin/mkdir"
LOCKF="/usr/bin/lockf"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
cleanup() {
rc=$?
if [ -n "${TMP_FILE}" ] && [ -f "${TMP_FILE}" ]; then
$RM -f "${TMP_FILE}" || true
log "Temporary file removed: ${TMP_FILE}"
fi
exit $rc
}
fail() {
log "ERROR: $*"
exit 1
}
check_binaries() {
[ -x "$MYSQL" ] || fail "mysql not found: $MYSQL"
[ -x "$MYSQLADMIN" ] || fail "mysqladmin not found: $MYSQLADMIN"
[ -x "$MYSQLDUMP" ] || fail "mysqldump not found: $MYSQLDUMP"
[ -x "$GZIP" ] || fail "gzip not found: $GZIP"
[ -x "$DF" ] || fail "df not found: $DF"
[ -x "$AWK" ] || fail "awk not found: $AWK"
[ -x "$LS" ] || fail "ls not found: $LS"
[ -x "$TAIL" ] || fail "tail not found: $TAIL"
[ -x "$RM" ] || fail "rm not found: $RM"
[ -x "$MKDIR" ] || fail "mkdir not found: $MKDIR"
[ -x "$LOCKF" ] || fail "lockf not found: $LOCKF"
}
check_free_space() {
FREE_KB=$($DF -k "$BACKUP_DIR" | $AWK 'NR==2 {print $4}')
[ -n "$FREE_KB" ] || fail "cannot determine free disk space for $BACKUP_DIR"
case "$FREE_KB" in
''|*[!0-9]*)
fail "invalid free space value: $FREE_KB"
;;
esac
if [ "$FREE_KB" -lt "$MIN_FREE_KB" ]; then
fail "not enough free disk space in $BACKUP_DIR: ${FREE_KB} KB available, required at least ${MIN_FREE_KB} KB"
fi
}
check_mysql() {
"$MYSQLADMIN" \
--host="$DB_HOST" \
--port="$DB_PORT" \
--user="$DB_USER" \
--password="$DB_PASS" \
ping >/dev/null 2>>"$LOG_FILE" || fail "MySQL is unavailable on ${DB_HOST}:${DB_PORT}"
"$MYSQL" \
--host="$DB_HOST" \
--port="$DB_PORT" \
--user="$DB_USER" \
--password="$DB_PASS" \
--batch --skip-column-names \
-e "USE \`${DB_NAME}\`; SELECT 1;" >/dev/null 2>>"$LOG_FILE" || fail "cannot access database ${DB_NAME}"
}
rotate_backups() {
FILES=$($LS -1t "${BACKUP_DIR}/${DB_NAME}"_*.sql.gz 2>/dev/null || true)
if [ -n "$FILES" ]; then
echo "$FILES" | $TAIL -n +$((KEEP_FILES + 1)) | while IFS= read -r oldfile; do
[ -n "$oldfile" ] || continue
$RM -f "$oldfile" && log "Old backup removed: $oldfile"
done
fi
}
do_backup() {
trap cleanup EXIT HUP INT TERM
$MKDIR -p "$BACKUP_DIR" || fail "cannot create backup dir: $BACKUP_DIR"
check_binaries
check_free_space
check_mysql
FINAL_FILE="${BACKUP_DIR}/${DB_NAME}_${DATE}.sql"
TMP_FILE="${FINAL_FILE}.tmp"
ARCHIVE_FILE="${FINAL_FILE}.gz"
log "Backup started for database: ${DB_NAME}"
"$MYSQLDUMP" \
--host="$DB_HOST" \
--port="$DB_PORT" \
--user="$DB_USER" \
--password="$DB_PASS" \
--single-transaction \
--quick \
--routines \
--triggers \
--events \
--hex-blob \
--default-character-set=utf8mb4 \
"$DB_NAME" > "$TMP_FILE" 2>>"$LOG_FILE" || fail "mysqldump failed for ${DB_NAME}"
mv "$TMP_FILE" "$FINAL_FILE" || fail "cannot move temporary dump to final file"
"$GZIP" "$FINAL_FILE" 2>>"$LOG_FILE" || fail "gzip failed for ${FINAL_FILE}"
log "Backup created: ${ARCHIVE_FILE}"
rotate_backups
log "Backup finished for database: ${DB_NAME}"
}
if [ "${1:-}" = "--run" ]; then
do_backup
exit 0
fi
$LOCKF -s -t 0 "$LOCK_FILE" "$0" --run
STATUS=$?
if [ "$STATUS" -eq 0 ]; then
exit 0
fi
log "Backup skipped: another instance is already running"
exit 0
Что здесь добавлено
set -eu- -e завершает скрипт при ошибке команды
- -u завершает скрипт при обращении к неинициализированной переменной
trap cleanup
Если скрипт упадёт посередине, временный .tmp будет удалён.Временный файл
Сначала дамп пишется в:mydatabase_2026-03-11_03-00-00.sql.tmp
и только после успешного завершения переименовывается в .sql. Это защищает от битых полуготовых файлов.
Проверка свободного места
Перед стартом проверяется, что в файловой системе каталога бэкапов есть хотя бы MIN_FREE_KB.--quick
Для крупных баз это правильнее: mysqldump выгружает строки потоком и меньше нагружает RAM.
Cron
Код:
0 3 * * * /usr/local/bin/mysql_backup_db.sh
Рекомендация по безопасности
Лучше убрать пароль из скрипта вообще и использовать ~/.my.cnf для пользователя cron:
Код:
[client]
user=backup_user
password=strong_password
host=127.0.0.1
port=3306
Права:
chmod 600 ~/.my.cnf
Тогда из скрипта можно удалить:
Bash:
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_USER="backup_user"
DB_PASS="strong_password"
и заменить вызовы на такие:
Bash:
"$MYSQLADMIN" ping
"$MYSQL" --batch --skip-column-names -e "USE \`${DB_NAME}\`; SELECT 1;"
"$MYSQLDUMP" --single-transaction --quick --routines --triggers --events --hex-blob --default-character-set=utf8mb4 "$DB_NAME"
Практическая доработка
Если хочешь избежать ситуации, когда лог растёт бесконечно, можно вынести логирование в syslog через logger вместо файла. Тогда скрипт будет ещё аккуратнее для production.
