Это руководство построено так: сценарий резервного копирования со структурированным журналированием, исключением шаблонов, ограничением пропускной способности и сохранением на основе жестких ссылок (ежедневные, еженедельные, ежемесячные снимки, которые обмениваются неизмененными файлами на диске). Систематизированный таймер заменяет cron лучшей интеграцией журналов, пропущенным восстановлением и управлением ресурсами. Все здесь работает на синхронизации через SSH, что означает, что для резервной цели нужен только порт 22.

Предпосылки

  • Два сервера под управлением Ubuntu 26.04.
  • Исходный сервер (10.0.1.60): машина резервируется.
  • Цель резервного копирования (10.0.1.61): удаленный сервер, который принимает и хранит резервные копии через SSH.
  • Права пользователя: пользователь root или обычный пользователь с привилегиями sudo.
  • Rsync установлен на обоих серверах.

Конвенции

1
2
# - данные команды должны выполняться с правами root либо непосредственно от имени пользователя root, либо с помощью команды sudo.
$ - данные команды должны выполняться от имени обычного пользователя

Настройка SSH Key Authentication

Сценарий резервного копирования работает без присмотра, поэтому ему нужен SSH без пароля от исходного сервера до цели резервного копирования. Создайте пару ключей Ed25519 на источнике (10.0.1.60):

1
sudo ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ''

Копировать открытый ключ к цели резервного копирования:

1
sudo ssh-copy-id -i /root/.ssh/id_ed25519.pub email

Проверьте соединение. Это должно вернуть имя хоста цели без запроса пароля:

1
sudo ssh -i /root/.ssh/id_ed25519 email hostname

Это должно вернуть имя хоста цели без запроса пароля:

1
backup-server

Если SSH запрашивает пароль, проверьте, что /root/.ssh/authorized keys на целевом объекте имеет правильный открытый ключ и что разрешения 600 в файле и 700 в каталоге .ssh.

Создайте резервные каталоги на цели

На цели резервного копирования (10.0.1.61) создайте структуру каталога для ежедневных, еженедельных и ежемесячных снимков. Префикс Server01 сохраняет организованность, если вы создаете резервные копии нескольких машин для одной и той же цели.

1
sudo mkdir -p /backup/server01/{daily,weekly,monthly}

Проверьте структуру:

1
tree /backup/server01

На выходе должны отображаться три пустых каталога:

1
2
3
4
5
6
/backup/server01
├── daily
├── monthly
└── weekly

3 directories, 0 files

Сценарий Backup

В этом и заключается суть установки. Сценарий обрабатывает несколько источников резервного копирования, исключает шаблоны, ограничение пропускной способности, дедупликацию на основе жестких ссылок на снимках и автоматическую очистку от удержания. Каждый запуск создает ежедневный снимок с меткой даты. По воскресеньям он копирует ежедневный снимок в неделю (используя жесткие ссылки, чтобы не потреблялось дополнительное дисковое пространство). В первый день месяца он делает то же самое. Последний симлинк всегда указывает на самую последнюю ежедневную резервную копию, которую rsync использует в качестве ссылки --link-dest для следующего запуска.

Создайте файл сценария:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
sudo tee /usr/local/bin/rsync-backup.sh > /dev/null <<'EOF'
#!/bin/bash
#
# rsync-backup.sh - Production backup with hardlink-based retention
# Runs daily via systemd timer. Keeps daily/weekly/monthly snapshots.
#

set -euo pipefail

# ============================================================
# Configuration
# ============================================================
REMOTE_HOST="10.0.1.61"
REMOTE_USER="root"
REMOTE_DIR="/backup/server01"
SSH_KEY="/root/.ssh/id_ed25519"

# Directories to back up (add or remove as needed)
BACKUP_SOURCES=(
    "/etc"
    "/srv/data"
    "/var/spool/cron"
    "/root"
)

# Patterns to exclude from backup
EXCLUDE_PATTERNS=(
    "lost+found"
    ".cache"
    "*.tmp"
    "*.swap"
    "*.swp"
    "__pycache__"
    ".terraform"
    "node_modules"
)

# Retention settings (number of snapshots to keep)
RETENTION_DAILY=7
RETENTION_WEEKLY=4
RETENTION_MONTHLY=6

