پرش به محتویات

۱۱. فشرده‌سازی و پشتیبان‌گیری

بکاپی که بازیابی‌اش را تست نکرده‌اید، بکاپ نیست؛ امید است. این فصل هم ابزارها و هم رویهٔ اعتمادپذیر کردن بکاپ را پوشش می‌دهد.

راهنمای سریع

فرمان کاربرد ریسک
tar -tzf / unzip -l / sha256sum -c بررسی آرشیو و صحت بدون تغییر
tar -czf / zip / rsync -n ساخت آرشیو / تست همگام‌سازی تغییر
rsync -a کپی و همگام‌سازی تغییر
tar -x / unzip / rsync به مسیر زنده بازیابی تغییر (روی داده‌ٔ جاری: مخرب)
rsync --delete همگام‌سازی با حذف مخرب
restic forget --prune / borg prune حذف نسخه‌های قدیمی مخرب

قانون ۳-۲-۱

  • ۳ نسخه از داده (اصلی + دو بکاپ)
  • روی ۲ نوع رسانه/سامانهٔ متفاوت
  • ۱ نسخه بیرون از سایت (و ترجیحاً غیرقابل‌تغییر / append-only تا باج‌افزار نتواند پاکش کند)

مفاهیم: RPO = حداکثر داده‌ای که می‌پذیرید از دست بدهید (فاصلهٔ بکاپ‌ها)؛ RTO = حداکثر زمانی که بازیابی طول می‌کشد. هر دو را بنویسید و اندازه بگیرید.

tar و gzip

umask 077                                                  # بکاپ فقط برای مالک
tar --xattrs --acls --numeric-owner -czpf backup-etc-$(date +%F).tar.gz /etc
tar -tzf backup-etc-2026-01-01.tar.gz | head
tar -xzpf backup.tar.gz -C /restore/path
tar --exclude='*.tmp' --exclude='cache' -czf app.tar.gz /opt/app
gzip -k file.log          # -k: فایل اصلی را نگه دار
gunzip file.log.gz
  • --numeric-owner مالکیت را به‌صورت UID عددی ذخیره می‌کند (در بازیابی روی سیستم دیگر، نام‌ها اشتباه نگاشت نمی‌شوند)؛ -p مجوزها را حفظ می‌کند؛ --acls --xattrs ACL و SELinux را هم.
  • بکاپ /etc حاوی کلیدها و رمزهاست؛ مجوز آن را 600 و رمزگذاری‌شده نگه دارید.

قبل از استخراج آرشیو ناشناس، محتویات آن را با tar -tzf بررسی کنید (مسیرهای / یا ../) و از استخراج به مسیرهای حساس خودداری کنید.

zip و unzip

zip -r project.zip project/
unzip -l project.zip
unzip project.zip -d /restore/path

رمز zip -e استاندارد ضعیف است؛ برای محرمانگی از age/gpg استفاده کنید.

rsync

rsync -aHAXn --numeric-ids /srv/data/ /backup/data/          # -n: خشک (dry-run)
rsync -aHAX  --numeric-ids /srv/data/ /backup/data/
rsync -avh --delete --dry-run /srv/data/ /backup/data/

-aHAX = آرشیو + hardlink + ACL + xattr. --dry-run تغییرات احتمالی را نشان می‌دهد. --delete فایل‌هایی را که در مبدأ نیستند از مقصد حذف می‌کند؛ ابتدا dry-run و بررسی مسیرها ضروری است.

مرگبارترین اشتباه rsync --delete

اگر مبدأ mount نبود یا خالی بود (مثلاً NFS قطع شد)، rsync --delete بکاپ را هم خالی می‌کند. در اسکریپت قبل از اجرا حتماً بررسی کنید: mountpoint -q /srv/data || exit 1 و [ -n "$(ls -A /srv/data)" ] || exit 1. ترجیحاً از ابزار نسخه‌دار (restic/borg) یا --link-dest استفاده کنید تا حذف، نسخهٔ قدیمی را نابود نکند.

