s.reza
s.reza
خواندن ۵ دقیقه·۳ ماه پیش

یک شب سخت: بازیابی دیتابیس MySQL از نابودی کامل


در این مقاله قصد دارم  براتون داستانی رو تعریف کنم که هر تیم فنی، حتی در کابوس‌هاش هم دوست نداره بهش فکر کنه: خرابی ناگهانی دیتابیس! ولی این کابوس برای ما توی آمادست (amadast.com) به واقعیت تبدیل شد؛ جایی که دیتابیس MySQL ما به یکباره از کار افتاد و ما با کلی تلاش و سختی تونستیم اونو نجات بدیم. شاید این تجربه به دردتون بخوره و توی شرایط مشابه، کمک‌تون کنه.

وقتی همه‌چیز یهویی بهم می‌ریزه

صبح دوشنبه بود، از همون روزایی که همه چی ظاهراً عادیه و تو انتظار داری همه کارها روتین پیش بره. ساعت ۱۲:۱۵:۱۱ بود که یه دفعه کانتینر دیتابیس ما ریستارت شد و با یه خطای عجیب و غریب روبرو شدیم:

Execution of server-side SQL statement 'INSERT IGNORE INTO server_cost(cost_name) VALUES (&quotrow_evaluate_cost&quot), (&quotkey_compare_cost&quot), (&quotmemory_temptable_create_cost&quot), (&quotmemory_temptable_row_cost&quot), (&quotdisk_temptable_create_cost&quot), (&quotdisk_temptable_row_cost&quot); ' failed with error code = 1881, error message = 'Operation not allowed when innodb_force_recovery > 0.'.

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

قدم به قدم به سوی ریکاوری

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

در MySQL و انجین InnoDB برای بازیابی داده‌ها شش سطح ریکاوری وجود داره. ما از سطح ۱ شروع کردیم و کم‌کم سطوح بالاتر رو تست کردیم. ولی انگار هر سطح بیشتر مارو به سمت ناکامی می‌برد. با خطاهای مختلفی مواجه شدیم که حتی  تا حالا ندیده بودیم:

برای اطلاعات بیشتر درباره MySQL recovery modes  هم  می‌تونید به مستندات MySQL مراجعه کنید.

از سطح ۱ تا ۵ این ارور ‌:

Execution of server-side SQL statement 'ALTER TABLE tables_priv MODIFY Db char(64) NOT NULL default '', MODIFY User char(32) NOT NULL default '', MODIFY Table_name char(64) NOT NULL default '', CONVERT TO CHARACTER SET utf8mb3 COLLATE utf8mb3_bin; ' failed with error code = 1050, error message = 'Table '#sql2-2e-4' already exists'.

و سطح ۶ این ارور ها :‌

Tablespace 'innodb_undo_002' Page [page id: space=4294967278, page number=3] log sequence number 117726770646 is in the future! Current system log sequence number 117724575410. -------------------------------------------------------------------- [InnoDB] Your database may be corrupt or you may have copied the InnoDB tablespace but not the InnoDB redo log files. Please refer to http://dev.mysql.com/doc/refman/8.2/en/forcing-innodb-recovery.html for information about forcing recovery.


حالا باید یه راه دیگه پیدا می‌کردیم. ساعت از ۶ عصر گذشته بود و ما هرجایی رو بگید سرچ زدیم و دنبال راه حل ولی هیچ نتیجه‌ای نگرفتیم .

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

یه راه جدید : استفاده از فایل‌های .ibd

جزئیات این کار رو تو این مقاله میتونید بخونید .
تصمیم گرفتیم از فایل‌های دیتابیس قبلی (همون .ibd‌ها) استفاده کنیم و اونا رو به یه دیتابیس جدید منتقل کنیم. اما دیتابیس جدید این فایل‌ها رو نمی‌شناخت. ساعت ۲ شب بود و ما هنوز دنبال راه حل بودیم.