# Bandwidth limit in KB/s (0 = unlimited)
BANDWIDTH_LIMIT=5000

# Logging
LOG_DIR="/var/log/rsync-backup"
LOG_FILE="${LOG_DIR}/backup-$(date '+%Y%m%d-%H%M%S').log"

# ============================================================
# Functions
# ============================================================

log() {
    local level="$1"
    shift
    local msg="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[${timestamp}] [${level}] ${msg}" | tee -a "${LOG_FILE}"
}

build_excludes() {
    local excludes=""
    for pattern in "${EXCLUDE_PATTERNS[@]}"; do
        excludes="${excludes} --exclude=${pattern}"
    done
    echo "${excludes}"
}

run_backup() {
    local source_dir="$1"
    local dest_dir="$2"
    local link_ref="$3"
    local excludes
    excludes=$(build_excludes)

    local link_dest_flag=""
    if [ -n "${link_ref}" ] && ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" "[ -d '${link_ref}' ]" 2>/dev/null; then
        link_dest_flag="--link-dest=${link_ref}"
    fi

    local source_name
    source_name=$(basename "${source_dir}")

    log "INFO" "Backing up ${source_dir} to ${REMOTE_HOST}:${dest_dir}/${source_name}/"

    ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p '${dest_dir}/${source_name}'"

    # shellcheck disable=SC2086
    rsync -avz --delete --stats \
        --bwlimit="${BANDWIDTH_LIMIT}" \
        ${link_dest_flag} \
        ${excludes} \
        -e "ssh -i ${SSH_KEY}" \
        "${source_dir}/" \
        "${REMOTE_USER}@${REMOTE_HOST}:${dest_dir}/${source_name}/" 2>&1 | tee -a "${LOG_FILE}"

    local rc=${PIPESTATUS[0]}
    if [ "${rc}" -eq 0 ]; then
        log "INFO" "Completed: ${source_dir}"
    elif [ "${rc}" -eq 24 ]; then
        log "WARN" "Completed with vanished files: ${source_dir} (exit 24, safe to ignore)"
    else
        log "ERROR" "Failed: ${source_dir} (exit code ${rc})"
        return "${rc}"
    fi
}