snapshot چرخشی با hardlink (فضا فقط برای تغییرات):

today=$(date +%F)
rsync -aHAX --numeric-ids --link-dest=/backup/latest /srv/data/ /backup/$today/ \
  && ln -sfn /backup/$today /backup/latest

کپی از راه دور

rsync -avh -e ssh /srv/data/ backupuser@BACKUP_HOST:/backup/server1/

حساب پشتیبان را با حداقل مجوز لازم تنظیم کنید و دسترسی حذف/بازنویسی را محدود سازید (command= در authorized_keys یا حالت append-only). مهاجمی که سرور اصلی را بگیرد نباید بتواند بکاپ را هم پاک کند: سرور بکاپ باید بکاپ را «بکشد» (pull) یا فقط اجازهٔ افزودن داشته باشد.

ابزارهای نسخه‌دار و رمزگذاری‌شده: restic و borg

نسبت به tar/rsync: deduplication، رمزگذاری، نسخه‌های متعدد، بررسی صحت و حذف سیاست‌محور.

# restic
export RESTIC_REPOSITORY=/backup/restic-repo     # یا sftp:user@host:/path ، s3:...
export RESTIC_PASSWORD_FILE=/root/.restic-pass   # مجوز 600
restic init
restic backup /srv/data /etc --exclude-caches
restic snapshots
restic check                                      # صحت ساختار
restic check --read-data-subset=5%                # بخشی از داده را هم بخوان
restic restore latest --target /restore/test
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

# borg
borg init --encryption=repokey-blake2 /backup/borg-repo
borg create --stats /backup/borg-repo::'{hostname}-{now:%Y-%m-%d}' /srv/data /etc
borg list /backup/borg-repo
borg check /backup/borg-repo
borg prune --keep-daily 7 --keep-weekly 4 --keep-monthly 6 /backup/borg-repo

رمز/کلید مخزن را جدا از سرور نگه دارید

بدون رمز restic یا کلید borg، بکاپ بی‌مصرف است. نسخهٔ رمز را در گاوصندوق رمز یا جایی خارج از سرور نگه دارید و بازیابی با آن را آزمایش کنید.

بکاپ پایگاه‌داده

کپی خام فایل‌های دیتابیس زنده معمولاً ناسازگار است. از ابزار خود دیتابیس استفاده کنید:

# PostgreSQL
sudo -u postgres pg_dump -Fc -f /backup/db-$(date +%F).dump DBNAME
sudo -u postgres pg_dumpall --globals-only > /backup/globals-$(date +%F).sql   # نقش‌ها
pg_restore --list /backup/db-2026-01-01.dump | head        # بررسی خوانا بودن

# MySQL / MariaDB
mysqldump --single-transaction --routines --triggers --events DBNAME | gzip > /backup/db-$(date +%F).sql.gz
gzip -t /backup/db-2026-01-01.sql.gz

رمز را در خط فرمان نگذارید؛ از ~/.my.cnf (600) یا ~/.pgpass (600) استفاده کنید. برای حجم بزرگ: snapshot LVM/ZFS یا replica + dump از replica.

رمزگذاری

age -r AGE_PUBLIC_KEY -o backup.tar.gz.age backup.tar.gz        # با کلید عمومی
age -d -i key.txt backup.tar.gz.age > backup.tar.gz
gpg --symmetric --cipher-algo AES256 backup.tar.gz               # با رمز

بررسی پشتیبان

sha256sum backup.tar.gz > SHA256SUMS
sha256sum -c SHA256SUMS
tar -tzf backup.tar.gz >/dev/null && echo "آرشیو سالم"

وجود فایل پشتیبان به معنی قابل‌بازیابی بودن آن نیست. بازیابی آزمایشی، ثبت مدت بازیابی و بررسی سازگاری برنامه/دیتابیس ضروری است.

