Это руководство построено так: сценарий резервного копирования со структурированным журналированием, исключением шаблонов, ограничением пропускной способности и сохранением на основе жестких ссылок (ежедневные, еженедельные, ежемесячные снимки, которые обмениваются неизмененными файлами на диске). Систематизированный таймер заменяет 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
|
Это должно вернуть имя хоста цели без запроса пароля:
Если SSH запрашивает пароль, проверьте, что /root/.ssh/authorized keys на целевом объекте имеет правильный открытый ключ и что разрешения 600 в файле и 700 в каталоге .ssh.
Создайте резервные каталоги на цели#
На цели резервного копирования (10.0.1.61) создайте структуру каталога для ежедневных, еженедельных и ежемесячных снимков. Префикс Server01 сохраняет организованность, если вы создаете резервные копии нескольких машин для одной и той же цели.
1
| sudo mkdir -p /backup/server01/{daily,weekly,monthly}
|
Проверьте структуру:
На выходе должны отображаться три пустых каталога:
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
|
Подтвердите правило:
Результаты подтверждают, что 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, чтобы наверстать упущенное после перезагрузки или простоя.
Эти скрипты могут показаться простыми на первый взгляд, но они выполняют критически важные задачи, которые поддерживают стабильность, безопасность и бесперебойную работу серверов.
Вы также можете поделиться статьей со своими друзьями в социальных сетях, которым может быть интересна эта статья, или просто оставить комментарий ниже. Спасибо.