تو این ساعت‌ها، بالاخره جستجو توی اینترنت و فروم‌ها برامون یه نور امید پیدا کرد: یه اسکریپت توی GitHub که فایل‌های .ibd رو به SQL تبدیل می‌کرد. اما اینم کار زیادی داشت. مجبور شدیم تو کدش دست ببریم و بعد از کلی تست، بالاخره دیدیم جواب میده! (https://github.com/anyongjin/mysql_ibd) 

این هم در پرانتز بگم ما کد های این اسکریپت رو خوندیم که خطرناک نباشه.

« برای جاهای حساس اگر از اسکریپتی استفاده میکنید حتما بخونید که اتفاق بدی نیفته »

ایجاد دوباره تیبل‌ها و روبرو شدن با ارورهای جدید

حالا باید تیبل‌ها رو از نو می‌ساختیم و دیتاها رو یکی یکی وارد دیتابیس جدید می‌کردیم. اما باز هم ارورهای جدیدی منتظرمون بودن:

InnoDB: Failing assertion: page_get_n_recs(page) > 1

توی فروم‌های MySQL کلی گشتیم تا اینکه راه حل رو پیدا کردیم: باید ایندکس‌ها رو از فایل‌های SQL حذف می‌کردیم و بعد یکی یکی اضافه‌شون می‌کردیم.

لحظه نجات و بازگشت به زندگی

بالاخره ساعت ۷ صبح روز بعد، بعد از یه ماراتن ۲۱ ساعته، سایت رو با دسترسی محدود بالا آوردیم. تست‌ها رو انجام دادیم و به ارورهایی مربوط به IDهای تکراری برخوردیم. دلیلش این بود که همه ID‌ها توی دیتابیس جدید از ۱ شروع می‌شد. برای حل این مشکل یه اسکریپت PHP نوشتیم که اوضاع رو ردیف کرد.

درس‌هایی که یاد گرفتیم

این ماجرا یه سری درس‌های خیلی مهم به ما داد:

  • مشورت و استفاده از کسایی که میشناسیم :اصلی ترین و مهم ترین کاری که ما کردیم این بود که از کمک دیگران بهره گرفتیم. گاهی یه نظر دوم می‌تونه نجات‌بخش باشه .
  • بکاپ‌ها رو جدی بگیرید: همیشه بکاپ‌های سالم و قابل اعتمادی داشته باشید
    و یک سیستم دوره ای تست کردن بکاپ ها رو داشته باشید که مطمئن باشید در مواقع نیاز بک اپ ها سالم هستن .
  • مانیتورینگ :‌ مانیتورینگ باعث میشه اتفاقات عجیب سیستم رو قبل از اینکه واقعا اتفاق بیفتن یا به نقطه ی بحرانی برسن متوجه بشید
  • از رپلیکا استفاده کنید: با توجه به اسکیلی که هستید(نه اینکه اول کار سراغ این راه برید) میتونید رپلیکا داشته باشید که در چنین مواقعی علاوه بر توضیع بار روی چن دیتابیس اگر یک دیتابیس از مدار خارج شد سایت به مشکل نخوره .
  • دسترسی پذیری یا HA : حتما باتوجه به نیازتون و اسکیلی که در اون قرار دارید کار هایی رو در این زمینه انجام بدید .


« در نهایت، می‌خوام از همه دوستانی که توی این ماجرا به ما کمک کردن تشکر کنم ، این مشکل بدون وجود اون ها حل نمی شد. »

این بود داستان ما، یه ماجرای پر از استرس ولی با پایانی خوش!

دیتابیسmysqlDatabaseRecoveryServerCrashmysql recovery
من سید رضا فلاحی هستم، مهندس نرم افزار هستم ، از چالش های سخت خوشم میاد و سعی میکنم اینجا از تجربیات و چالش هام بنویسم
شاید از این پست‌ها خوشتان بیاید