تمرین بازیابی (ماهانه، مستند):

  1. یک بکاپ تصادفی را روی ماشین/پوشهٔ جدا بازیابی کنید.
  2. دیتابیس را بالا بیاورید؛ چند رکورد/فایل مشخص را بررسی کنید.
  3. زمان صرف‌شده را ثبت کنید (RTO واقعی).
  4. نتیجه را در گزارش بنویسید؛ اگر شکست خورد، تیکت فوری بزنید.

اسکریپت نمونهٔ بکاپ (قابل‌اتکا)

#!/usr/bin/env bash
# /usr/local/sbin/backup-data.sh   (نمونه — مسیرها را تطبیق دهید)
set -Eeuo pipefail
umask 077

SRC=/srv/data
DEST=/backup/data
LOCK=/var/lock/backup-data.lock
TAG=backup-data

log() { logger -t "$TAG" -p user.notice -- "$*"; echo "$(date -Is) $*"; }
fail() { logger -t "$TAG" -p user.err -- "FAILED: $*"; echo "$(date -Is) FAILED: $*" >&2; exit 1; }
trap 'fail "خطا در خط $LINENO"' ERR

exec 9>"$LOCK"
flock -n 9 || fail "نمونهٔ دیگری در حال اجراست"

mountpoint -q "$SRC"  || fail "$SRC mount نیست"          # جلوگیری از بکاپ/--delete روی مبدأ خالی
[ -n "$(ls -A "$SRC")" ] || fail "$SRC خالی است"
mountpoint -q "$DEST" || fail "$DEST mount نیست"

log "شروع"
rsync -aHAX --numeric-ids --link-dest="$DEST/latest" "$SRC/" "$DEST/$(date +%F)/"
ln -sfn "$DEST/$(date +%F)" "$DEST/latest"
KEEP=14   # آخرین ۱۴ نسخه نگه داشته می‌شود، نه «نسخه‌های جدیدتر از ۱۴ روز»:
          # اگر چند هفته بکاپ شکست بخورد، با حذف بر اساس سن همهٔ نسخه‌های سالم پاک می‌شدند
mapfile -t old < <(find "$DEST" -maxdepth 1 -type d -name '20*' | sort | head -n -"$KEEP")
[ "${#old[@]}" -eq 0 ] || rm -rf -- "${old[@]}"
log "پایان موفق"

# هشدار «مرگ‌ضربانی»: اگر این پیام نیاید، مانیتورینگ باید هشدار بدهد
# curl -fsS -m 10 https://monitoring.example.internal/ping/backup-data >/dev/null || true
sudo install -m 750 backup-data.sh /usr/local/sbin/
sudo /usr/local/sbin/backup-data.sh && echo "OK"
# زمان‌بندی: cron یا systemd timer ([زمان‌بندی](13-scheduling.md))

نظارت: شکست بکاپ بی‌صدا رایج‌ترین خطاست. «نبود سیگنال موفقیت» را هشدار کنید (heartbeat)، نه فقط «وجود خطا».

تست خودکار بازیابی و تازگی بکاپ (restic)

بکاپ بی‌صدا شکست می‌خورد؛ این اسکریپت قدیمی‌بودن بکاپ و قابل‌بازیابی بودن آن را هر شب می‌سنجد (آزموده‌شده با restic 0.16 در سه حالت: سالم، snapshot قدیمی، مخزن خالی). jq لازم است.

#!/usr/bin/env bash
# /usr/local/sbin/restic-verify.sh   (نمونه — مسیرها و آستانه را تطبیق دهید)
set -Eeuo pipefail
export RESTIC_REPOSITORY=/backup/restic-repo
export RESTIC_PASSWORD_FILE=/root/.restic-pass
MAX_AGE_H=26                       # بکاپ شبانه + کمی تلورانس
TARGET=$(mktemp -d /var/tmp/restic-verify.XXXXXX)
trap 'rm -rf "$TARGET"' EXIT
fail() { logger -t restic-verify -p user.err -- "FAILED: $*"; echo "FAILED: $*" >&2; exit 1; }

