المستوى: محترف. هذا الشرح مبني على فرضية إن عندك قاعدة PostgreSQL في الإنتاج، نسخ احتياطية تلقائية شغّالة فعلًا، وسيرفر واحد على الأقل تقدر تشغّل عليه حاوية مؤقتة للاختبار.
لو بتاخد backup كل ليلة وعمرك ما جرّبت تستعيد واحدة، انت مش مأمّن. انت بتقامر إن الملفات دي سليمة يوم ما تحتاجها. آخر المقال هيبقى عندك بايبلاين بيستعيد أحدث نسخة تلقائيًا كل ليلة ويقولك برقم: النسخة دي تنفع ولا لأ.
النسخة الاحتياطية اللي معملتش لها استعادة مش نسخة احتياطية
المشكلة باختصار
الـ backup اللي مجربتوش هو Schrödinger's backup: حالته سليم وتالف في نفس الوقت لحد ما تفتحه. اللي بيحصل فعلاً إن الملف بيتكتب كل ليلة، الـ cron بيرجّع exit 0، وكله مبسوط. لكن الملف نفسه ممكن يكون ناقص، أو متشفّر بمفتاح ضاع، أو نسخة pg_dump اختلفت عن نسخة السيرفر. مفيش حد بيكتشف ده إلا ساعة الكارثة، وساعتها بيبقى فات الأوان.
المفهوم أولًا بمثال، ثم بدقة
تخيّل إطفائية عندها 12 طفاية حريق معلّقة على الحيط. شكلها تمام والعدّاد بيقول مليانة. بس محدش ضغط على واحدة منهم من سنتين. يوم الحريق الحقيقي تكتشف إن 4 منهم فاضية والضغط راح. الطفاية اللي متجرّبتش مش وسيلة أمان، دي ديكور بيطمّنك غلط.
النسخة الاحتياطية بالظبط زي كده. علميًا: صحة الـ backup مش خاصية بتتحدد وقت الكتابة، دي خاصية بتتحدد وقت الاستعادة. الـ backup سليم لما تقدر تعمل منه restore كامل يوصل الداتا لحالة قابلة للاستخدام قبل انتهاء زمن التعافي المسموح (RTO). أي حاجة أقل من كده مجرد ملف على القرص، مش ضمان.
عشان كده الأتمتة الصح مش "خُد نسخة كل ليلة". الأتمتة الصح "استعِد نسخة كل ليلة على بيئة معزولة، وشغّل استعلام يثبت إن الداتا موجودة، وقيس الزمن".
الحل: بايبلاين استعادة ليلي في 5 خطوات
- هات أحدث ملف dump من مخزن النسخ (S3 أو قرص محلي).
- قوم بحاوية PostgreSQL نضيفة ومؤقتة، معزولة تمامًا عن الإنتاج.
- اعمل
pg_restoreللملف جوّه الحاوية دي. - شغّل استعلام تحقق (smoke test): عدّ صفوف جدول حسّاس وتأكد إنه أكبر من حد أدنى منطقي.
- سجّل النتيجة وزمن الاستعادة، وابعت تنبيه على Slack لو أي خطوة فشلت.
#!/usr/bin/env bash
# nightly-restore-drill.sh — يستعيد أحدث نسخة ويتحقق منها في حاوية معزولة
set -euo pipefail
BACKUP_DIR="/backups/pg"
LATEST=$(ls -t "$BACKUP_DIR"/*.dump | head -n1)
CONTAINER="restore-drill-$$"
MIN_ROWS=1000 # حد أدنى منطقي لجدول orders
SLACK_URL="https://hooks.slack.com/services/XXX/YYY/ZZZ"
alert() { curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"❌ فشل التحقق من الاستعادة: $1\"}" "$SLACK_URL"; }
start=$(date +%s)
# 1) حاوية مؤقتة معزولة
docker run -d --name "$CONTAINER" -e POSTGRES_PASSWORD=drill \
postgres:16 >/dev/null
sleep 8 # انتظر إقلاع الخادم
# 2) استعادة أحدث نسخة
if ! docker exec -i "$CONTAINER" pg_restore -U postgres -d postgres \
--no-owner < "$LATEST"; then
alert "pg_restore رجع خطأ على الملف $LATEST"
docker rm -f "$CONTAINER" >/dev/null; exit 1
fi
# 3) استعلام تحقق حقيقي — مش مجرد اتصال
ROWS=$(docker exec "$CONTAINER" psql -U postgres -tAc \
"SELECT count(*) FROM orders;")
if [ "$ROWS" -lt "$MIN_ROWS" ]; then
alert "جدول orders رجع $ROWS صف فقط — النسخة ناقصة على الأرجح"
docker rm -f "$CONTAINER" >/dev/null; exit 1
fi
end=$(date +%s)
echo "✅ الاستعادة سليمة: $ROWS صف، RTO الفعلي = $((end-start)) ثانية"
# 4) نضّف الحاوية المؤقتة
docker rm -f "$CONTAINER" >/dev/null