أتمتة النسخ الاحتياطي لـ PostgreSQL إلى Cloudflare R2
المشكلة باختصار
أغلب المشاريع الصغيرة والمتوسطة عندها backup strategy من اتنين: إما cronjob بيعمل pg_dump على نفس السيرفر (وده بيموت مع السيرفر لو وقع)، أو خدمة مُدارة بتاخد 30 دولار شهريًا على قاعدة 10GB. الطريقتين غلط. الأولى هشة، والتانية مبالَغ فيها.
الحل اللي هنبنيه: سكربت bash واحد، بينسخ القاعدة، بيضغطها، بيشفّرها، بيرفعها على bucket في Cloudflare R2، بيتحقق من الـ checksum، وبيبعت تنبيه لو في مشكلة. التكلفة الكاملة لقاعدة 10GB مع 30 نسخة يومية محفوظة: أقل من 5 دولار شهريًا.
يعني إيه "نسخة احتياطية" أصلاً؟ — مثال للمبتدئ
تخيّل إنك بتكتب كراسة مذكرات كل يوم. في آخر اليوم بتصوّرها بالموبايل وبتبعت الصور لنفسك على Google Drive. لو الكراسة ضاعت، مش مشكلة، الصور موجودة. ده بالظبط اللي بيحصل مع الـ backup:
- الكراسة = قاعدة البيانات الحيّة (PostgreSQL).
- صورة الكراسة = ملف الـ dump (snapshot للحظة معينة).
- Google Drive = Cloudflare R2 (storage بعيد عن السيرفر الأصلي).
علميًا: pg_dump بيطلع ملف فيه كل الـ DDL (هيكل الجداول) والـ DML (البيانات نفسها) في صيغة يقدر pg_restore يعيدها كاملة. الملف ده consistent snapshot — حتى لو القاعدة شغالة وبيتكتب فيها أثناء الـ dump، النتيجة بتبقى متسقة على لحظة بداية العملية.
ليه Cloudflare R2 بالذات؟
التخزين السحابي ليه 3 تكاليف: الـ storage شهريًا، الـ requests، والـ egress (بتدفع لما تنزّل الملف برّه). الـ egress هو اللي بيوجع في AWS S3 — حوالي 9 سنت لكل GB.
R2 بيلغي الـ egress بالكامل. لما تيجي تسترجع نسخة احتياطية حجمها 10GB من S3، بتدفع 0.90 دولار مرة واحدة. من R2، بتدفع صفر. ده مش فرق صغير في سيناريو كارثة لما بتسترجع يوميًا عشرة نسخ لحد ما تلاقي السليمة.
- Storage: 1.5 سنت لكل GB شهريًا (أرخص من S3 Standard بحوالي 35%).
- Egress: صفر.
- API: S3-compatible، يعني أي tool بيشتغل مع S3 هيشتغل مع R2 من غير تغيير يُذكر.
السكربت الكامل — خطوة بخطوة
قبل ما تبدأ، هتحتاج 3 حاجات: postgresql-client، rclone (أداة المزامنة)، و R2 bucket متعمل + Access Key.
1) إعداد rclone للاتصال بـ R2
# ثبّت rclone
curl https://rclone.org/install.sh | sudo bash
# اعمل config جديد
rclone config
# اختر n (new remote)
# الاسم: r2
# النوع: s3
# provider: Cloudflare
# access_key_id + secret_access_key من R2 dashboard
# endpoint: https://<ACCOUNT_ID>.r2.cloudflarestorage.com
2) السكربت الأساسي
#!/bin/bash
set -euo pipefail
# الإعدادات
DB_NAME="production_db"
DB_USER="postgres"
R2_BUCKET="db-backups"
RETENTION_DAYS=30
SLACK_WEBHOOK="https://hooks.slack.com/services/xxx/yyy/zzz"
# متغيرات الوقت
TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_FILE="/tmp/${DB_NAME}_${TIMESTAMP}.dump"
CHECKSUM_FILE="${BACKUP_FILE}.sha256"
notify_fail() {
curl -s -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"❌ Backup failed: $1\"}"
exit 1
}
# 1) خُد snapshot بصيغة custom (مضغوط + قابل للـ restore الانتقائي)
pg_dump -U "$DB_USER" -Fc -Z 9 -f "$BACKUP_FILE" "$DB_NAME" \
|| notify_fail "pg_dump failed"
# 2) تحقق إن الملف مش فاضي وإنه صالح للـ restore
pg_restore --list "$BACKUP_FILE" > /dev/null \
|| notify_fail "dump file corrupted"
# 3) احسب checksum لتوثيق السلامة
sha256sum "$BACKUP_FILE" > "$CHECKSUM_FILE"
# 4) ارفع الـ dump والـ checksum على R2
rclone copy "$BACKUP_FILE" "r2:${R2_BUCKET}/daily/" \
|| notify_fail "rclone upload failed"
rclone copy "$CHECKSUM_FILE" "r2:${R2_BUCKET}/daily/"
# 5) امسح النسخ الأقدم من 30 يوم
rclone delete --min-age "${RETENTION_DAYS}d" "r2:${R2_BUCKET}/daily/"
# 6) نظّف المحلي
rm -f "$BACKUP_FILE" "$CHECKSUM_FILE"
# 7) نجاح
curl -s -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"✓ Backup OK: ${DB_NAME} ${TIMESTAMP}\"}"
3) جدولة السكربت مع cron
# نفّذ يوميًا الساعة 3 فجرًا
crontab -e
# ضيف السطر ده
0 3 * * * /usr/local/bin/pg_backup.sh >> /var/log/pg_backup.log 2>&1
التحقق من أن النسخة فعلًا سليمة
قاعدة محفورة: نسخة احتياطية ما جرّبتهاش مش نسخة احتياطية. السكربت فوق بيعمل validation أساسي بـ pg_restore --list، لكن ده مش كفاية لوحده. مرة كل أسبوع اعمل restore فعلي على قاعدة staging:
# نزّل آخر نسخة من R2
rclone copy "r2:db-backups/daily/$(rclone lsf r2:db-backups/daily/ --files-only | sort | tail -1)" /tmp/
# ارجعها لـ DB جديد
createdb staging_restore_test
pg_restore -d staging_restore_test /tmp/production_db_*.dump
# شغّل استعلام تحقّق
psql staging_restore_test -c "SELECT COUNT(*) FROM users;"
لو العدد قريب من قاعدة الإنتاج، النسخة سليمة. لو صفر أو رقم غريب، عندك مشكلة في الـ dump لازم تتصلح فورًا.
الأرقام الفعلية — before/after
- قاعدة 10GB (قبل الضغط): الـ dump بصيغة
-Fc -Z 9بيطلع حوالي 2.1GB. توفير 79% في الحجم. - التكلفة الشهرية: 30 نسخة × 2.1GB = 63GB × 0.015$ = 0.95 دولار.
- زمن الـ dump: على VM بـ 4 CPU + 8GB RAM، حوالي 90 ثانية لقاعدة 10GB (على NVMe).
- زمن الرفع على R2: من سيرفر في أوروبا، متوسط 40 ثانية لـ 2.1GB.
- زمن الاسترجاع الكامل في كارثة: تنزيل +
pg_restore= حوالي 4 دقائق.
trade-offs صريحة
المكاسب: تكلفة أقل بـ 85% من S3 مع egress، RTO (Recovery Time Objective) ممتاز لمشروع متوسط، مفيش vendor lock-in لأن R2 متوافق مع S3 API.
التكاليف: pg_dump single-threaded على صيغة custom — يعني لو قاعدتك 500GB، هياخد ساعات. لازم تحوّل لـ directory format مع -j N لاستخدام parallel. كمان، الـ dump بياخد lock خفيف (ACCESS SHARE) على الجداول، مش هيعطّل الكتابة لكن هيزوّد I/O. لو عندك replica، شغّل الـ dump من عليها مش من الـ primary.
الافتراض هنا إن حجم قاعدتك ≤ 100GB وإنك بتحتاج RPO (Recovery Point Objective) ≤ 24 ساعة. تحت ده، الطريقة ممتازة. فوق ده، تحتاج حل مختلف.
متى لا تستخدم هذه الطريقة
- قواعد أكبر من 500GB: استخدم
pgBackRestأوWAL-Gمع incremental backups بدل full daily dumps. - RPO أقل من ساعة:
pg_dumpمش كفاية. تحتاج continuous archiving مع WAL shipping. - متطلبات compliance صارمة (HIPAA, PCI-DSS): R2 لسه مش معتمد في كل الـ frameworks. راجع مع الـ auditor.
- قاعدة multi-terabyte مع كتابة مستمرة: الـ dump هيطوّل لدرجة إن الـ snapshot ممكن يبقى قديم وقت ما بيخلص.
الخطوة التالية
انسخ السكربت، غيّر الـ 3 متغيرات في الأول (DB_NAME, R2_BUCKET, SLACK_WEBHOOK)، شغّله يدويًا مرة للتأكد، وبعدين ضيفه على cron. في نهاية اليوم الأول، روح R2 dashboard وشوف الملف موجود فعلًا. في نهاية الأسبوع، اعمل restore test على staging. لو الاستعلام رجّع العدد المتوقع، إنت بقيت في أمان فعلي.