بررسی بکاپ خودکار: وقتی بی‌صدا شکست می‌خورد

بکاپ خودکاری که ماه‌ها بی‌خطا اجرا شده اما فایلش خالی است، بدترین حالت ممکن است. اینجا یاد می‌گیرید چطور شکست خاموش را قبل از فاجعه بگیرید.

۶ دقیقه به‌روزرسانی ۴ آبان ۱۴۰۵

کرون‌جاب را باز می‌کنید، خط بکاپ سر جایش است، لاگ هم پر از خطوط موفق. اما وقتی سرور را از دست می‌دهید و به فایل بکاپ نگاه می‌کنید، حجمش ۲۰ کیلوبایت است. یک فایل SQL خالی با چند خط کامنت. این دقیقاً همان حالتی است که «بررسی بکاپ خودکار» را از یک کار تشریفاتی به یک ضرورت تبدیل می‌کند. بکاپی که بی‌صدا شکست بخورد، از نداشتن بکاپ خطرناک‌تر است، چون خیال‌تان راحت است و دنبال مشکل نمی‌گردید.

چرا بکاپ خودکار بدون خطا شکست می‌خورد

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

  • رمز عبور عوض شده. کسی رمز کاربر MySQL را از پنل تغییر داده، اما ~/.my.cnf هنوز رمز قدیمی را دارد. mysqldump با خطای Access denied for user بیرون می‌آید، ولی چون خروجی به فایل ریدایرکت شده، فایل ساخته می‌شود و خالی می‌ماند.
  • دیسک پر شده. بکاپ دیروز نصفه نوشته شده و امروز اصلاً جا نداشته. اگر چرخش (rotation) درست تنظیم نشده باشد، نسخه‌های قدیمی پاک نمی‌شوند و دیسک بی‌سروصدا پر می‌شود.
  • مسیر عوض شده. دیتابیس جدیدی اضافه شده یا مسیر /var/www به /srv/app منتقل شده و اسکریپت هنوز سراغ مسیر قدیمی می‌رود. rsync روی مسیر ناموجود خطا می‌دهد اما اگر || true انتهای دستور باشد، خطا بلعیده می‌شود.

نکته مشترک اینجاست: هیچ‌کدام از این‌ها در crontab دیده نمی‌شوند. کرون فقط می‌داند دستور اجرا شد. نمی‌داند نتیجه چه بود.

سه بررسی که واقعاً شکست خاموش را می‌گیرد

بررسی حجم فایل، نه وجود فایل

وجود فایل بکاپ هیچ چیزی را ثابت نمی‌کند. حجمش چیز زیادی را ثابت می‌کند. یک اسکریپت ساده که حجم آخرین بکاپ را با حجم دیروز مقایسه کند، بیشتر خطاها را می‌گیرد:

#!/bin/bash
LAST=$(stat -c%s /backup/db-$(date -d yesterday +%F).sql.gz)
TODAY=$(stat -c%s /backup/db-$(date +%F).sql.gz)
if [ "$TODAY" -lt $((LAST / 2)) ]; then
  echo "backup shrank: $LAST -> $TODAY" | mail -s "BACKUP ALERT" ops@example.com
fi

اگر امروز حجم بکاپ کمتر از نصف دیروز باشد، یعنی چیزی سر جایش نیست. این آستانه ساده در عمل بیشتر از هر مانیتورینگ پیچیده‌ای کار می‌کند. عدد را بر اساس رشد واقعی داده‌تان تنظیم کنید؛ اگر روزانه ۵ درصد رشد دارید، افت ۵۰ درصدی قطعاً یعنی خطا.

کد خروج را جدی بگیرید

در crontab اگر دستور با خطا تمام شود، کرون یک ایمیل می‌فرستد. اما فقط اگر MAILTO تنظیم شده باشد و سرور MTA داشته باشد. روی اکثر سرورهای امروزی هیچ‌کدام نیست. راه مطمئن‌تر، گرفتن کد خروج در خود اسکریپت است:

