الاستئناف قد ينجح مرتين وهذه هي المشكلة
تعالج أدلة استعادة سير العمل عادة حفظ نقطة التقدم ثم استكمال الخطوة التالية بعد انقطاع الكهرباء أو انهيار العملية. لكن هناك حالة أصعب: وكيل تنفيذ قديم يتوقف عن إرسال Heartbeat بسبب انقطاع الشبكة، فيعتبره الخادم مفقودًا ويعطي المهمة لوكيل جديد. بعد ثوان تعود الشبكة، فيستأنف الوكيل القديم عمله أيضًا. أصبح لدينا منفذان يعتقد كل منهما أنه صاحب المهمة، وقد يرسلان الطلب نفسه مرتين. هذه حالة ازدواج قيادة أو split-brain على مستوى المهمة، وليست مجرد فشل استئناف. سنشرح كيف تختبر ملكية المهمة عبر Lease ونسخة ملكية متزايدة، وكيف تمنع الآثار المكررة عندما لا تكفي مراقبة Heartbeat وحدها.
ما الفرق بين Heartbeat و Lease؟
Heartbeat إشارة دورية تقول إن العملية ما زالت قادرة على إرسال نبضة إلى جهة المراقبة، لكنه لا يثبت أن العملية تملك الحق الحصري في تعديل مورد. Lease عقد ملكية مؤقت له هوية حامل ووقت تجديد ومدة صلاحية، ويجب أن يخضع الحصول عليه وتجديده لتنسيق متسق بين المتنافسين. توثق Kubernetes كائنات Lease في مجموعة coordination.k8s.io لاستخدامات منها نبضات العقد وانتخاب القائد. لا يعني نسخ هذه البنية حرفيًا أنها مناسبة لكل نظام؛ قد تستخدم قاعدة بيانات تدعم تحديثًا شرطيًا أو مخزن تنسيق مخصصًا. الأهم وجود مصدر واحد لحقيقة الملكية وقاعدة واضحة تمنع وكيلين من الحصول على الحق نفسه في الوقت ذاته.
المهلة الزمنية ليست إثباتًا أن القديم توقف
إذا مضت مدة Lease دون تجديد، يمكن للنظام إسناد المهمة إلى وكيل آخر. لكن الوكيل القديم قد يكون معزولًا عن قاعدة التنسيق فقط، بينما ما زال قادرًا على الوصول إلى موقع خارجي أو قاعدة أخرى. لذلك لا يكفي أن يقول الخادم «انتهت المهلة» ثم يفترض أن القديم توقف فعليًا. صمم كل خطوة ذات أثر بحيث تتحقق من حق الملكية قبل تنفيذها، ويفضل عند نقطة كتابة الأثر نفسها. إذا كان النظام لا يملك طريقة لإلغاء الطلب الخارجي بعد إرساله، فستحتاج إلى idempotency أو تعويض أو مراجعة بشرية بحسب نوع الإجراء. لا تجعل الزمن المحلي للوكيل هو المرجع الوحيد؛ قد تختلف الساعات أو تتوقف العملية مؤقتًا.
رقم الجيل Fencing Token يمنع القائد القديم
الفكرة العملية هي أن يحصل كل مالك جديد على رقم جيل متزايد، مثل 41 ثم 42، عند انتقال ملكية المهمة. عند محاولة كتابة نتيجة، يرسل الوكيل رقم الجيل معه، ولا يقبل مخزن الحالة كتابة من جيل أقدم من الجيل المسجل. هكذا قد يعود الوكيل القديم إلى العمل، لكنه لا يستطيع تسجيل نتيجة جديدة إذا كانت طبقة التخزين تطبق الشرط بصورة ذرية. يجب أن يكون إصدار الرقم وتحديث الملكية في عملية متسقة، لا عبر قراءة ثم كتابة منفصلتين يمكن أن تتسابقا. انتبه إلى أن Fencing في قاعدة بياناتك لا يمنع بالضرورة طلبًا سبق أن وصل إلى خدمة خارجية لا تفهم هذا الرقم؛ هنا يلزم ضمان إضافي عند حدود الخدمة الخارجية.
سجل حالة المهمة كآلة حالات واضحة
اقترح حالات pending و leased و running و succeeded و failed و needs_reconciliation، مع حقول ownerId و leaseExpiresAt و generation و attemptId. يجب ألا يتحول نجاح قديم إلى نجاح نهائي إذا كانت ملكيته قد انتهت، وألا ينتقل النظام من succeeded إلى running لمجرد وصول رسالة متأخرة. استخدم تحديثًا شرطيًا يربط حالة السجل برقم الجيل المتوقع. لا تحفظ كلمات مرور أو بيانات جلسات حساسة في سجلات المحاولات؛ يكفي معرف العملية وملخص الخطوة ووقت الحدث. افصل حالة محاولة التنفيذ عن حالة المهمة الإجمالية حتى تستطيع التحقيق في محاولتين متنافستين دون محو أثر إحداهما. سجل سبب انتقال الملكية ونتيجة فحص الملكية عند كل كتابة مهمة.
اختبار انقطاع الشبكة في اللحظة الصعبة
أنشئ Workflow اختباريًا ينفذ خطوة غير مؤذية على خدمة وهمية تحفظ عدد مرات استقبال الطلب. شغّل الوكيل الأول حتى يحصل على Lease، ثم اقطع اتصاله بخدمة التنسيق فقط مع إبقاء قدرته على الوصول إلى الوجهة التجريبية. انتظر انتهاء المهلة وإسناد المهمة إلى الوكيل الثاني، ثم أعد الاتصال بالأول. تحقق من عدد الطلبات التي وصلت ومن عدد النتائج التي قبلتها قاعدة الحالة. النتيجة الجيدة ليست مجرد ظهور حالة succeeded؛ بل وجود محاولة واحدة معتمدة وعدم قبول كتابة قديمة، ومعرفة ما إذا كانت الوجهة الخارجية استقبلت طلبًا مكررًا. لا تنفذ هذا السيناريو على عمليات دفع أو نشر أو حذف حقيقية.
التجديد المتأخر والتسابق على الملكية
اختبر تأخر تجديد Lease عندما تكون قاعدة البيانات بطيئة، ثم وصول طلب التجديد القديم بعد أن حصل الوكيل الجديد على الملكية. ينبغي أن يرفض النظام التجديد المتأخر باستخدام شرط على هوية الحامل ورقم الجيل، لا أن يعيد تمديد ملكية انتهت. اختبر كذلك بدء وكيلين في اللحظة نفسها: يجب أن ينجح أحد تحديثي الحصول على Lease فقط. لا تعتمد على ترتيب رسائل الشبكة أو على نوم ثابت داخل الكود لإثبات السلامة؛ استخدم حواجز تزامن اصطناعية في الاختبار تجعل التنافس قابلًا للتكرار. إذا فشل الاختبار مرة من مئة، فذلك فشل حقيقي يستحق التحقيق لا ضوضاء يمكن تجاهلها.
المخرجات الخارجية تحتاج سياسة خاصة
عند تنفيذ إجراء خارج مخزن الحالة، مثل إرسال رسالة أو إنشاء طلب لدى مزود آخر، قد لا تستطيع فرض Fencing على الطرف الآخر. هنا استخدم مفتاح idempotency ثابتًا لكل أثر منطقي إذا كانت الواجهة تدعمه، وسجل الاستجابة والمعرّف الخارجي في مخزن موثوق. إذا كانت الواجهة لا تدعم منع التكرار، فصمم خطوة تحقق قبل إعادة المحاولة أو وضعًا يحتاج إلى تسوية يدوية عند عدم اليقين. لا تعتبر انتهاء مهلة HTTP دليلًا على أن الطلب لم يُنفذ؛ قد يكون الخادم عالجه ثم ضاع الرد. فرّق بين exactly-once كهدف عمل وبين ضمانات at-least-once التي تقدمها كثير من أنظمة الرسائل. لا تعد بمنع مطلق للتكرار إذا كانت الحدود الخارجية لا تسمح بذلك.
اختبر فقدان Heartbeat دون فقدان حق التنفيذ
قد يتوقف إرسال النبضات بسبب ضغط CPU أو توقف مؤقت في مجمع القمامة أو تجميد النظام، بينما تكون العملية سليمة. إذا كان النظام يقتلها فورًا عند غياب نبضة واحدة، فقد يزيد عدد محاولات المهمة ويضاعف الضغط. صمم مهلة وعتبة بناء على زمن العمل الفعلي وتوزيع تأخر الشبكة، ثم اختبر فقد نبضتين أو ثلاث بصورة اصطناعية. لكن زيادة المهلة كثيرًا تطيل الاسترداد بعد العطل الحقيقي. وثق المقايضة بين سرعة الاستئناف واحتمال الملكية المزدوجة، واجعل Fencing هو الحماية الأساسية عند الكتابة. لا تتعامل مع قيمة Heartbeat بوصفها دليلًا على سلامة كل خطوة؛ قد تكون العملية ترسل نبضات بينما عالق تنفيذ المهمة نفسها.
مصفوفة فشل تغطي أكثر من Crash
ضع حالات: انهيار الوكيل قبل حفظ النتيجة، انقطاع اتصاله بخادم التنسيق، انقطاعه عن الوجهة الخارجية فقط، تأخر تجديد Lease، ازدواج بدء الوكلاء، عودة رسالة نجاح قديمة، وإعادة تشغيل الخادم أثناء نقل الملكية. لكل حالة سجل المالك قبل وبعد، والجيل، وعدد الآثار الخارجية، وحالة المهمة النهائية. لا تكتف باختبار حالة النجاح المعتادة؛ كثير من أخطاء الازدواج لا تظهر إلا عند ترتيب رسائل نادر. شغّل السيناريوهات بتزامن محدود على بيئة اختبار، واجعلها قابلة للتوقف والتنظيف. احفظ سجلات مختصرة ذات معرّفات ترابط، ولا تضع فيها محتوى جلسات المستخدمين أو مفاتيح الاتصال.
اختبارات تنازع ملكية المهمة
| عودة وكيل قديم | رفض كتابة الجيل القديم | قبول نتيجتين |
| بدء وكيلين | حامل Lease واحد | ملكية مزدوجة |
| رد HTTP مفقود | تسوية أثر خارجي | تكرار أثر بلا تحقق |
| تجديد متأخر | رفض تجديد منتهي | تمديد ملكية قديمة |
كيف تقيس سلامة التنفيذ؟
المؤشر الأهم هو عدد الآثار المنطقية المعتمدة لكل مهمة، وليس فقط نسبة المهام التي وصلت إلى succeeded. أضف عدد محاولات الحصول على Lease، وعدد الكتابات المرفوضة بسبب جيل قديم، وعدد الحالات التي تحتاج إلى تسوية، ومدة التعافي بعد انقطاع الوكيل. اعرض أعدادًا فعلية مع عدد السيناريوهات؛ لا تقل إن النظام مضمون تمامًا لأن عشر تجارب نجحت. تحقق من أن الرفض لا يؤدي إلى حلقة محاولات بلا نهاية أو تراكم عمليات معلقة. اختبر إيقاف الوكيل القديم بأمان بعد اكتشاف فقد الملكية، مع عدم حذف سجله قبل انتهاء التحقيق. وأعد اختبار كل حالة بعد تعديل منطق التزامن أو التخزين.
المراجع والقراءة المتصلة
تشرح وثائق Kubernetes مفهوم Lease واستخدامه في التنسيق: https://kubernetes.io/docs/concepts/architecture/leases/ ، وتفاصيل حقول Lease: https://kubernetes.io/docs/reference/kubernetes-api/coordination/lease-v1/ . هذه المراجع تصف نظام Kubernetes ولا تثبت تلقائيًا سلامة أي تطبيق مستقل. وللانتقال من مفهوم حفظ نقطة التقدم إلى هذه المشكلة المتخصصة، اقرأ دليل إيجي تاج عن استئناف Workflow بعد Crash: https://egytag.com/ar/articles/تصميم-workflow-قابل-للاستئناف-بعد-انقطاع-الكهرباء-أو-crash . اجعل الاختبار في بيئة غير إنتاجية، وميّز بين حماية سجل الحالة وبين ضمان عدم تكرار الأثر لدى خدمات لا تدعم المفاتيح الثابتة أو الأجيال.
شارك في تقييم ونقاش المقال
رأيك يضيف قيمة للمقال ويساعدنا على تحسين المحتوى والنقاش حوله.
النقاش
جارٍ تحميل التعليقات…