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

التوقف الناتج عن تغيير قاعدة بيانات ليس دائمًا انقطاعًا واضحًا وصريحًا. للمستخدمين قد يظهر على شكل صفحة لا تنتهي من التحميل، عملية دفع تفشل، أو تطبيق يظهر فجأة "حدث خطأ ما". بالنسبة للفرق، يظهر كتنبيهات، ارتفاع في معدلات الأخطاء، وتكدس من الكتابات الفاشلة التي تحتاج تنظيفًا.
تغييرات المخطط محفوفة بالمخاطر لأن قاعدة البيانات مشتركة بين كل إصدارات التطبيق العاملة. أثناء الإصدار غالبًا ما تكون هناك نسخ قديمة وجديدة من الكود تعمل في نفس الوقت (نشر متدرج، مثيلات متعددة، مهام خلفية). ترحيل يبدو صحيحًا قد يكسر إحدى تلك النسخ.
أوضاع الفشل الشائعة تشمل:
حتى عندما يكون الكود سليمًا، تتعطل الإصدارات بسبب التوقيت والتوافق بين النسخ. تغييرات المخطط بدون توقف تعود لقاعدة واحدة: يجب أن تكون كل حالة وسيطة آمنة لكلٍّ من الكود القديم والجديد. تغيّر قاعدة البيانات دون كسر القراءات والكتابات القائمة، تنشر كودًا يستطيع التعامل مع الشكلين، ولا تزيل المسار القديم إلا بعد التأكد من عدم الاعتماد عليه.
هذا الجهد الإضافي يستحق عندما لديك حركة فعلية، اتفاقيات مستوى خدمة صارمة، أو العديد من مثيلات التطبيق والعمال. لأداة داخلية صغيرة وقاعدة بيانات هادئة، قد تكون نافذة صيانة مخططًا أبسط.
معظم الحوادث الناتجة عن أعمال قاعدة البيانات تحدث لأن التطبيق يتوقع أن يتغير المخطط فورًا، بينما التغيير يستغرق وقتًا. يتجنب نمط التوسيع/التقليص ذلك عن طريق تقسيم تغيير واحد محفوف بالمخاطر إلى خطوات أصغر وآمنة.
لفترة قصيرة، يدعم نظامك لهجتين في الوقت نفسه. تُدخل البنية الجديدة أولًا، تحافظ على القديمة عاملة، تنقل البيانات تدريجيًا، ثم تنظف.
النمط بسيط:
هذا يتماشى جيدًا مع النشر المتدرج. إذا حدّثت 10 خوادم واحدة تلو الأخرى، ستشغّل لفترة نسخًا قديمة وجديدة معًا. نمط التوسيع/التقليص يحافظ على التوافق مع نفس قاعدة البيانات أثناء التداخل.
كما يجعل عمليات التراجع أقل رعبًا. إذا وجود خطأ في الإصدار الجديد، يمكنك التراجع عن التطبيق دون التراجع عن قاعدة البيانات، لأن البنى القديمة لا تزال موجودة خلال نافذة التوسيع.
مثال: تريد تقسيم عمود PostgreSQL full_name إلى first_name وlast_name. تضيف الأعمدة الجديدة (توسيع)، تُشغّل كودًا يقرأ ويكتب كلا الشكلين، تملأ الصفوف القديمة، ثم تحذف full_name عندما تتأكد من عدم استخدامه (تقليص).
مرحلة التوسيع تتعلق بإضافة خيارات جديدة، لا بإزالة القديمة.
خطوة شائعة أولى هي إضافة عمود جديد. في PostgreSQL، عادةً ما يكون الأسلم إضافته كقابل لأن يكون NULL وبدون قيمة افتراضية. إضافة عمود غير NULL مع قيمة افتراضية قد يؤدي إلى إعادة كتابة الجدول أو أقفال أثقل، حسب إصدار Postgres الخاص بك والتغيير المحدد. تسلسل أكثر أمانًا هو: أضف العمود كقابل لأن يكون NULL، انشر كودًا متسامحًا، املأ البيانات، ثم طبق NOT NULL لاحقًا.
الفهارس تحتاج عناية أيضًا. إنشاء فهرس عادي قد يعرقل الكتابات أكثر مما تتوقع. عندما يكون ممكنًا، استخدم الإنشاء المتزامن (concurrent) حتى تظل القراءات والكتابات سارية. يستغرق وقتًا أطول، لكنه يتجنّب القفل الموقِف للإصدار.
التوسيع قد يعني أيضًا إضافة جداول جديدة. إذا كنت تنتقل من عمود واحد إلى علاقة كثير-إلى-كثير، قد تضيف جدول ربط مع الاحتفاظ بالعمود القديم. المسار القديم يبقى يعمل بينما البنية الجديدة تبدأ في جمع البيانات.
عمليًا، يتضمن التوسيع غالبًا:
بعد التوسيع، يجب أن تكون نسخ التطبيق القديمة والجديدة قادرة على العمل معًا دون مفاجآت.
معظم ألم النشر يحدث في المنتصف: بعض الخوادم تشغّل كودًا جديدًا، وأخرى لا تزال تشغّل القديم، بينما قاعدة البيانات بدأت تتغير. هدفك واضح: أي نسخة في عملية التدرج يجب أن تعمل مع كلٍّ من المخطط القديم والموسّع.
نهج شائع هو الكتابة المزدوجة. إذا أضفت عمودًا جديدًا، يكتب التطبيق الجديد إلى العمودين القديم والجديد معًا. نسخ التطبيق القديمة تظل تكتب إلى القديم فقط، وهذا مقبول لأن القديم لا يزال موجودًا. اجعل العمود الجديد اختياريًا في البداية، وأجّل تطبيق القيود الصارمة حتى تتأكد أن كل الكتابين قد تم ترقيتهم.
عادةً ما تتحول القراءات بحذر أكبر من الكتابات. لفترة، احتفظ بالقراءة من العمود القديم (الذي تعلم أنه مُعبّأ بالكامل). بعد الملء والتحقق، حرّك القراءات لتفضيل العمود الجديد مع تراجع إلى القديم إذا كان الجديد مفقودًا.
وأبقِ مخرجات واجهات برمجة التطبيقات مستقرة أثناء تغيّر قاعدة البيانات تحتيك. حتى إن قدمت حقلًا داخليًا جديدًا، تجنّب تغيير شكل الاستجابات حتى تكون كل المستهلكات (ويب، جوال، تكاملات) جاهزة.
تبدو عملية الانتشار الصديقة للتراجع عادة كالتالي:
الفكرة الأساسية أن الخطوة الأولى التي لا رجعة فيها هي حذف البنية القديمة، لذا أرجئها إلى النهاية.
الملء الخلفي هو المكان الذي تفشل فيه كثير من محاولات "تغييرات المخطط بدون توقف". تريد تعبئة العمود الجديد للصفوف الحالية دون أقفال طويلة، استعلامات بطيئة، أو قفزات حمولة مفاجئة.
التقسيم إلى دُفعات مهم. استهدف دُفعات تنتهي بسرعة (بالثواني، لا بالدقائق). إذا كانت كل دُفعة صغيرة، يمكنك الإيقاف المؤقت، الاستئناف، وضبط المهمة دون إيقاف الإصدارات.
لتتبع التقدّم، استخدم مؤشرًا ثابتًا. في PostgreSQL غالبًا ما يكون المفتاح الأساسي. عالج الصفوف بترتيب وخزن آخر id أنهيته، أو اعمل بنطاقات id. هذا يتجنّب مسوح الجدول الكاملة المكلفة عند إعادة تشغيل المهمة.
هذا نمط بسيط:
UPDATE my_table
SET new_col = ...
WHERE new_col IS NULL
AND id > $last_id
ORDER BY id
LIMIT 1000;
اجعل التحديث شرطيًا (مثلاً WHERE new_col IS NULL) حتى تكون المهمة قابلة للإعادة دون آثار جانبية. إعادة التشغيل لا تلمس إلا الصفوف التي لا تزال بحاجة للعمل، مما يقلل من الكتابات غير الضرورية.
خطط للبيانات الجديدة التي قد تصل أثناء الملء الخلفي. الترتيب الشائع هو:
الملء الخلفي الجيد ممل: ثابت، قابل للقياس، وسهل الإيقاف إذا ارتفعت حرارة قاعدة البيانات.
أخطر لحظة ليست إضافة العمود الجديد. إنها قرار الاعتماد عليه.
قبل الانتقال إلى التقليص، أثبت أمرين: البيانات الجديدة كاملة، والإنتاج يقرأها بأمان.
ابدأ بفحوصات اكتمال سريعة وقابلة للتكرار:
إذا كنت تكتب مزدوجًا، أضف فحص اتساق لاكتشاف الأخطاء الصامتة. مثلاً، شغّل استعلامًا كل ساعة يجد الصفوف حيث old_value <> new_value ونبّه إذا لم تكن النتيجة صفر. هذا غالبًا أسرع طريقة لاكتشاف أن كاتبًا ما لا يزال يحدث الحقل القديم فقط.
راقب إشارات الإنتاج الأساسية أثناء تشغيل الترحيل. إذا ارتفع زمن الاستعلام أو فترات الانتظار على الأقفال، قد تساهم استعلامات التحقق "الآمنة" نفسها في الحمل. راقب معدلات الأخطاء لأي مسارات كود تقرأ الحقل الجديد، خاصة بعد النشر مباشرة.
كم من الوقت تحتفظ بالمسارين؟ طويلاً بما يكفي لتجاوز دورة إصدار كاملة وإعادة تشغيل ملء خلفي واحدة على الأقل. كثير من الفرق تستخدم أسبوع إلى أسبوعين، أو حتى تتأكد من عدم وجود نسخة تطبيق قديمة لا تزال تعمل.
التقليص يجعل الفرق متوترة لأنه يبدو نقطة اللانراجع. إذا أُنجز التوسيع بشكل صحيح، فالتقليص مجرد تنظيف، ويمكن تنفيذه أيضًا بخطوات صغيرة ومنخفضة المخاطر.
اختر التوقيت بعناية. لا تحذف أي شيء فور انتهاء الملء الخلفي. امنحه دورة إصدار كاملة على الأقل حتى تظهر المهام المؤجلة وحالات الحافة.
تتابع إغلاق آمن عادة بهذا الشكل:
إذا استطعت، قسم عملية التقليص إلى إصدارين: واحد يزيل مراجع الكود (مع تسجيل إضافي)، وآخر لاحق يزيل كائنات قاعدة البيانات. هذا الانقسام يجعل التراجع واستكشاف الأخطاء أسهل.
تفاصيل PostgreSQL مهمة هنا. حذف عمود عادةً ما يكون مجرد تغيير ميتاداتا، لكنه مع ذلك يأخذ قفل ACCESS EXCLUSIVE لفترة وجيزة. خطط لوقت هادئ واجعل الترحيل سريعًا. إذا أنشأت فهارس إضافية، فضّل حذفها بـ DROP INDEX CONCURRENTLY لتجنب حجب الكتابات (لا يمكن تشغيلها داخل كتلة معاملة، لذا يجب أن يدعم أدوات الترحيل ذلك).
تفشل ترحيلات بدون توقف عندما تتوقف قاعدة البيانات والتطبيق عن الاتفاق حول ما المسموح به. النمط يعمل فقط إذا كانت كل حالة وسيطة آمنة لكلٍّ من الكود القديم والجديد.
هذه الأخطاء تظهر كثيرًا:
سيناريو واقعي: تبدأ بكتابة full_name من الـ API، لكن مهمة خلفية تنشئ المستخدمين لا تزال تضبط first_name وlast_name فقط. تعمل في الليل، تُدرج صفوفًا مع full_name = NULL، والكود لاحقًا يفترض أن full_name دائمًا موجود.
عامل كل خطوة كإصدار قد يستمر أياما:
قائمة تحقق قابلة للتكرار تبقيك من نشر كود يعمل فقط في حالة واحدة من حالات قاعدة البيانات.
قبل النشر، تأكد أن قاعدة البيانات لديك تحتوي بالفعل على أجزاء التوسيع (أعمدة/جداول جديدة، فهارس أضيفت بطريقة منخفضة القفل). ثم تأكد أن التطبيق متسامح: يجب أن يعمل ضد الشكل القديم، والشكل الموسّع، وحالة نصف مكتملة للملء.
اجعل القائمة قصيرة:
الترحيل يُعتبر مكتملًا فقط عندما تستخدم القراءات البيانات الجديدة، والتطبيق لم يعد يكتب البيانات القديمة، وقد تحققت من الملء الخلفي بواسطة فحص بسيط على الأقل (عدّ أو عينات).
افترض لديك جدول PostgreSQL customers مع عمود phone يخزن قيمًا متباينة (تنسيقات مختلفة، أحيانًا فارغ). تريد استبداله بـ phone_e164، لكن لا يمكنك إيقاف الإصدارات أو أخذ التطبيق للخارج.
تتبع تسلسل توسيع/تقليص نظيف هكذا:
phone_e164 كقابل لأن يكون NULL، بدون قيمة افتراضية، ودون قيود ثقيلة بعد.phone وphone_e164، لكن احتفظ بالقراءات على phone حتى لا يتغيّر شيء للمستخدمين.phone_e164 أولًا والتراجع إلى phone إذا كان NULL.phone_e164، أزل التراجع، احذف phone، ثم أضف قيودًا أكثر صرامة إذا لزم.يبقى التراجع بسيطًا عندما تكون كل خطوة متوافقة للخلف. إذا تسبب تبديل القراءات في مشاكل، تراجع عن التطبيق بينما تبقى الأعمدة موجودة. إذا تسبب الملء الخلفي بارتفاع في الحمل، أوقف المهمة، قلّل حجم الدُفعات، واستأنف لاحقًا.
للبقاء منسقين، دوّن الخطة في مكان واحد: SQL الدقيق، أي إصدار يقلب القراءات، كيف تقيس الاكتمال (مثل نسبة phone_e164 غير الفارغة)، ومن يملك كل خطوة.
نمط التوسيع/التقليص يعمل أفضل عندما يصبح روتينيًا. اكتب دفتر إجراءات قصير يمكن لفريقك إعادة استخدامه لكل تغيير مخطط، ويفضل أن يكون صفحة واحدة ومحدّدًا بما يكفي ليتبعه زميل جديد.
قالب عملي يغطي:
قرّر من المسؤولية مقدمًا. "كل واحد ظن أن شخصًا آخر سيتولى التقليص" هو سبب بقاء أعمدة قديمة وأعلام ميزة شهورًا.
حتى لو كان الملء الخلفي يعمل عبر الإنترنت، جدوله عندما يكون المرور أقل. أسهل أن تحتفظ بدُفعات صغيرة، تراقب حمل القاعدة، وتتوقّف بسرعة إذا ارتفع التأخير.
إذا كنت تبني وتنشر باستخدام Koder.ai (koder.ai)، فإن وضع التخطيط Planning Mode قد يكون مفيدًا لرسم المراحل ونقاط الفحص قبل لمس الإنتاج. قواعد التوافق نفسها تنطبق، لكن تدوين الخطوات يجعل تجاوز الأجزاء "المملة" التي تمنع الانقطاعات أصعب.
لأن قاعدة البيانات مشتركة بين جميع نسخ التطبيق التي تعمل في نفس الوقت. أثناء نشر متدرج وتشغيل مهام خلفية، قد تعمل نسخ قديمة وجديدة معًا، وفي هذه الحالة أي ترحيل يغيّر أسماء أعمدة أو يحذفها أو يضيف قيودًا قد يكسر النسخة التي لم تُحدَّث لتتوافق مع الحالة الحالية للمخطط.
يعني تصميم الترحيل بحيث أن كل حالة وسيطة لقاعدة البيانات تعمل لكلٍّ من الكود القديم والجديد. تضيف البُنى الجديدة أولًا، تشغّل المسارين معًا لفترة، ثم تزيل البنية القديمة فقط بعد أن تتأكد أنه لا أحد يعتمد عليها.
مرحلة التوسيع تضيف أعمدة أو جداول أو فهارس جديدة دون إزالة ما يحتاجه التطبيق الحالي. مرحلة التقليص هي التنظيف، حيث تزيل الأعمدة القديمة وقراءات/كتابات القديمة والمنطق المؤقت بعد إثبات أن المسار الجديد يعمل تمامًا.
بدءًا بإضافة عمود قابل لأن يكون NULL ودون قيمة افتراضية هو الأسلم عادةً، لأنه يتجنّب عمليات إعادة كتابة الجداول أو الأقفال الثقيلة. بعد ذلك تنشر كودًا يتعامل مع غياب العمود أو وجوده كـ NULL، تُملأ البيانات تدريجيًا، ثم تشدّد القيود مثل NOT NULL لاحقًا.
تستخدم الكتابة المزدوجة عندما يبدأ الإصدار الجديد بالكتابة إلى الحقل القديم والجديد معًا أثناء الانتقال. هذا يحافظ على اتساق البيانات بينما لا تزال نسخ التطبيق والمهام القديمة تكتب في الحقل القديم فقط.
قم بالملء الخلفي بدُفعات صغيرة تنتهي سريعًا، واجعل كل دُفعة قابلة للإعادة (idempotent) حتى أن إعادة التشغيل لا تعيد كتابة كل الصفوف بلا داع. راقب زمن الاستعلام، فترات الانتظار على الأقفال، وتأخر النسخ المتماثل، وكن جاهزًا لإيقاف المهمة مؤقتًا أو تقليل حجم الدُفعات إذا ارتفعت الأحمال.
أولًا افحص الاكتمال: كم صفًا ما زال به NULL في العمود الجديد. ثم نفّذ فحص اتساق يقارن القيم القديمة والجديدة لعينة من الصفوف (أو بشكل مستمر إذا كان رخيصًا)، وراقب أخطاء الإنتاج بعد النشر لاكتشاف المسارات التي لا تزال تستخدم المخطط القديم.
القيود الجديدة أو NOT NULL قد تُعيق الكتابات أثناء التحقق، وإنشاء الفهارس قد يحمل أقفالًا طويلة. عمليات إعادة التسمية والحذف خطرة أيضًا لأن الكود القديم قد يستمر في الإشارة إلى الأسماء القديمة أثناء نشر متدرج.
بعد توقُّف الكتابة إلى الحقل القديم، وتحويل القراءات إلى الحقل الجديد بدون تراجع، وانتظار فترة كافية للتأكد من عدم وجود نسخ تطبيق أو مهام قديمة، يصبح حذف العمود القديم آمنًا. كثير من الفرق تجعل هذا في إصدار منفصل لتسهيل التراجع.
إذا كان بإمكانك قبول نافذة صيانة صغيرة مع حركة قليلة، قد يكفي ترحيل واحد. لكن إذا لديك مستخدمون حقيقيون، نسخ تطبيق متعددة، مهام خلفية، أو اتفاقيات مستوى خدمة، فإن نمط التوسيع/التقليص يستحق الجهد لأنه يجعل النشر والتراجع أكثر أمانًا؛ وضع الخطة في Koder.ai Planning Mode يساعد على عدم تخطي الخطوات "المملة" التي تمنع الانقطاعات.