mysqldump --single-transaction --routines --triggers \
  --databases app_db | gzip > /backup/db-$(date +%F).sql.gz
if [ ${PIPESTATUS[0]} -ne 0 ]; then
  curl -s -X POST https://hooks.example.com/alert -d "dump failed"
fi

دقت کنید که ${PIPESTATUS[0]} را چک می‌کنیم، نه $?. چون $? کد خروج gzip را برمی‌گرداند و gzip حتی روی ورودی خالی هم موفق می‌شود. این‌جا اشتباه می‌کنند: خیلی‌ها $? می‌گذارند و ماه‌ها فکر می‌کنند بکاپ سالم است، در حالی که هر شب یک فایل gzip شده از هیچ می‌سازند.

تست بازیابی، نه تست بکاپ

بکاپی که یک بار بازیابی نشده باشد، بکاپ نیست؛ یک فایل است. ماهی یک بار روی یک سرور جداگانه این را اجرا کنید:

gunzip -c /backup/db-2025-01-15.sql.gz | mysql -u root -p restore_test
mysql -u root -p -e "SELECT COUNT(*) FROM restore_test.orders;"

اگر تعداد رکوردها با سرور اصلی هم‌خوانی داشت، بکاپ سالم است. اگر خطای ERROR 1064 یا Table doesn't exist گرفتید، یعنی فایل ناقص است. این تست را روی همان سرور اصلی اجرا نکنید؛ ممکن است دیتابیس زنده را خراب کنید. اگر سرور جداگانه ندارید، یک سرور ابری موقت برای همین کار کافی است.

مقایسه روش‌های بکاپ و هزینه واقعی‌شان

روشمزیتهزینه پنهان
mysqldump روزانهساده، قابل خواندن، قابل ویرایشروی دیتابیس‌های بزرگ قفل و کندی ایجاد می‌کند
rsync فایل‌هافقط تغییرات را منتقل می‌کندفایل‌های حذف‌شده در مقصد باقی می‌مانند مگر --delete بدهید
اسنپ‌شات LVMسریع، بدون توقف سرویسروی همان دیسک است؛ با خرابی دیسک از دست می‌رود
بکاپ داغ با xtrabackupبدون قفل جدولنیاز به فضای دیسک دو برابر و تنظیمات دقیق

انتخاب من برای اکثر پروژه‌ها: mysqldump با --single-transaction برای دیتابیس‌های زیر ۵۰ گیگابایت، و xtrabackup بالای آن. اگر دیتابیس شما زیر ۵۰ گیگ است و می‌روید سراغ xtrabackup، دارید پیچیدگی بی‌دلیل می‌خرید. برعکسش هم درست است: روی دیتابیس ۲۰۰ گیگی، mysqldump هر شب چند ساعت قفل می‌گذارد و کاربران‌تان قطعی حس می‌کنند.

جایی که بکاپ را نگه می‌دارید مهم‌تر از خود بکاپ است

بکاپ روی همان سرور، بکاپ نیست. اگر سرور از دست برود، هر دو با هم می‌روند. حداقل یک نسخه باید جای دیگری باشد. rsync روی SSH ساده‌ترین راه است:

rsync -avz --delete -e "ssh -p 2222" /backup/ backup@remote:/mnt/backups/$(hostname)/

دقت کنید که پورت SSH را صریح داده‌ام. اگر پورت پیش‌فرض را عوض کرده‌اید و این را ننویسید، rsync با Connection refused رد می‌شود و اگر در کرون باشد، فقط یک ایمیل بی‌سرانجام می‌فرستد. برای اینکه بدانید کدام پورت‌ها باید باز باشند و کدام‌ها نه، مرجع پورت سرویس‌های پرکاربرد را ببینید.

یک نکته درباره رمزنگاری: اگر بکاپ را روی سرور شخص ثالث می‌گذارید، قبل از انتقال رمزنگاری کنید. gpg --encrypt یا openssl enc -aes-256-cbc هر دو کار می‌کنند. کلید را روی همان سروری که بکاپ می‌گیرد نگه ندارید؛ اگر سرور از دست برود، کلید هم رفته و بکاپ غیرقابل بازیابی می‌شود.