rotate_backups() {
    local today
    today=$(date '+%Y%m%d')
    local daily_dir="${REMOTE_DIR}/daily/${today}"
    local latest_link="${REMOTE_DIR}/daily/latest"
    local day_of_week
    day_of_week=$(date '+%u')
    local day_of_month
    day_of_month=$(date '+%d')

    log "INFO" "Starting backup rotation for ${today}"

    # Determine link-dest reference (previous latest snapshot)
    local link_ref=""
    link_ref=$(ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" \
        "readlink -f '${latest_link}' 2>/dev/null || echo ''")

    # Create today's daily directory
    ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p '${daily_dir}'"

    # Back up each source directory
    local failed=0
    for src in "${BACKUP_SOURCES[@]}"; do
        if [ -d "${src}" ]; then
            run_backup "${src}" "${daily_dir}" "${link_ref}" || ((failed++))
        else
            log "WARN" "Source directory does not exist, skipping: ${src}"
        fi
    done

    # Update the latest symlink
    ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" \
        "ln -snf '${daily_dir}' '${latest_link}'"
    log "INFO" "Updated latest symlink to ${daily_dir}"

    # Weekly snapshot on Sundays (day 7)
    if [ "${day_of_week}" -eq 7 ]; then
        local week_label
        week_label=$(date '+%Y-W%V')
        local weekly_dir="${REMOTE_DIR}/weekly/${week_label}"
        ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" \
            "cp -al '${daily_dir}' '${weekly_dir}'"
        log "INFO" "Weekly snapshot created: ${weekly_dir}"
    fi

    # Monthly snapshot on the 1st
    if [ "${day_of_month}" -eq "01" ]; then
        local month_label
        month_label=$(date '+%Y-%m')
        local monthly_dir="${REMOTE_DIR}/monthly/${month_label}"
        ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" \
            "cp -al '${daily_dir}' '${monthly_dir}'"
        log "INFO" "Monthly snapshot created: ${monthly_dir}"
    fi

    # Retention cleanup
    log "INFO" "Running retention cleanup"

    # Remove daily snapshots older than retention period
    ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" \
        "cd '${REMOTE_DIR}/daily' && ls -d [0-9]* 2>/dev/null | sort -r | tail -n +$((RETENTION_DAILY + 1)) | xargs -r rm -rf"

    # Remove weekly snapshots older than retention period
    ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" \
        "cd '${REMOTE_DIR}/weekly' && ls -d [0-9]* 2>/dev/null | sort -r | tail -n +$((RETENTION_WEEKLY + 1)) | xargs -r rm -rf"

    # Remove monthly snapshots older than retention period
    ssh -i "${SSH_KEY}" "${REMOTE_USER}@${REMOTE_HOST}" \
        "cd '${REMOTE_DIR}/monthly' && ls -d [0-9]* 2>/dev/null | sort -r | tail -n +$((RETENTION_MONTHLY + 1)) | xargs -r rm -rf"

    log "INFO" "Retention cleanup complete"

    if [ "${failed}" -gt 0 ]; then
        log "ERROR" "Backup finished with ${failed} failed source(s)"
        return 1
    fi

    log "INFO" "All backups completed successfully"
}

# ============================================================
# Main
# ============================================================

mkdir -p "${LOG_DIR}"
log "INFO" "=========================================="
log "INFO" "rsync backup started"
log "INFO" "=========================================="

START_TIME=$(date +%s)

rotate_backups
RC=$?

END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
log "INFO" "Total runtime: ${DURATION} seconds"

# Clean up old log files (keep 30 days)
find "${LOG_DIR}" -name "backup-*.log" -mtime +30 -delete 2>/dev/null || true

exit ${RC}
EOF

Сделайте сценарий исполняемым:

1
sudo chmod +x /usr/local/bin/rsync-backup.sh

Основные функции сценария

Массив BACKUP_SOURCES контролирует резервное копирование. Настройте его в соответствии с вашей средой. Общие дополнения включают /home, /var/lib/postgresql или каталоги данных приложений.

Массив EXCLUDE_PATTERNS сохраняет кэш-файлы, временные данные и создает артефакты из резервных копий.

Жесткие ссылки являются ключом к тому, чтобы сделать это пространство эффективным. Когда rsync работает с --link-dest, указывающим на предыдущий снимок, неизмененные файлы жестко связаны, а не скопированы. Неделя ежедневных резервных копий может потреблять немного больше дискового пространства, чем одна полная резервная копия.

Команда cp -al для еженедельных и ежемесячных снимков использует ту же технику.

Тестирование Backup Script

Запустите сценарий вручную, чтобы проверить, что все работает, прежде чем настраивать таймер. Выполните его как root, так как ему нужен доступ к системным каталогам:

1
sudo /usr/local/bin/rsync-backup.sh

Сценарий регистрируется как в терминале, так и в файле журнала. Первый успешный запуск выглядит так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[2026-05-23 11:42:08] [INFO] ==========================================
[2026-05-23 11:42:08] [INFO] rsync backup started
[2026-05-23 11:42:08] [INFO] ==========================================
[2026-05-23 11:42:08] [INFO] Starting backup rotation for 20260523
[2026-05-23 11:42:09] [INFO] Backing up /etc to 10.0.1.61:/backup/server01/daily/20260523/etc/
sending incremental file list
./
NetworkManager/
NetworkManager/conf.d/
...

Number of files: 1,705 (reg: 839, dir: 232, link: 634)
Number of created files: 1,705 (reg: 839, dir: 232, link: 634)
Number of deleted files: 0
Number of regular files transferred: 839
Total file size: 12,927,418 bytes
Total transferred file size: 12,927,418 bytes

[2026-05-23 11:42:11] [INFO] Completed: /etc
[2026-05-23 11:42:11] [INFO] Backing up /srv/data to 10.0.1.61:/backup/server01/daily/20260523/data/
sending incremental file list
./

Number of files: 34 (reg: 28, dir: 6)
Number of created files: 34 (reg: 28, dir: 6)
Number of regular files transferred: 28
Total file size: 385,201 bytes
Total transferred file size: 385,201 bytes

[2026-05-23 11:42:12] [INFO] Completed: /srv/data
[2026-05-23 11:42:12] [INFO] Updated latest symlink to /backup/server01/daily/20260523
[2026-05-23 11:42:12] [INFO] Running retention cleanup
[2026-05-23 11:42:12] [INFO] Retention cleanup complete
[2026-05-23 11:42:12] [INFO] All backups completed successfully
[2026-05-23 11:42:12] [INFO] Total runtime: 4 seconds

Первый запуск передает все, потому что предыдущий снимок не существует для жесткой ссылки. Последующие запускаются только для передачи измененных файлов, что значительно быстрее. При тестировании второй прогон без изменений переносится на 0 байт и завершается менее чем за 2 секунды.

Проверьте структуру резервного копирования на целевом сервере:

1
find /backup/server01 -maxdepth 3 -type d | sort

Дерево каталогов подтверждает снимок с меткой даты с каждым резервным источником в качестве подкаталога:

1
2
3
4
5
6
7
8
9
/backup/server01
/backup/server01/daily
/backup/server01/daily/20260523
/backup/server01/daily/20260523/cron
/backup/server01/daily/20260523/data
/backup/server01/daily/20260523/etc
/backup/server01/daily/20260523/root
/backup/server01/monthly
/backup/server01/weekly

Проверьте latest точки симлинка на сегодняшний снимок:

1
ls -la /backup/server01/daily/latest

Симлинк должен решить до текущей даты:

1
lrwxrwxrwx. 1 root root 38 May 23 11:42 /backup/server01/daily/latest -> /backup/server01/daily/20260523

Проверьте использование диска снимка:

1
du -sh /backup/server01/daily/20260523/

Первая полная резервная копия потребляла около 13 МБ в этой тестовой среде:

1
13M	/backup/server01/daily/20260523/

Создайте Systemd Timers

Системные таймеры лучше подходят, чем cron для резервного копирования. Они интегрируются с журналируемыми для централизованной регистрации, поддерживают Persistent=true, чтобы догнать пропущенные запуски после простоя, и позволяют контролировать ресурсы, такие как приоритет планирования ввода-вывода.

Необходимы два файла блока: сервисный блок, который определяет, что запускать, и блок таймера, который определяет, когда запускать его.

Создать сервисный блок:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
sudo tee /etc/systemd/system/rsync-backup.service > /dev/null <<'EOF'
[Unit]
Description=rsync backup to remote server
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/rsync-backup.sh
Nice=10
IOSchedulingClass=idle
TimeoutStartSec=3600

[Install]
WantedBy=multi-user.target
EOF

Настройки Nice=10 и IOSchedulingClass=idle обеспечивают работу резервных копий с низким приоритетом процессора и ввода/вывода, поэтому они не мешают производственным нагрузкам. TimeoutStartSec=3600 обеспечивает большие резервные копии до часа, прежде чем система уничтожит процесс.

Теперь создайте блок таймера:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sudo tee /etc/systemd/system/rsync-backup.timer > /dev/null <<'EOF'
[Unit]
Description=Daily rsync backup timer

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

[Install]
WantedBy=timers.target
EOF

Таймер стреляет ежедневно в 3 часа ночи с 15-минутной случайной задержкой, чтобы избежать грома стада на многосерверных настройках:

Здесь важны три настройки. Календарь =*-** 03:00:00 пожары в 3:00 ночи ежедневно. RandomizedDelaySec=900 добавляет случайную задержку до 15 минут, что предотвращает одновременное забивание цели резервного копирования несколькими серверами. Persistent=true имеет решающее значение для производства: если сервер был выключен или спал в 3:00 ночи, резервная копия запускается сразу после следующей загрузки.

Перезагрузить систему и включить таймер:

1
2
sudo systemctl daemon-reload
sudo systemctl enable --now rsync-backup.timer

Подтвердите, что таймер активен и показывает следующий запланированный запуск:

1
systemctl list-timers rsync-backup.timer

Выход показывает, когда будет запущена следующая резервная копия:

1
2
3
4
NEXT                         LEFT       LAST PASSED UNIT               ACTIVATES
Sun 2026-05-24 03:03:33 UTC  14h left   -    -      rsync-backup.timer rsync-backup.service

1 timers listed.

Случайная задержка объясняет, почему NEXT показывает 03:03 вместо ровно 03:00.

Проверьте ручной пробег через systemd

Пробуждение резервной копии через systemd (а не запускать скрипт напрямую) подтверждает, что сервисный блок, среда и разрешения работают правильно:

1
sudo systemctl start rsync-backup.service

Смотрите прогресс в журнале:

1
sudo journalctl -u rsync-backup.service --no-pager -n 20

Выход журнала отражает лог-сообщения сценария:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
May 23 11:58:02 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:02] [INFO] ==========================================
May 23 11:58:02 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:02] [INFO] rsync backup started
May 23 11:58:02 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:02] [INFO] ==========================================
May 23 11:58:02 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:02] [INFO] Starting backup rotation for 20260523
May 23 11:58:03 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:03] [INFO] Backing up /etc to 10.0.1.61:...
May 23 11:58:05 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:05] [INFO] Completed: /etc
May 23 11:58:06 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:06] [INFO] Completed: /srv/data
May 23 11:58:06 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:06] [INFO] All backups completed successfully
May 23 11:58:06 source-server rsync-backup.sh[4521]: [2026-05-23 11:58:06] [INFO] Total runtime: 4 seconds
May 23 11:58:06 source-server systemd[1]: rsync-backup.service: Deactivated successfully.
May 23 11:58:06 source-server systemd[1]: Finished rsync backup to remote server.

