◀ العودة إلى المدونة
Linux

النسخ الاحتياطي التلقائي للخادم: MySQL والملفات وrestic

نشر في 11 Aug 2026· 7 min قراءة
#Linux#Sauvegarde#MySQL#restic#systemd

النسخة الاحتياطية التي لم تُستعَد قط غير موجودة

الجميع "لديه نسخ احتياطية" إلى أن يأتي يوم الاستعادة: التفريغ فارغ منذ ثلاثة أشهر، أو الأرشيف موجود على قرص الخادم نفسه، أو لا أحد يعرف كلمة مرور التشفير. الاستراتيجية الجيدة تختصرها قاعدة 3-2-1: ثلاث نسخ من البيانات، على وسيطين مختلفين، إحداها خارج الموقع. ويختصرها انضباط واحد: اختبار الاستعادة.

يُعدّ هذا الدليل على خادم Linux يستضيف تطبيق ويب ما يلي:

  • تفريغاً متّسقاً لقاعدة MySQL دون تعطيل التطبيق؛
  • نسخاً احتياطياً مشفّراً وخالياً من التكرار وتزايدياً عبر restic إلى تخزين كائنات S3؛
  • سياسة احتفاظ وجدولة عبر systemd وتنبيهاً عند الفشل.

1. تفريغ MySQL

نسخ ملفات /var/lib/mysql أثناء عمل الخادم يعطي نسخة غير متّسقة. استخدم mysqldump مع الخيار --single-transaction: بالنسبة لجداول InnoDB يُؤخذ التفريغ داخل معاملة (transaction)، فيكون متّسقاً دون قفل عمليات الكتابة.

أولاً، مستخدم مخصص للنسخ الاحتياطي بالحد الأدنى من الصلاحيات:

CREATE USER 'backup'@'localhost' IDENTIFIED BY 'long-password';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, PROCESS ON *.* TO 'backup'@'localhost';

توضع بيانات اعتماده في ملف لا يقرؤه إلا root، حتى لا تظهر أبداً في قائمة العمليات:

# /root/.my-backup.cnf  (chmod 600)
[client]
user=backup
password=long-password
mysqldump --defaults-extra-file=/root/.my-backup.cnf \
  --single-transaction --quick --routines --triggers --events \
  --databases app | gzip > /var/backups/mysql/app.sql.gz

يقرأ الخيار --quick الصفوف واحداً تلو الآخر بدلاً من تحميل كل جدول في الذاكرة، وهذا ضروري للجداول الكبيرة. وبعد بضع عشرات من الغيغابايتات انتقل إلى نسخ احتياطي فيزيائي (Percona XtraBackup أو MySQL Enterprise Backup).

2. لماذا restic

الأرشيف اليومي بصيغة tar.gz ينسخ كل شيء كل يوم. أما restic فيقسّم الملفات إلى كتل، ويخزّن كل كتلة مرة واحدة فقط، ويشفّر كل شيء على جهازك (AES-256) قبل الإرسال. النتيجة: نسخ يومية لا تكلّف إلا حجم التغييرات، وتخزين بعيد لا يرى بياناتك مكشوفة أبداً، وإمكانية الاستعادة إلى أي نقطة زمنية محفوظة.

sudo apt install -y restic

توضع إعدادات المستودع في ملف بيئة محمي هو /etc/restic/env (chmod 600). ويصلح أي تخزين متوافق مع S3 (AWS وScaleway وOVH وBackblaze B2 وCloudflare R2…):

RESTIC_REPOSITORY=s3:https://s3.fr-par.scw.cloud/my-backup-bucket/web-server
RESTIC_PASSWORD_FILE=/etc/restic/password
AWS_ACCESS_KEY_ID=xxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxx
sudo sh -c 'openssl rand -base64 48 > /etc/restic/password && chmod 600 /etc/restic/password'
sudo sh -c 'set -a; . /etc/restic/env; restic init'

احتفظ بكلمة مرور restic خارج الخادم في مدير كلمات مرور. بدونها لا يمكن استرجاع المستودع، وهذا مقصود. فإذا احترق الخادم ومعه النسخة الوحيدة من كلمة المرور، احترقت نسخك الاحتياطية معه.

