تنبيهات تُرسل مرة واحدة، لا مرة لكل خادم
أحد تنبيهاتنا التشغيلية، وهو «رصيد منخفض» لتطبيق منشور لأحد المنشئين، من المفترض أن يصلنا مرة واحدة في اليوم على الأكثر. وفي أحد الأيام وصل خمس مرات. السبب: كل نسخة من واجهة برمجة التطبيقات لدينا تتذكر التنبيهات التي أرسلتها في ذاكرتها الخاصة، فأرسل خادمان التنبيه مرتين، وكان كل نشر يمسح تلك الذاكرة فيسمح لهما بالإرسال مجدداً. والنمط نفسه جعل مهمة خلفية تعمل مرتين. وكلاهما يتنسق الآن عبر مخزن مشترك، ويفشلان بالطريقة الآمنة إذا تعذر الوصول إلى المخزن.
العَرَض
يرسل Mythex تنبيهات الفريق عندما يحتاج شيء إلى نظر: خدمة متوقفة، أو مهمة متعثرة، أو رصيد منشئ منخفض بينما تطبيقه منشور. ولكل تنبيه نافذة إزالة تكرار، أي فترة يُرسل فيها التنبيه نفسه مرة واحدة فقط. وبالنسبة لتنبيه الرصيد المنخفض، هذه النافذة 24 ساعة.
خمس نسخ من التنبيه نفسه في عصر واحد تعني أن النافذة لم تصمد. والأسوأ أنها جاءت في أزواج، تفصل بينها نحو 11 دقيقة.
لماذا حدث ذلك
كانت حزمة التنبيهات لدينا تحتفظ بإزالة التكرار في الذاكرة: جدول من «مفتاح التنبيه ← متى أُرسل آخر مرة»، داخل العملية الجارية. وهذا يعمل عندما توجد عملية واحدة لا تُعاد تشغيلها أبداً. والإنتاج لدينا لا يملك أياً من الأمرين:
- نسختان من واجهة برمجة التطبيقات. يعمل الإنتاج بنسختين من واجهة البرمجة. كانت لكل منهما جدولها الخاص، فأرسلت كل واحدة التنبيه مرة. وهذه هي الأزواج.
- النشر. كل نشر يستبدل النسختين بعمليات جديدة بجداول فارغة. وبعد النشر، خرج التنبيه مجدداً كأنه لم يُرسل من قبل.
أما الفاصل البالغ نحو 11 دقيقة بين التنبيهين في كل زوج، فجاء من المهمة التي ترفع هذا التنبيه. تعمل هذه المهمة على النسختين وتأخذ قفلاً، فتفوز بالقفل كل نسخة بالتناوب، ورفعت كل واحدة التنبيه عندما جاء دورها.
وكان هناك عيب ثانٍ أهدأ في الكود نفسه. فلمنع الجدول من النمو إلى الأبد، كان يُنظّف نفسه عندما يبلغ 500 مفتاح، فيحذف كل مدخل أقدم من 15 دقيقة، بصرف النظر عن نافذته. وتحت الحمل، كانت نافذة إزالة التكرار البالغة 24 ساعة قد تدوم ربع ساعة فقط.
الإصلاح
سجل واحد مشترك لكل تنبيه
قبل أن ترسل واجهة البرمجة تنبيهاً، تطالب الآن بمفتاح إزالة التكرار الخاص به في Redis، وهو مخزن في الذاكرة مشترك تتحدث إليه النسختان، ولا يُعاد تشغيله عندما تُعاد تشغيل واجهة البرمجة. والمطالبة عملية واحدة: «اضبط هذا المفتاح فقط إذا لم يكن موجوداً، وانتهِ صلاحيته بعد النافذة». من يطالب به أولاً يرسل التنبيه، أما الأخرى فتجد المفتاح محجوزاً فتبقى صامتة. والعملية الجديدة بعد النشر تجد المفتاح ما زال موجوداً.
المفتاح والنافذة يأتيان من القاعدة نفسها التي تستخدمها حزمة التنبيهات، وقد صارت الآن مشتركة بينهما، فلا يمكن للطبقتين أن تختلفا حول ما يُعد «التنبيه نفسه».
فشل مفتوح، لا مغلق
أهم قرار كان ما نفعله عندما لا يمكن الوصول إلى Redis. اخترنا أن نرسل التنبيه على أي حال. إذا كان Redis متوقفاً، أو لم يستجب خلال 1.5 ثانية، يُرسل التنبيه. فالتنبيه المكرر ضجيج، أما التنبيه المفقود فيعني أن أحداً لم يُبلَّغ.
وتنبيهات P0، وهي الأخطر، لا يعيقها إزالة التكرار أبداً.
كل مدخل يحتفظ بنافذته
في إزالة التكرار داخل الذاكرة لحزمة التنبيهات، صار كل مدخل يخزن وقت انتهاء نافذته، ولا يحذف التنظيف إلا المدخلات التي انتهت نافذتها. مفتاح بنافذة 24 ساعة يبقى 24 ساعة، مهما وصل من تنبيهات أخرى.
النمط نفسه في مكان آخر
النمط نفسه ضاعف عملاً خلفياً أيضاً: مهمة مطابقة تحافظ على تطابق سجلنا لحالة كل مساحة عمل مع مزود البيئات السحابية لدينا. كل دورة تسرد كل بيئة في الحساب، وهي آلاف في الإنتاج وتستغرق نحو خمس ثوانٍ، إضافة إلى كل مشروع قيد التشغيل أو الإيقاف المؤقت. وكانت النسختان تشغّلانها كل دقيقتين.
وكتاباتها idempotent، أي أن تنفيذها مرتين يعطي النتيجة نفسها كما لو نُفّذت مرة، فلم يتعطل شيء. كان الأمر مجرد ضعف في العمل. والآن تُطالَب كل نافذة مدتها دقيقتان في Redis بالطريقة نفسها التي طالبت بها عدادات الاستخدام لدينا نوافذها، ولا تشغّل المهمة إلا النسخة التي تفوز بالمطالبة. وإذا تعذر الوصول إلى Redis، تعمل المهمة على أي حال: تنفيذ العمل مرتين غير ضار، أما عدم تنفيذه فضار.
كيف اختبرناه
تحاكي الاختبارات الإعداد الحقيقي. فهي تحمّل وحدة التنبيهات مرتين، كأنهما «نسختان»، وتوجههما إلى مخزن مشترك واحد، وترفع التنبيه نفسه من كل منهما، ثم تحمّل نسخة ثالثة جديدة تمثل «ما بعد النشر» وترفعه مجدداً. فيُرسل تنبيه واحد بالضبط. وتتحقق اختبارات أخرى من أن تنبيه منشئ مختلف ما زال يُرسل، وأن تنبيه P0 يُرسل من النسختين، وأن التنبيه ما زال يُرسل عندما يكون المخزن متوقفاً أو يعلق بعد المهلة. ولإصلاح التنظيف اختبار خاص: مفتاح بنافذة 24 ساعة يبقى بعد وصول 600 تنبيه آخر بعد 20 دقيقة.
أما مهمة المطابقة، فإن تشغيل نسختين معاً، إضافة إلى دورة ثالثة في النافذة نفسها، ينتج عنه استعلام واحد.
ما استخلصناه
- الحالة داخل الذاكرة تخص كل عملية وحدها. وأي شيء يجب أن يكون صحيحاً عبر الخوادم، مثل «أُرسل مرة واحدة» أو «عمل مرة واحدة»، يحتاج إلى مكان تتشاركه الخوادم.
- النشر إعادة تشغيل. الحالة التي تبقى فقط حتى إعادة التشغيل التالية لا تبقى خلال يوم عمل عادي.
- قرّر بأي طريقة تفشل. بالنسبة للتنبيهات، ينبغي أن يعني توقف المخزن المشترك تنبيهات أكثر، لا أقل.
- ابحث عن النمط لا الحالة الواحدة. كان التنبيه الحالة الصاخبة، أما المهمة المضاعفة فكانت المشكلة نفسها تستنزف العمل بصمت.
لا يظهر أي من هذا من داخل Mythex. ولمزيد عن كيفية إبقاء المنصة مستقرة، راجع كيف وصلنا إلى صفر أخطاء 502 أثناء النشر وملف التبادل المفقود. وإذا كانت فكرة تشغيل أكثر من نسخة من الخدمة جديدة عليك، فدليلنا عن ما هو الخادم يشرح الأساسيات.