المهل الزمنية والمحاولات المتكررة والتراجع التدريجي
المهل الزمنية والمحاولات المتكررة والتراجع التدريجي
كل نظام موزع يُجري استدعاءات عن بُعد — إلى قاعدة بيانات، أو خدمة مصغّرة، أو واجهة برمجية خارجية. أي من هذه الاستدعاءات يمكن أن يتوقف، أو يفشل، أو يستجيب ببطء. بدون ضمانات واضحة، يتحول الاعتماد الخاطئ على خدمة واحدة بطيئة إلى عطل يشمل النظام بأكمله. المهل الزمنية (Timeouts)، والمحاولات المتكررة (Retries)، والتراجع الأسي (Exponential Backoff) هي الأدوات الثلاثة الأساسية التي تمنع الاستدعاء البعيد من أن يتحول إلى أزمة.
لماذا المهل الزمنية غير قابلة للتفاوض
المهلة الزمنية هي موعد نهائي: إذا لم يرد الطرف البعيد خلال N ميلي ثانية، أوقف الاستدعاء وأطلق الخيط والاتصال والذاكرة التي كانت تنتظر. بدون مهل زمنية:
- تتراكم الخيوط في انتظار استجابة قد لا تأتي أبدًا.
- تنضب مجمّعات الاتصال، وتبدأ الطلبات الجديدة في الانتظار أو الفشل فورًا.
- تجرّ خدمة واحدة بطيئة كل خدمة تستدعيها إلى نفس البطء — وهو ما يُعرف بـالفشل المتتالي (cascading failure).
هناك قيمتان مختلفتان للمهلة الزمنية يجب ضبطهما لكل استدعاء بعيد:
- مهلة الاتصال (Connect timeout) — المدة التي تنتظر فيها اكتمال مصافحة TCP (عادةً 1–5 ثوان). مهلة اتصال طويلة جداً تعني في الغالب أن المضيف غير متاح أو مثقل قبل أن يبدأ المعالجة.
- مهلة القراءة (Read timeout) — المدة التي تنتظر فيها الاستجابة الكاملة بعد فتح الاتصال. يجب أن تعكس هذا أسوأ وقت معالجة متوقع للعملية، وليس فقط زمن الشبكة.
مرجعيات من الأنظمة الإنتاجية الكبرى: إطار gRPC الداخلي لـGoogle يستخدم مهلة افتراضية مدتها 5 ثوان؛ حزمة AWS SDK لـDynamoDB تستخدم ثانيتين للاتصال و10 ثوان للقراءة؛ توصي Stripe بـ80 ثانية لاستدعاءات واجهة الدفع. لا توجد قيمة عالمية — كل خدمة تحتاج إلى قيمة مضبوطة خاصة بها.
المحاولات المتكررة: متى وكيف
المهلة الزمنية تُخبرك أن الاستدعاء فشل. المحاولة المتكررة تحاول مرة أخرى. لكن المحاولات الساذجة تُسبب مشاكل جدية:
- عاصفة المحاولات (Retry storms): عندما تكون خدمة أدنى بطيئة أو تعيد أخطاء، تُعيد مئات العملاء المحاولة في وقت واحد، مما يُضاعف الحمل تمامًا حين لا تستطيع الخدمة تحمّله.
- العمليات غير المتماثلة (Non-idempotent): إعادة محاولة عملية دفع أو إنشاء طلب بدون مفاتيح التكافؤ تُسبب رسوماً أو طلبات مكررة.
المحاولات المتكررة منطقية فقط للـفشل العابر — انقطاعات الشبكة، الحمل اللحظي الزائد (HTTP 429 أو 503)، التعذر المؤقت. وهي تأتي بنتيجة عكسية في حالة:
- أخطاء العميل مثل
400 Bad Requestأو404 Not Found— لن يُغيّر أي عدد من المحاولات طلباً مشوهاً. - أخطاء الخادم المستمرة التي تدل على عطل جذري في الخدمة الأدنى.
التراجع الأسي مع الاضطراب العشوائي
التراجع الأسي يعني أن وقت الانتظار بين المحاولات ينمو بشكل أسي: المحاولة الأولى تنتظر ثانية واحدة، الثانية ثانيتين، الثالثة 4 ثوان، وهكذا. هذا يمنح الخدمة الأدنى وقتاً متزايداً للتعافي قبل أن تُصاب مرة أخرى.
لكن التراجع الأسي الصرف يخفي فخاً: إذا أصاب نفس الخطأ الآلاف من العملاء في وقت واحد، فسيتراجعون جميعاً بنفس المقدار ثم يُعيدون المحاولة جميعاً في وقت واحد — وهو ما يُسمى القطيع الرعاد (thundering herd). الحل هو الاضطراب العشوائي (jitter): إضافة عنصر عشوائي لكل فترة انتظار بحيث تتوزع المحاولات عبر الزمن.
صيغة شائعة (توصي بها AWS):
يمنع cap الانتظار من النمو إلى ما لا نهاية. random(0.5, 1.0) يوزع المحاولات عبر نافذة زمنية بدلاً من توافقها. يُسمى هذا النمط الاضطراب الكامل (full jitter)، وهو الأكثر استخداماً.
ميزانيات المهل الإجمالية وانتشار الموعد النهائي
عندما تستدعي الخدمة A الخدمةَ B التي تستدعي بدورها الخدمة C، لكل قفزة مهلتها الخاصة. إذا منحت A الخدمةَ B مهلة 500 ميلي ثانية وأعطت B الخدمةَ C كذلك 500 ميلي ثانية، لكن B تحتاج 200 ميلي ثانية من المعالجة الداخلية، فإن C لديها في الواقع 300 ميلي ثانية فقط — لكن مهلتها مضبوطة على 500 ميلي ثانية. ستستمر في المحاولة طويلاً بعد أن تكون A قد أعادت خطأً للمستخدم بالفعل. هذا يُهدر الموارد ويُسبب عمل يتيم (orphaned work).
الحل هو انتشار الموعد النهائي (deadline propagation) — يضبط المُستدعي الأصلي موعداً نهائياً بساعة الحائط ويُمرّره عبر كل قفزة كترويسة طلب (مثل grpc-timeout في gRPC أو ترويسة مخصصة X-Request-Deadline في HTTP). كل خدمة أدنى تتحقق مما إذا كانت هناك ميزانية متبقية قبل بدء العمل، وتلغي الطلب فوراً إذا انتهى الموعد.
التكافؤ: شبكة الأمان للمحاولات المتكررة
إعادة محاولة GET آمنة دائماً — قراءة البيانات مرتين تُعيد نفس النتيجة. أما إعادة محاولة POST /payments بدون ضمانات فتُفرض الرسوم على العميل مرتين. التكافؤ (Idempotency) هو خاصية تجعل تنفيذ عملية أكثر من مرة ينتج نفس النتيجة كما لو نُفِّذت مرة واحدة.
النمط المعتمد: يُنشئ العميل UUID لكل عملية منطقية (وليس لكل محاولة) ويُرسله كترويسة أو حقل في جسم الطلب (Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000). يخزّن الخادم النتيجة مفهرسةً بذلك UUID لفترة قصيرة (عادةً 24 ساعة). عند أي محاولة لاحقة بنفس المفتاح، يُعيد الخادم النتيجة المحفوظة دون إعادة تنفيذ العملية.
الجمع: نمط استدعاء مرن في الإنتاج
الاستدعاء البعيد الجاهز للإنتاج يجمع الآليات الثلاث:
- اضبط مهلة اتصال (1–3 ثوان) ومهلة قراءة بناءً على p99 للخدمة الأدنى.
- عند المهلة أو الخطأ القابل للمحاولة: انتظر باستخدام التراجع الأسي مع اضطراب كامل.
- حدّد المحاولات بـ2–3 محاولات؛ استسلم وأعد خطأ للمُستدعي.
- انشر الموعد النهائي في كل استدعاء أدنى لكيلا تهدر أي قفزة موارد بعد انتهاء المهلة الموجّهة للمستخدم.
- استخدم مفاتيح التكافؤ لجميع العمليات غير الآمنة وغير المتكافئة.
هذه الخطوات الخمس تمنع أكثر أصناف فشل الأنظمة الموزعة شيوعاً على مستوى الاستدعاء الواحد. الدرس التالي يُضيف قواطع الدائرة والحواجز — التي تعمل على مستوى أعلى، إذ تقرر هل تحاول الاستدعاء أصلاً بناءً على سجل الفشل الأخير.