عندما تؤدي ترحيلات قاعدة البيانات إلى تعطيل التطبيقات قيد التشغيل

لقد نشر فريقك للتو ميزة جديدة. يبدو النشر نظيفًا. لكن بعد خمس دقائق، يرسل مهندس الاستجابة الفورية لقطة شاشة لسجلات الأخطاء. نسخ التطبيق القديمة تتعطل مع أخطاء في قاعدة البيانات. الاستعلام SELECT * FROM users WHERE status = 'active' يفشل فجأة. ماذا حدث؟

لقد قمت بتغيير عمود status من VARCHAR إلى INT في الترحيل. الكود الجديد للتطبيق يتعامل مع الأعداد الصحيحة بشكل جيد. لكن أثناء التحديث التدريجي، تعمل نسخ التطبيق القديمة والجديدة جنبًا إلى جنب. النسخ القديمة لا تزال تتوقع نصوصًا. تغير مخطط قاعدة البيانات تحتها، فتعطلت.

هذا هو التوتر الأساسي في ترحيلات قاعدة البيانات في النشر الحديث: قاعدة البيانات مشتركة، لكن إصدارات التطبيق ليست كذلك.

مشكلة قاعدة البيانات المشتركة

عند النشر بالتحديثات التدريجية، أو النشر الأزرق-الأخضر، أو الإصدارات التجريبية، تعمل إصدارات متعددة من تطبيقك في وقت واحد. جميعها تتصل بنفس قاعدة البيانات. لكن كل إصدار لديه توقعات مختلفة حول المخطط.

التطبيق القديم يتوقع أعمدة وأنواع بيانات وقيودًا معينة. التطبيق الجديد يتوقع هيكلًا مختلفًا قليلاً. كلاهما يحتاج إلى العمل بشكل صحيح خلال الفترة الانتقالية. إذا كسر ترحيلك التوافق مع التطبيق القديم، ستحصل على أخطاء في الإنتاج.

هذه ليست مشكلة نظرية. تحدث في كل مرة يغير فيها الترحيل شيئًا يعتمد عليه الكود قيد التشغيل.

التوافق العكسي: القاعدة غير القابلة للتفاوض

القاعدة الأساسية بسيطة: كل ترحيل يجب أن يكون متوافقًا عكسيًا مع التطبيق القديم. يجب أن يكون الكود القديم قادرًا على قراءة وكتابة البيانات دون أخطاء، حتى بعد تشغيل الترحيل.

تأمل هذين الترحيلين SQL لترى الفرق:

-- آمن: إضافة عمود قابل للقيمة الفارغة مع قيمة افتراضية
-- التطبيق القديم لا يزال قادرًا على INSERT دون تحديد phone_number
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) DEFAULT NULL;

-- مسبب للكسر: تغيير نوع العمود من VARCHAR إلى INT
-- استعلام التطبيق القديم SELECT * FROM users WHERE status = 'active' سيفشل
-- لأن 'active' نص وليس عددًا صحيحًا
ALTER TABLE users ALTER COLUMN status TYPE INT USING status::integer;

بعض التغييرات متوافقة عكسيًا بطبيعتها. إضافة عمود قابل للقيمة الفارغة بقيمة افتراضية، على سبيل المثال، لا يكسر الاستعلامات الحالية. استعلام INSERT INTO users (name, email) للتطبيق القديم لا يزال يعمل لأن العمود الجديد phone يقبل القيم الفارغة.

تغييرات أخرى تكسر التوافق فورًا. تغيير نوع عمود، إعادة تسمية عمود، إضافة قيد NOT NULL إلى عمود مملوء، أو إضافة مفتاح خارجي لا تستطيع البيانات الحالية تحقيقه سيسبب أخطاء في نسخ التطبيق القديمة.

القاعدة ليست اختيارية. إذا لم تستطع ضمان التوافق العكسي، لا يمكنك النشر بأمان مع وقت توقف صفري.

نمط التوسيع والانكماش

أكثر الطرق أمانًا للتعامل مع التغييرات المسببة للكسر هو نمط التوسيع والانكماش، الذي يُسمى أحيانًا الكتابة المزدوجة. الفكرة هي إجراء التغييرات على مراحل، دون إزالة الهياكل القديمة حتى يتم تحديث جميع نسخ التطبيق.

يوضح مخطط التسلسل التالي الجدول الزمني لنمط التوسيع والانكماش، موضحًا كيف تتفاعل نسخ التطبيق القديمة والجديدة مع قاعدة البيانات خلال كل مرحلة.

sequenceDiagram participant OldApp as التطبيق القديم participant DB as قاعدة البيانات participant NewApp as التطبيق الجديد Note over OldApp,NewApp: المرحلة 1: التوسيع DB->>DB: إضافة عمود جديد (الاحتفاظ بالقديم) OldApp->>DB: قراءة/كتابة العمود القديم (ناجح) NewApp->>DB: كتابة كلا العمودين (ناجح) Note over OldApp,NewApp: المرحلة 2: ترحيل البيانات DB->>DB: ملء العمود الجديد من القديم Note over OldApp,NewApp: المرحلة 3: تحديث جميع التطبيقات OldApp->>NewApp: نشر الإصدار الجديد NewApp->>DB: قراءة/كتابة كلا العمودين (ناجح) Note over OldApp,NewApp: المرحلة 4: الانكماش DB->>DB: حذف العمود القديم NewApp->>DB: قراءة/كتابة العمود الجديد فقط (ناجح)