مانیتورینگ: چطور بفهمید بکاپ دیشب اجرا شد

ساده‌ترین راه، یک فایل نشانه (heartbeat) است. اسکریپت بکاپ در انتهای موفقیت‌آمیز خودش یک فایل با تاریخ امروز می‌سازد. یک اسکریپت جدا هر صبح چک می‌کند که این فایل مربوط به امروز است یا نه:

find /backup/.heartbeat -mtime +1 -type f | grep -q . && \
  echo "backup stale" | mail -s "ALERT" ops@example.com

این روش از مانیتورینگ مبتنی بر لاگ مطمئن‌تر است، چون لاگ می‌تواند پر شود و بچرخد، اما فایل نشانه فقط وقتی ساخته می‌شود که بکاپ واقعاً تمام شده باشد. اگر می‌خواهید عمیق‌تر بروید و بفهمید چرا سرور کند شده یا چرا بکاپ طول می‌کشد، راهنمای تشخیص لود بالای سرور نقطه شروع خوبی است.

اگر تازه سرور را راه انداخته‌اید و هنوز ساختار بکاپ را نچیده‌اید، قبل از هر چیز انتخاب توزیع لینوکس را جدی بگیرید، چون ابزارهای بکاپ در توزیع‌های مختلف تفاوت‌های عملی دارند. و اگر روی یک سرور اختصاصی کار می‌کنید، فضای دیسک را از همان ابتدا برای بکاپ جدا کنید، نه روی پارتیشن روت.

پرسش‌های پرتکرار

چطور بفهمم بکاپ خودکار واقعاً اجرا می‌شود؟

به زمان تغییر فایل نگاه کنید، نه به وجودش. دستور stat /backup/db-$(date +%F).sql.gz باید تاریخ امروز را نشان بدهد. اگر فایل هست ولی تاریخ دیروز یا قدیمی‌تر است، بکاپ اجرا نشده یا نیمه‌کاره مانده.

راه مطمئن‌تر، فایل نشانه است: اسکریپت بکاپ در پایان موفقیت‌آمیز یک فایل خالی با تاریخ امروز بسازد و یک اسکریپت جدا هر صبح وجودش را چک کند.

چرا فایل بکاپ MySQL من خالی است؟

تقریباً همیشه به خاطر خطای احراز هویت یا نبود دسترسی است. اگر mysqldump نتواند به دیتابیس وصل شود، خطا را روی stderr می‌نویسد ولی چون خروجی به فایل ریدایرکت شده، یک فایل خالی ساخته می‌شود و کد خروج هم ممکن است صفر باشد.

راه‌حل: کد خروج ${PIPESTATUS[0]} را چک کنید و مطمئن شوید ~/.my.cnf رمز فعلی را دارد. یک بار دستی همان دستور را اجرا کنید و خروجی را ببینید.

هر چند وقت یک بار باید بازیابی بکاپ را تست کنم؟

ماهی یک بار حداقل. اگر داده‌تان حیاتی است، هر دو هفته. تست بازیابی روی سرور اصلی انجام ندهید؛ یک محیط جدا بسازید و فایل را آنجا باز کنید.

معیار موفقیت این است که تعداد رکوردهای جدول اصلی با نسخه بازیابی‌شده بخواند. اگر فقط فایل را باز کردید و خطا نگرفتید، تست نکرده‌اید.

بکاپ روی همان سرور کافی نیست؟

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

اگر بکاپ را رمزنگاری می‌کنید، کلید را جای دیگری نگه دارید. کلید روی همان سرور یعنی بکاپ رمزنگاری‌شده‌ای که هرگز نمی‌توانید بازش کنید.

همین امروز یک بار دستی اسکریپت بکاپ را اجرا کنید و حجم فایل خروجی را ببینید. اگر کوچک‌تر از انتظار بود، تا فردا صبر نکنید.

آیا این مطلب برایتان مفید بود؟