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

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

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

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

نشر البرامج المستقلة مقابل نشر البرامج الموزعة

بالنسبة للبرامج المستقلة التي يتم تشغيلها كعملية واحدة على جهاز واحد، فإن عمليات النشر تكون ذرية. ولن يعمل إصداران من نفس البرنامج مطلقًا في وقت واحد. فإذا حافظ البرنامج المستقل على حالته، فحينئذ يجب أن يقرأ الإصدار الجديد البيانات (أي، دون تسلسل) المكتوبة (أي، متسلسلة) بواسطة إصدار قديم والعكس. إن تلبية هذا الشرط يجعل عملية النشر آمنة للترحيل إلى الأمام والرجوع إلى الحالة السابقة.
 
في النظام الموزع، تكون عمليات النشر أكثر تعقيدًا. حيث تتم عمليات النشر من خلال التحديثات حتى لا يتأثر التوافر. فيتم طرح الإصدار الجديد على مجموعة فرعية من المضيفين مرة واحدة حتى يتمكن المضيفون الآخرون من متابعة تقديم الطلبات. عادةً ما يتواصل هؤلاء المضيفون مع بعضهم البعض من خلال استدعاء إجراء عن بُعد (RPC) أو الحالة الثابتة المشتركة (على سبيل المثال، بيانات التعريف أو نقاط الفحص). وقد يفرض هذا الاتصال أو هذه الحالة المشتركة تحديات إضافية. ويمكن لبرنامج الكتابة والقراءة تشغيل إصدارات مختلفة من البرنامج. ونتيجة لذلك، يمكنهما تفسير البيانات بشكل مختلف. وقد يفشل برنامج القراءة في قراءة البيانات تمامًا، ما يتسبب في توقف الخدمة.

المشاكل المتعلقة بتغيرات البروتوكول

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

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

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

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

تقنية النشر على مرحلتين

تتمثل إحدى الطرق التي نضمن بها أنه يمكننا الرجوع إلى الحالة السابقة بأمان في استخدام تقنية يُشار إليها عادةً باسم النشر على مرحلتين. فكر في السيناريو الافتراضي التالي مع خدمة تدير البيانات (الكتابة والقراءة) على Amazon Simple Storage Service (Amazon S3). تعمل الخدمة على أسطول من الخوادم في جميع مناطق توافر الخدمات المتعددة للتوسعة والتوافر.

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

وكما هو موضح في الرسم البياني السابق، فإننا نطلق على المرحلة الأولى اسم الإعداد. وفي هذه المرحلة، نقوم بإعداد جميع الخوادم لقراءة تنسيق JSON (بالإضافة إلى XML) لكن مع استمرار كتابة XML من خلال نشر الإصدار V2. وهذا التغيير لا يغير أي شيء من وجهة نظر تشغيلية. حيث لا يزال بإمكان جميع الخوادم قراءة XML، ولا تزال جميع البيانات تُكتب بتنسيق XML. فإذا قررنا الرجوع إلى الحالة السابقة لهذا التغيير، فستعود الخوادم إلى حالة عدم القدرة على قراءة JSON. ولا تمثل هذه الحالة مشكلة لأنه لا توجد إلى الآن أي بيانات مكتوبة بتنسيق JSON.

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

رغم أن الرسم البياني السابق يوضح تغيير تنسيق التسلسل من تنسق XML إلى تنسيق JSON، إلا أن التقنية العامة تنطبق على جميع الحالات الموضحة في القسم السابق تغييرات البروتوكول. على سبيل المثال، فكر مرة أخرى في السيناريو السابق الذي كان من المفترض فيه زيادة فترة النبض بين الخوادم من خمس إلى 10 ثوان. في مرحلة الإعداد، يمكننا أن نجعل جميع الخوادم تمدد فترة النبض المتوقعة إلى 10 ثوانٍ رغم أن جميع الخوادم مستمرة في إرسال نبضة كل خمس ثوانٍ. في مرحلة التنشيط، نغيّر معدل التكرار إلى نبضة واحدة كل 10 ثوانٍ.

احتياطات عمليات النشر على مرحلتين

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

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

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

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

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

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

أفضل ممارسات التسلسل

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

• بشكل عام نحن نتجنب تطوير التنسيقات المخصصة للتسلسل.

قد يبدو المنطق الأولي للتسلسل المخصص بسيطًا ويوفر حتى أداءً أفضل. ومع ذلك، فإن التكرارات اللاحقة للتنسيق فرضت تحديات قد تم حلها بالفعل بواسطة أُطر عمل راسخة مثل JSON و Protocol Buffers و Cap’n Proto و FlatBuffers. عند استخدام أُطر العمل هذه بصورة مناسبة، فإنها توفر ميزات أمان مثل الخروج والتوافق مع الإصدارات السابقة وتتبع وجود السمة (أي، إذا تم تعيين حقل صراحة أو تم تعيين قيمة افتراضية ضمنيًا).

• مع كل تغيير، نخصص صراحةً نسخة مميزة لبرنامج التسلسل.

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

• نتجنب إجراء تسلسل لهياكل البيانات التي لا يمكننا التحكم فيها.

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

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

وكما هو الحال مع العديد من أفضل ممارساتنا، فإننا نشاركها مع تنويه بأن إرشاداتنا لا تنطبق على جميع التطبيقات والسيناريوهات.

التحقق من أن التغيير آمن للرجوع إلى الحالة السابقة

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

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

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

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

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

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

الخاتمة

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

نصوص أخرى للقراءة

لمزيد من المعلومات حول كيفية قيام Amazon بتحسين آمان الخدمات وتوافرها مع زيادة رضا العميل وإنتاجية المطور، راجع  الإسراع بالتسليم المتواصل


نبذة عن المؤلف

يشغل سانديب بوكونوري منصب كبير المهندسين في AWS. ومنذ انضمامه إلى Amazon في عام 2011، فقد عمل على العديد من الخدمات بما في ذلك Amazon DynamoDB وAmazon Simple Queue Service (SQS). وهو يركز حاليًا على تقنيات تعلم الآلة التي تتضمن لغات البشرية (مثل، ASR «التعرّف الآلي على الكلام» و NLP «معالجة اللغة الطبيعية» و NLU «فهم اللغة الطبيعية» و Machine Translation «الترجمة الآلية»)، وهو المهندس الرئيسي لشركة Amazon Lex. قبل انضمامه إلى AWS، عمل في Google على مشاكل تعلم الآلة مثل البريد العشوائي والكشف عن المحتوى المسيء في وسائل التواصل الاجتماعي واكتشاف الحالات الشاذة في سجلات الوصول إلى الشبكة.

الإسراع بالتسليم المتواصل