المرحلة 1: التوسيع. أضف الهيكل الجديد دون إزالة القديم. إذا كنت تريد استبدال status (VARCHAR) بـ status_id (INT)، أضف العمود الجديد مع الاحتفاظ بالقديم. التطبيق الجديد يكتب في كلا العمودين. التطبيق القديم يستمر في استخدام status. كلاهما يعمل.

المرحلة 2: ترحيل البيانات. املأ العمود الجديد بقيم محولة من العمود القديم. يمكن تشغيل هذا كمهمة خلفية أو خطوة ترحيل منفصلة.

المرحلة 3: تحديث كود التطبيق. انشر إصدار التطبيق الجديد لجميع النسخ. الآن كل نسخة قيد التشغيل تعرف كلا العمودين.

المرحلة 4: الانكماش. في نشر منفصل، أزل العمود القديم. بحلول هذه النقطة، لا يوجد تطبيق قيد التشغيل يعتمد عليه.

يضيف النمط تعقيدًا. كود التطبيق الخاص بك يحتاج إلى التعامل مع منطق الكتابة المزدوجة خلال الفترة الانتقالية. لديك أعمدة إضافية للحفاظ عليها مؤقتًا. لكن هذا هو ثمن تجنب وقت التوقف والأخطاء أثناء النشر.

التوافق الأمامي: الاتجاه الآخر

التوافق العكسي يحمي كود التطبيق القديم. التوافق الأمامي يحمي كود التطبيق الجديد عندما لا تكون قاعدة البيانات قد ترحلت بالكامل بعد.

تأمل سيناريو حيث تنشر التطبيق الجديد أولاً، لكن الترحيل لم يعمل على جميع نسخ قاعدة البيانات. الكود الجديد يحتاج إلى التعامل مع تنسيقات المخطط القديمة والجديدة. إذا قرأ status كـ VARCHAR لكنه يتوقع INT، يجب أن يتعامل مع التحويل بسلاسة.

التوافق الأمامي أصعب في التحقيق وعادة ما يكون له حدود. يعني أن كودك الجديد يجب أن يكون دفاعيًا بشأن البيانات التي يقرأها. لا يجب أن يفترض أن المخطط قد تغير بالفعل. هذا غالبًا يعني إضافة منطق احتياطي أو تحويل بيانات في طبقة التطبيق حتى يكتمل الترحيل.

أبعد من الأعمدة: الفهارس والقيود والمفاتيح الخارجية

التوافق لا يتعلق فقط بالأعمدة وأنواع البيانات. الفهارس والقيود والمفاتيح الخارجية يمكنها أيضًا تعطيل التطبيقات قيد التشغيل.

إضافة قيد مفتاح خارجي جديد يمكن أن يتسبب في فشل استعلامات INSERT أو UPDATE الحالية إذا كانت البيانات المرجعية غير موجودة. إضافة قيد UNIQUE إلى عمود كان يسمح سابقًا بالتكرار سيكسر أي استعلام يحاول إدراج قيم مكررة. حتى إضافة فهرس يمكن أن يسبب مشاكل في الأداء إذا كانت قاعدة البيانات تقفل الجدول أثناء إنشاء الفهرس.

كل تغيير في المخطط يحتاج إلى تقييم تأثيره على كود التطبيق قيد التشغيل. اسأل نفسك: هل سيؤدي هذا التغيير إلى فشل أي استعلام من التطبيق القديم؟ هل سيغير السلوك بطرق لا يتوقعها الكود القديم؟

قائمة تحقق عملية للترحيلات الآمنة

قبل تشغيل ترحيل في الإنتاج، تحقق من هذه النقاط:

  • هل يمكن للتطبيق القديم قراءة جميع البيانات الحالية دون أخطاء بعد الترحيل؟
  • هل يمكن للتطبيق القديم كتابة بيانات جديدة دون أخطاء بعد الترحيل؟
  • هل جميع الأعمدة الجديدة قابلة للقيمة الفارغة أو لها قيم افتراضية؟
  • هل القيود الجديدة تنطبق بالفعل على البيانات الحالية؟
  • هل سيؤدي الترحيل إلى أقفال جدول تمنع الاستعلامات؟
  • هل هناك خطة تراجع إذا حدث خطأ ما؟

الخلاصة

ترحيلات قاعدة البيانات أثناء النشر بدون توقف تتطلب معاملة المخطط كواجهة مشتركة بين إصدارات التطبيق. كل ترحيل يجب أن يكون متوافقًا عكسيًا مع الكود القديم. التغييرات المسببة للكسر تحتاج إلى نمط التوسيع والانكماش، بإضافة هياكل جديدة أولاً وإزالة القديمة فقط بعد تحديث جميع النسخ.

قاعدة البيانات هي المصدر الوحيد للحقيقة الذي تشاركه جميع إصدارات التطبيق. إذا غيرتها بلا مبالاة، ستعطل الكود قيد التشغيل. صمم ترحيلاتك مثل جسور يمكن للتطبيقات القديمة والجديدة عبورها بأمان. فقط بعد أن تنتقل كل نسخة إلى الجانب الجديد، يجب عليك تعديل الجسر نفسه.