Линия «Deactivated successfully» подтверждает, что система счастлива. Если сервис выходит из строя, systemctl status rsync-backup.service. Сервис показывает код выхода и последние строки журнала.

Для мониторинга вы также можете настроить оповещения по электронной почте или Slack о сбое. Простой подход заключается в добавлении email в раздел [Unit] файла службы с соответствующим блоком службы уведомлений.

Конфигурация Firewall

Поскольку rsync работает над SSH, резервной цели (10.0.1.61) требуется только порт 22. Исходный сервер инициирует исходящие соединения, поэтому никаких изменений брандмауэра там не требуется.

1
2
sudo ufw allow ssh
sudo ufw enable

Подтвердите правило:

1
sudo ufw status

Результаты подтверждают, что SSH разрешен:

1
2
3
4
5
6
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)

Настройка сценария

Несколько корректировок, которые стоит рассмотреть для развертывания производства.

Добавление дополнительных резервных источников: Редактируйте массив BACKUP_SOURCES в сценарии. Каталоги данных базы данных (/var/lib/postgresql, /var/lib/mysql) должны быть сохранены только после сброса с помощью pg_dump или mysqldump, поскольку rsync не может гарантировать последовательный снимок запущенной базы данных.

Ограничение пропускной способности: передача по умолчанию BANDWIDTH_LIMIT=5000 кэпов примерно на 5 МБ/с. Установите его до 0 для неограниченного числа или опустите на общие или дозированные соединения. Это особенно важно при резервном копировании по ссылкам WAN.