last=$(restic snapshots --json | jq -r 'map(.time) | max // empty')
[ -n "$last" ] || fail "هیچ snapshot ای وجود ندارد"
age_h=$(( ( $(date +%s) - $(date -d "$last" +%s) ) / 3600 ))
[ "$age_h" -le "$MAX_AGE_H" ] || fail "آخرین snapshot ${age_h} ساعت پیش است"

restic check --read-data-subset=5% >/dev/null || fail "restic check"
SECONDS=0
restic restore latest --target "$TARGET" --include /etc/hostname --verify >/dev/null || fail "restore --verify"
[ -s "$TARGET/etc/hostname" ] || fail "فایل شاهد در بازیابی نیست"
logger -t restic-verify -p user.notice -- "OK age=${age_h}h sample_restore=${SECONDS}s"
  • restic مسیر کامل را زیر --target بازسازی می‌کند ($TARGET/etc/hostname)؛ برای همین فایل شاهد با مسیر مطلق بررسی می‌شود.
  • زمان نمونه‌بازیابی، RTO واقعی نیست؛ بازیابی کامل ماهانه را هم مطابق «تمرین بازیابی» انجام دهید.
  • --read-data-subset=5% فقط بخشی از داده را می‌خواند. برای پوشش کامل و قابل‌پیش‌بینی، از شکل --read-data-subset=N/20 استفاده کنید و N را با روز ماه بچرخانید تا داده‌ها در یک چرخه کامل خوانده شوند.

بکاپ فقط‌افزودنی (append-only)

هدف: مهاجمی که سرور اصلی را بگیرد نتواند بکاپ‌ها را پاک کند. روی سرور بکاپ، کلید SSH کلاینت را به یک فرمان ثابت محدود کنید:

# ~backup/.ssh/authorized_keys (روی سرور بکاپ)
command="borg serve --append-only --restrict-to-path /backup/borg/server1",restrict ssh-ed25519 AAAA... backup@server1

در این حالت borg prune از سمت کلاینت هم چیزی را واقعاً پاک نمی‌کند؛ حذف نسخه‌های قدیمی (prune/compact) را از یک ماشین مدیریتی مورد اعتماد با دسترسی کامل انجام دهید. معادل restic: rest-server --append-only --private-repos. این بخش در sandbox آزموده نشده؛ بعد از راه‌اندازی، تلاش برای پاک کردن از سمت کلاینت را عمداً امتحان کنید و شکست‌خوردنش را ببینید.

زمان‌بندی با systemd و هشدار شکست

نسخهٔ service/timer برای اسکریپت بالا (با systemd-analyze verify بررسی شد):

# /etc/systemd/system/backup-data.service
[Unit]
Description=Data backup
OnFailure=backup-failed@%n.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/backup-data.sh
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/backup-failed@.service  — به‌جای logger، ایمیل/وب‌هوک واقعی بگذارید
[Unit]
Description=Alert: %i failed

[Service]
Type=oneshot
ExecStart=/usr/bin/logger -t backup-alert -p user.crit "unit %i failed"
# /etc/systemd/system/backup-data.timer
[Unit]
Description=Daily data backup

