عندما تفشل ترحيلات قاعدة البيانات في الإنتاج: ثلاثة سيناريوهات ستؤرق نومك
لقد قمت للتو بتشغيل ترحيل (migration) في بيئة الإنتاج. اكتمل بنجاح. لا أخطاء، لا مهلات زمنية، لا جداول مقفلة. تتنفس الصعداء وتنتقل إلى المهمة التالية.
بعد ساعتين، يهتز هاتفك. تقرير معطل. البيانات تبدو خاطئة. خدمة نسيت أمرها تكتب قيم NULL في جداول حرجة. الترحيل نجح، لكن نظام الإنتاج ينهار.
هذا هو كابوس ترحيلات قاعدة البيانات. على عكس نشر التطبيقات حيث تكون حالات الفشل عادةً فورية وواضحة، يمكن أن تختبئ حالات فشل الترحيل لساعات أو أيام. بحلول الوقت الذي تلاحظ فيه، يكون الضرر قد انتشر بالفعل.
دعني أريك ثلاثة سيناريوهات حقيقية حيث تسوء الترحيلات، ليس لأن SQL فشلت، ولكن لأن الآثار الجانبية فاجأت الجميع.
السيناريو الأول: العمود الجديد الذي كسر كل شيء
يحتاج فريقك إلى إضافة عمود phone_number إلى جدول users. يعمل الترحيل بشكل جيد في بيئة الاختبار. تجتاز جميع الاختبارات. تدفع إلى الإنتاج بثقة.
يتم إنشاء العمود. لا أخطاء. لكن بعد ثوانٍ، يبدأ التطبيق في التصرف بغرابة.
إليك ما حدث: تطبيق الإنتاج لم يتم تحديثه بعد. الكود القديم لا يزال قيد التشغيل، وهو يرسل استعلامات مثل SELECT * FROM users. هذا يعمل بشكل جيد - العمود الجديد يتم تجاهله ببساطة. المشكلة الحقيقية في مكان آخر. جزء آخر من الكود يبدأ في إدراج البيانات في phone_number، لكنه يستخدم تنسيقًا مختلفًا عما يتوقعه التطبيق الجديد. تأتي أرقام الهواتف بتنسيقات مختلطة - بعضها برموز دولية، وبعضها بدون، وبعضها بشرطات، وبعضها بدون.
تأمل الترحيل الذي تسبب في هذا السيناريو:
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
يبدو هذا غير ضار. لكن بدون قيد NOT NULL أو قيمة افتراضية، يقبل العمود أي تنسيق. الأسوأ من ذلك، إذا كان الجدول كبيرًا، فإن ALTER TABLE هذا يقفل الجدول ضد عمليات الكتابة طوال العملية بأكملها. في الإنتاج، يمكن أن يصطف هذا القفل مئات الطلبات في ثوانٍ. الخطر الحقيقي ليس SQL نفسها - بل أن المخطط (schema) تغير قبل أن تكون كل نسخة تطبيق جاهزة للتعامل معه.
الآن لديك بيانات غير متناسقة في عمود ستعتمد عليه أنظمة متعددة. يواجه فريقك خيارًا صعبًا: محاولة تنظيف البيانات الموجودة، أو التسرع في إصدار التطبيق الجديد إلى الإنتاج قبل أن يكون جاهزًا.
المشكلة الأساسية هنا هي التوقيت. تغير المخطط قبل أن يتم نشر كود التطبيق الذي يفهمه بالكامل. في نظام موزع، لا يتم تحديث كل مثيل في نفس اللحظة. لنافذة زمنية قصيرة - أحيانًا أطول - يتفاعل الكود القديم مع المخطط الجديد.
السيناريو الثاني: تغيير النوع الذي كسر تقريرًا ليليًا
هذا أكثر دقة. يقرر فريقك تغيير عمود price من integer إلى decimal. فكرة جيدة - الأسعار تحتاج إلى دقة. يعمل الترحيل بشكل مثالي. لا أخطاء فورية. يبدو التطبيق سعيدًا.
لكن قبل ستة أشهر، كتب شخص ما استعلام تقرير يعامل price كعدد صحيح. هذا الاستعلام لا يُستخدم على الصفحات الرئيسية. يتم تشغيله مرة واحدة ليلاً لتقرير مالي. في الساعة 2 صباحًا، يفشل التقرير تمامًا. كل استعلام يقارن الأسعار بقيم صحيحة يرمي الآن أخطاء عدم تطابق النوع.
هذا ما يسميه المهندسون تغييرًا حاجزًا (blocking change). تغيير المخطط لم يكسر أي شيء مرئي أثناء النهار، لكنه كسر بصمت عملية دفعية حرجة تعمل ليلاً. بحلول الصباح، يسأل فريق المالية لماذا لا تتطابق أرقام الأمس.
الجزء الخطير؟ قد لا تكتشف هذا الفشل لساعات. والإصلاح ليس بسيطًا. لا يمكنك فقط عكس تغيير النوع دون ترحيل آخر، والذي يحمل مخاطره الخاصة.
السيناريو الثالث: العمود المحذوف الذي سمم ثلاثة جداول
هذا هو السيناريو الأكثر خطورة. فريقك واثق من أن عمود old_status لم يعد مستخدمًا. تم إهماله منذ أشهر. لا أحد يشير إليه في التطبيق الرئيسي. تكتب ترحيلاً لحذفه.
يعمل الترحيل بسلاسة. يختفي العمود. لا أخطاء في أي مكان.
لكن هناك خدمة خلفية - وظيفة مزامنة بيانات كتبها فريق غادر الشركة منذ عامين - لا تزال تقرأ old_status بشكل دوري. لا تتعطل عندما يكون العمود مفقودًا. تبدأ فقط في كتابة قيم NULL في جداول أخرى. تنتشر القيم الخالية. تنكسر سلامة البيانات بصمت عبر ثلاثة جداول مختلفة خلال الساعتين التاليتين.
بحلول الوقت الذي يلاحظ فيه أحدهم، يكون الضرر قد وقع. لا يمكنك ببساطة "التراجع" عن حذف العمود. البيانات في تلك الجداول الأخرى تالفة بالفعل. يتطلب الاسترداد فهم الصفوف المتأثرة بالضبط، وإعادة بناء القيم المفقودة من النسخ الاحتياطية، وتشغيل نصوص إصلاح دقيقة.
لماذا تختلف ترحيلات قاعدة البيانات عن نشر التطبيقات
تشترك هذه السيناريوهات الثلاثة في نمط مشترك: تم تنفيذ الترحيل بنجاح، لكن الآثار الجانبية ظهرت لاحقًا. هذا ما يجعل ترحيلات قاعدة البيانات مختلفة جوهريًا عن نشر التطبيقات.
تشترك السيناريوهات الثلاثة أعلاه في نمط واضح: تغيير مخطط ناجح يؤدي إلى فشل متأخر. يوضح مخطط التدفق التالي كل سيناريو من السبب الجذري إلى النتيجة.
عندما يفشل نشر تطبيق، عادةً ما تعرف على الفور. تظهر الأخطاء في السجلات. يبلغ المستخدمون عن مشاكل. تنطلق تنبيهات المراقبة. يمكنك التراجع عن إصدار التطبيق واستعادة الخدمة بسرعة.
ترحيلات قاعدة البيانات لا تعمل بهذه الطريقة. يمكن أن يؤدي تغيير المخطط إلى:
- إنشاء تناقضات تظهر فقط عند وصول بيانات جديدة
- كسر الاستعلامات التي تعمل وفق جدول زمني، وليس بشكل مستمر
- التسبب في فساد البيانات الذي ينتشر ببطء عبر الجداول ذات الصلة
- التأثير على الخدمات التي نسيتها أو لم تكن تعلم بوجودها
الجزء الأسوأ؟ بمجرد أن يقع الضرر، لا يمكنك ببساطة "التراجع" عن تغيير المخطط كما تتراجع عن تغيير الكود. لا يمكن استعادة عمود محذوف بسهولة، خاصة إذا كانت الجداول الأخرى تعتمد بالفعل على غيابه. يتطلب تغيير نوع البيانات ترحيلًا عكسيًا يحمل مخاطره الخاصة.
قائمة مراجعة عملية قبل الترحيل التالي في الإنتاج
قبل تشغيل الترحيل التالي في الإنتاج، راجع هذه النقاط:
- حدد جميع المستهلكين. ضع قائمة بكل خدمة، وظيفة cron، تقرير، وخط أنابيب بيانات يلمس الجدول المتأثر. لا تفترض أنك تعرفهم جميعًا.
- تحقق من التنفيذ المؤجل. ابحث عن الاستعلامات التي تعمل وفق جداول زمنية، أو العمليات الدفعية، أو الوظائف الخلفية. هذه هي التي ستفشل بصمت بعد ساعات.
- تحقق من التوافق العكسي. هل يمكن لكود التطبيق القديم العمل مع المخطط الجديد؟ لدورة نشر واحدة على الأقل، يجب أن يدعم مخططك كلاً من الكود القديم والجديد.
- جهز خطة استرداد. اعرف بالضبط كيف ستستعيد البيانات إذا حدث خطأ ما. اختبر عملية الاسترداد، وليس فقط الترحيل.
- قم بتشغيل الترحيل خلال فترة حركة مرور منخفضة. حتى مع جميع الاحتياطات، امنح نفسك مساحة لالتقاط المشاكل قبل أن تؤثر على المستخدمين.
الخلاصة العملية
الترحيل الناجح ليس هو الذي يعمل بدون أخطاء. الترحيل الناجح هو الذي لا يكسر أي شيء - الآن، أو بعد ساعة، أو في الساعة 3 صباحًا عندما يعمل التقرير الليلي. تعامل مع كل تغيير في المخطط كقنبلة موقوتة محتملة، وتحقق من أن جميع الأنظمة، وليس فقط الواضحة منها، يمكنها التعامل مع الهيكل الجديد قبل اعتبار الترحيل منتهيًا.