Настройка удержания: по умолчанию хранятся 7 ежедневных, 4 еженедельных и 6 ежемесячных снимков. Благодаря жестким ссылкам накладные расходы на диск в основном пропорциональны скорости изменения файлов, а не количеству снимков. Мониторинг использования диска на цели резервного копирования с df -h и настройка значений удержания по мере необходимости.

Синхронизация в реальном времени: Если вам нужна непрерывная синхронизация файлов, а не запланированные снимки, rsync в сочетании с часами lsyncd для изменения файловой системы и запускает немедленную передачу. Этот подход дополняет запланированные резервные копии, а не заменяет их.

Шифрование в покое: для зашифрованных, дублированных резервных копий с проверками целостности на уровне репозитория стоит оценить BorgBackup с borgmatic. Он тяжелее, чем обычный, но добавляет функции, которые имеют значение, когда существуют требования соответствия.

Завершение

Сценарий обрабатывает ежедневное, еженедельное и ежемесячное вращение с использованием жестких ссылок для эффективности пространства с ограничением пропускной способности и встроенным структурированным журналированием. Систематизированный таймер заменяет cron лучшей интеграцией журналов, управлением ресурсами через Nice и IOSchedulingClass и Persistent=true, чтобы наверстать упущенное после перезагрузки или простоя.

Эти скрипты могут показаться простыми на первый взгляд, но они выполняют критически важные задачи, которые поддерживают стабильность, безопасность и бесперебойную работу серверов.

Вы также можете поделиться статьей со своими друзьями в социальных сетях, которым может быть интересна эта статья, или просто оставить комментарий ниже. Спасибо.