3. سكربت النسخ الاحتياطي

الملف /usr/local/bin/backup.sh:

#!/usr/bin/env bash
set -euo pipefail

set -a; . /etc/restic/env; set +a

DUMP_DIR=/var/backups/mysql
mkdir -p "$DUMP_DIR"

# 1. MySQL dump (temporary file, then rename: never a half-written dump)
mysqldump --defaults-extra-file=/root/.my-backup.cnf \
  --single-transaction --quick --routines --triggers --events \
  --databases app | gzip > "$DUMP_DIR/app.sql.gz.tmp"
mv "$DUMP_DIR/app.sql.gz.tmp" "$DUMP_DIR/app.sql.gz"

# 2. Back up the dumps, the uploaded files and the configuration
restic backup \
  "$DUMP_DIR" \
  /var/www/app/shared \
  /etc/nginx /etc/php \
  --tag daily \
  --exclude-caches

# 3. Retention: 7 days, 4 weeks, 6 months
restic forget --tag daily --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

# 4. Verify a sample of the data
restic check --read-data-subset=5%

السطر set -euo pipefail حاسم: بدون pipefail يمرّ فشل mysqldump متبوعاً بنجاح gzip دون أن يلاحظه أحد، فتنسخ ملفاً فارغاً لأشهر.

sudo chmod 700 /usr/local/bin/backup.sh

4. الجدولة بمؤقّت systemd

لمؤقّت systemd ميزتان مقارنة بـ cron: السجلات تذهب إلى journald، والخيار Persistent=true يعوّض أي تنفيذ فائت إذا كان الخادم مطفأً.

# /etc/systemd/system/backup.service
[Unit]
Description=MySQL + files backup to restic
After=network-online.target mysql.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup

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

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer      # next run
sudo systemctl start backup.service     # first manual run
journalctl -u backup.service -e         # logs

5. التنبيه عند الفشل

النسخة الاحتياطية التي تفشل بصمت هي أسوأ سيناريو. أبسط حل هو خدمة من نوع "dead man's switch" (مثل Healthchecks.io أو Uptime Kuma أو Better Stack): يرسل السكربت إشارة (ping) عند كل نجاح، وتنبّهك الخدمة إذا لم يصلها شيء في المهلة المتوقعة. أضف في نهاية backup.sh:

curl -fsS -m 10 --retry 3 https://hc-ping.com/your-uuid > /dev/null

بفضل set -e لا تُرسَل الإشارة إلا إذا نجحت كل الخطوات السابقة.

6. اختبار الاستعادة (فعلياً)

خطّط لاختبار استعادة منتظم، شهرياً مثلاً، على جهاز آخر:

set -a; . /etc/restic/env; set +a

restic snapshots --tag daily                       # list the backups
restic restore latest --target /tmp/restore        # latest version
restic restore latest --target /tmp/restore --include /var/www/app/shared/uploads

# Restore the database into a test database
mysql -e 'CREATE DATABASE app_restore_test'
zcat /tmp/restore/var/backups/mysql/app.sql.gz \
  | sed 's/`app`/`app_restore_test`/g' | mysql
mysql -e 'SELECT COUNT(*) FROM app_restore_test.user'

احسب الوقت الذي تستغرقه العملية: إنه RTO الحقيقي لديك (الوقت اللازم للعودة إلى الخدمة). وعمر آخر نسخة ناجحة هو RPO (كمية البيانات التي تقبل خسارتها). إذا لم تناسب هاتان القيمتان احتياجات العمل، فالآن هو وقت اكتشاف ذلك، لا أثناء حادث.

باختصار

  • تفريغ متّسق بالخيار --single-transaction، وبيانات اعتماد بعيدة عن سطر الأوامر
  • restic: مشفّر، بلا تكرار، خارج الموقع، مع سياسة احتفاظ واضحة
  • جدولة systemd مع Persistent=true، وتنبيه إذا لم تصل الإشارة
  • كلمة مرور المستودع محفوظة في مكان آخر غير الخادم
  • استعادة تُختبر بانتظام ويُقاس وقتها