[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=10m
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup-data.timer
systemctl list-timers backup-data.timer
sudo systemctl start backup-data.service && journalctl -u backup-data -n 20     # یک بار دستی امتحان کنید

OnFailure= فقط وقتی اجرا شود که سرویس شکست بخورد؛ اگر timer اصلاً اجرا نشود (غیرفعال، ساعت اشتباه) هشداری نمی‌آید. به همین دلیل heartbeat یا بررسی «تازگی بکاپ» (اسکریپت بالا) هم لازم است.

دیتابیس: بازیابی آزمایشی و بازیابی نقطه‌ای

این بخش در sandbox آزموده نشده؛ روی ماشین آزمایشی امتحان کنید.

# تست بازیابی dump روی دیتابیس موقت (نه روی production)
createdb restore_test
pg_restore -d restore_test --no-owner /backup/db-2026-01-01.dump
psql -d restore_test -c 'SELECT count(*) FROM important_table;'
dropdb restore_test

# بکاپ پایه برای بازیابی تا لحظهٔ دلخواه (PITR) — همراه با آرشیو WAL (archive_mode / archive_command)
pg_basebackup -D /backup/pgbase -Ft -z -X stream -P

dump منطقی فقط تا لحظهٔ خودش بازمی‌گردد. اگر RPO شما از «شبانه» کمتر است، آرشیو WAL (PostgreSQL) یا binlog (MySQL/MariaDB) لازم است و بازیابی تا یک لحظهٔ مشخص باید تمرین شود.

جدول RPO/RTO سرویس‌ها

این جدول را برای هر سرویس مهم پر کنید و ماهانه بازبینی کنید؛ «آخرین تست» خالی یعنی آن سرویس بکاپ ندارد.

سرویس RPO RTO روش نسخهٔ بیرونی آخرین تست بازیابی مالک
(نام سرویس) ۱ ساعت ۲ ساعت (مثلاً WAL + dump شبانه) (محل) (تاریخ) (نام)

خطاهای رایج

نشانه علت احتمالی روش بررسی
بکاپ بی‌خطا ولی ناقص مبدأ mount نبود findmnt ؛ شرط mountpoint -q در اسکریپت
بازیابی دیتابیس خراب کپی خام فایل زنده pg_dump/mysqldump --single-transaction
tar: Removing leading '/' عادی؛ مسیر مطلق نسبی ذخیره شد بازیابی با -C
مالکیت بعد از بازیابی اشتباه نام کاربر ≠ UID در سیستم جدید --numeric-owner
فضای بکاپ پر شد نگهداری/prune نیست سیاست retention ؛ restic forget --prune
بکاپ هست ولی رمز گم شد کلید مخزن جدا نگه‌داری نشده گاوصندوق رمز؛ تست بازیابی
cron بکاپ اجرا نمی‌شود PATH/مجوز/ساعت زمان‌بندی
همهٔ نسخه‌های قدیمی پاک شده‌اند retention بر اساس سن + چند هفته شکست نگهداری بر اساس تعداد؛ هشدار تازگی
prune از کلاینت کار می‌کند بکاپ append-only نیست authorized_keys با --append-only

سیاست پشتیبان‌گیری

  • حداقل یک نسخه جدا از سرور اصلی (و یک نسخه بیرون از سایت) نگهداری شود.
  • نسخه‌ها رمزگذاری و دسترسی آن‌ها محدود شود.
  • دوره نگهداری و RPO/RTO مشخص باشد.
  • از دیتابیس زنده با روش سازگار با همان دیتابیس پشتیبان بگیرید.
  • بازیابی را به‌صورت دوره‌ای تمرین و مستند کنید.
  • بکاپ در برابر حذف تصادفی/باج‌افزار محافظت شود (append-only، immutable، حساب جدا).
  • /etc، فهرست بسته‌ها، کرون‌ها و پیکربندی سرویس‌ها هم بکاپ شوند، نه فقط «داده».

خلاصهٔ اجرایی

  1. ۳-۲-۱ + RPO/RTO نوشته‌شده.
  2. --delete/prune فقط با بررسی mount و dry-run.
  3. دیتابیس با ابزار خودش؛ رمز مخزن بیرون از سرور.
  4. ماهانه بازیابی را واقعاً تمرین و زمان بگیرید.
  5. عدم‌دریافت سیگنال موفقیت = هشدار.
  6. نگهداری بر اساس «تعداد نسخه» نه «سن»؛ تازگی و قابل‌بازیابی بودن بکاپ را هر شب خودکار بسنجید.