حين تجعل إعادة المحاولة العطل أسوأ: مفاتيح عدم التكرار، والتراجع مع التشويش، وقواطع الدائرة
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗أهم النقاط
تضخيم إعادة المحاولة حساب بسيط: ثلاث محاولات إضافية لكل طلب تعني أن الخدمة التي تعاني أصلاً تستقبل ما يصل إلى أربعة أضعاف حِملها الطبيعي في اللحظة التي تملك فيها أقل قدرة على الاستيعاب.
أعد المحاولة فقط مع الأخطاء الآمنة للتكرار. أخطاء الاتصال و HTTP 502 و503 و504 و429 قابلة لإعادة المحاولة، أما 400 و401 و403 و422 فلا، لأن تكرار طلب فهمه الخادم ورفضه لن يعطي نتيجة مختلفة.
مفتاح عدم التكرار (Idempotency key) نص فريد يولّده العميل ويُرسل في ترويسة Idempotency-Key ليتعرّف الخادم على إعادة محاولة لعملية كتابة نفّذها بالفعل، فيُعيد الاستجابة الأصلية بدل تنفيذ العمل مرتين.
التشويش الكامل (full jitter) أفضل من التراجع الثابت. النوم مدة عشوائية بين الصفر والحد الأساسي مضروباً في اثنين أس رقم المحاولة يوزّع العملاء على النافذة بدل أن يتزامنوا في موجات متكررة.
قاطع الدائرة (circuit breaker) يفشل سريعاً أمام خدمة معطّلة أصلاً، فيحوّل مهلة 30 ثانية إلى خطأ فوري ويمنح الخدمة مساحة للتعافي. اجعله لكل خدمة على حدة، لا عاماً.
لماذا تجعل إعادة المحاولة العطل أسوأ بدل أن تصلحه؟
إعادة المحاولة تضاعف الحِمل تماماً حين تكون الخدمة أقل قدرة على استيعابه. خدمة تتعامل عادةً مع ألف طلب في الثانية، ومعها ثلاث إعادات محاولة من كل مستدعٍ، قد ترى أربعة آلاف طلب في الثانية لحظة بدء الفشل. الفشل يصنع الحِمل، والحِمل يُبقي الفشل. هذه الحلقة تُسمّى تضخيم إعادة المحاولة، وهي أكثر الطرق شيوعاً لتحويل تدهور جزئي إلى انقطاع كامل.
التزامن يزيد الأمر سوءاً. إذا أعاد كل عامل المحاولة بعد ثانية بالضبط، ثم ثانيتين، ثم أربع، يصل الأسطول كله إلى الخدمة في اللحظات الثلاث نفسها. الخدمة المتعافية تتلقى موجة، تسقط من جديد، وتُعيد ضبط ساعة الجميع على القيمة ذاتها — وهو قطيع الرعد الكلاسيكي. التراجع الثابت لا يوزّع الحِمل، بل يجدول اصطداماً.
التكلفة التي قلّل فريقنا من شأنها أن إعادة المحاولة تستهلك طاقتك أنت أيضاً. العامل النائم في انتظار المحاولة التالية عامل لا يعالج الطابور. وبدون مهلة لكل محاولة، أربع محاولات بثلاثين ثانية لكل منها تعني أن رسالة واحدة تحجز عاملاً لدقيقتين. الإنتاجية تنهار قبل أن تفشل الخدمة الخارجية تماماً.
أي حالات الفشل آمنة فعلاً لإعادة المحاولة؟
أعد المحاولة فقط حين يكون الطلب لم يصل إلى الخادم أصلاً، أو فشل بشكل مؤكد دون آثار جانبية. عملياً يعني ذلك التصنيف حسب رمز الحالة ونوع الخطأ، لا لفّ كل شيء في try/catch عام.
قابل لإعادة المحاولة: أخطاء اتصال TCP مثل ECONNRESET وECONNREFUSED وETIMEDOUT، وأخطاء DNS، وHTTP 502 و503 و504، وHTTP 429 مع أخذ التأخير من ترويسة Retry-After. هذه تعني أن الخادم غير متاح أو طلب منك الانتظار صراحة.
غير قابل لإعادة المحاولة: HTTP 400 و401 و403 و404 و422. الخادم استلم الطلب وفهمه ورفضه. تكرار حمولة خاطئة يعطي الرفض ذاته مع حِمل إضافي، وتكرار 401 قد يُفعّل حدود معدّل مزوّد المصادقة نفسه.
الحالة الصعبة حقاً هي HTTP 500 وانتهاء مهلة الطلب. كلاهما غامض: ربما تمّت الكتابة قبل ضياع الاستجابة. هنا بالضبط تظهر فائدة مفاتيح عدم التكرار. بدون مفتاح، إعادة محاولة طلب POST غامض إلى نقطة الدفع تخاطر بخصم مزدوج، وعدم إعادتها تخاطر بضياع دفعة صحيحة. مع المفتاح، يزول الغموض وتصبح إعادة المحاولة آمنة.
ما مفتاح عدم التكرار وكيف ننفّذه على جانب الخادم؟
مفتاح عدم التكرار نص فريد يولّده العميل، عادةً UUID من الإصدار الرابع، يُرسل في ترويسة Idempotency-Key حتى يُنتج تكرار الطلب النتيجة الأصلية بدل أثر جانبي ثانٍ. شركة Stripe نشرت هذه الترويسة، وصارت العُرف الفعلي في واجهات الدفع والتزويد.
التفصيلة الحاسمة أن المفتاح يجب أن يُولَّد مرة واحدة لكل عملية منطقية، لا مرة لكل محاولة HTTP. توليد UUID جديد داخل حلقة إعادة المحاولة يُبطل الآلية كلها، لأن كل محاولة تبدو حينها عملية جديدة تماماً للخادم. نحن نولّد المفتاح لحظة إدراج المهمة في الطابور ونخزّنه في سجلها، فتعيد كل محاولة — بما فيها محاولة بعد انهيار العملية وإعادة تشغيلها — استخدام القيمة نفسها.
على جانب الاستقبال، التنفيذ جدول بقيد فريد يتولّى التحكم بالتزامن، لا شيفرة تطبيق تفحص قبل الإدراج. يخزّن الجدول المفتاح كمفتاح أساسي، وبصمة لجسم الطلب، وحالة إما in_progress أو completed، ورمز الاستجابة وجسمها لإعادة تشغيلهما.
يُدرج المعالج المفتاح بحالة in_progress داخل المعاملة نفسها التي تنفّذ الكتابة. خطأ انتهاك القيد الفريد عند الإدراج يعني أن محاولة أخرى تتعامل مع المفتاح: إن كان السجل completed فأعد إرسال رمز الاستجابة وجسمها كما هما، وإن كان ما يزال in_progress فأعد HTTP 409 ليتراجع العميل بدل التسابق. الاعتماد على SELECT يتبعه INSERT يُعيد السباق الذي جاء المفتاح ليغلقه، لأن محاولتين متزامنتين قد تقرآن معاً أن السجل غير موجود.
خزّن بصمة جسم الطلب بجانب المفتاح. إذا وصل المفتاح نفسه بحمولة مختلفة فهذا خطأ في العميل، وإعادة HTTP 422 تكشفه فوراً بدل إعادة استجابة لا علاقة لها بصمت. واحذف السجلات بعد نافذة ثابتة — أربع وعشرون ساعة خيار شائع — لأن المفاتيح آلية أمان لإعادة المحاولة وليست تخزيناً دائماً.
كيف نضبط التراجع والتشويش والمهل؟
استخدم تراجعاً أُسّياً مع تشويش كامل، وضع سقفاً للتأخير، وحدّ العملية كلها بموعد نهائي. التشويش الكامل يعني النوم مدة عشوائية منتظمة بين الصفر وأصغر القيمتين: السقف، أو الأساس مضروباً في اثنين أس رقم المحاولة، بدل القيمة الحتمية. العشوائية هي المقصد: تفكّ ارتباط العملاء ببعضهم فترى الخدمة المتعافية معدّل وصول سلساً بدل موجات متزامنة.
في غلاف fetch بسيط تكون الحلقة كالتالي: اعتبر الخطأ المرمي قابلاً لإعادة المحاولة إلا إن كان AbortError، واعتبر الاستجابة قابلة لإعادة المحاولة فقط إذا كانت حالتها 429 أو 502 أو 503 أو 504، وإلا فأعِدها فوراً. وإذا حملت الاستجابة ترويسة Retry-After فاحترم قيمتها، وإلا فنَم مدة عشوائية حتى السقف الأُسّي.
ثلاث قواعد ضبط صارت غير قابلة للتفاوض عندنا. أولاً، لكل محاولة مهلتها الخاصة: AbortSignal.timeout سطر واحد يمنع مقبساً معلّقاً من حجز عامل إلى الأبد. ثانياً، احترم Retry-After حين يرسلها الخادم، لأن 429 بتأخير صريح هي الخدمة تخبرك بما تحتاجه بالضبط. ثالثاً، أبقِ المحاولات ضحلة — اثنتان أو ثلاث — ولا تُعد المحاولة داخل طبقة تُعاد محاولتها أصلاً، وإلا تضاعفت الأعداد: ثلاث محاولات في كل من ثلاث طبقات تعني 27 طلباً من استدعاء منطقي واحد.
ضع موعداً نهائياً للعملية كلها، لا عدّاد محاولات فقط. إذا كان المستدعي طلب HTTP ستنتهي مهلته خلال ثلاثين ثانية على أي حال، فجدول محاولات يمتد ستين ثانية هدر خالص: العميل استسلم بالفعل والعمل سيُهمل.
متى يفيد قاطع الدائرة ومتى يكون مبالغة؟
قاطع الدائرة غلاف يتتبّع الإخفاقات الأخيرة لخدمة واحدة، وبعد عتبة معيّنة يقصر الاستدعاءات التالية فيفشلها فوراً بدل محاولتها. له ثلاث حالات: مغلق، وتمرّ الاستدعاءات؛ ومفتوح، وتفشل فوراً لفترة تهدئة؛ ونصف مفتوح، حيث يقرر عدد صغير من الاستدعاءات التجريبية العودة للإغلاق أو الفتح من جديد.
القاطع يحلّ مشكلة لا تحلّها إعادة المحاولة. حين تكون الخدمة معطّلة تماماً، السلوك الصحيح هو التوقف عن استدعائها — لمنحها فرصة التعافي، ولوقف استهلاك ميزانية زمن الاستجابة لديك في نداءات محكوم عليها بالفشل. مهلة عشر ثوانٍ تصبح خطأً فورياً، أي أن واجهتك تعيد استجابة متدهورة سريعة وصادقة بدل تكديس الطلبات خلف اتصالات ميتة.
اجعل نطاق القاطع لكل خدمة، ويفضَّل لكل مجموعة نقاط نهاية. قاطع عام واحد يعني أن نقطة تحليلات معطوبة قد توقف نداءات الدفع. اختر العتبات من نسبة الفشل عبر نافذة متحركة لا من عدد خام، حتى لا تسقط الخدمات قليلة الحركة بسبب طلبين سيئي الحظ. وتأكد أن مسار البديل مفيد فعلاً: تقديم بيانات مخزّنة قديمة أو تأجيل العمل إلى الطابور يستحق بناء قاطع، أما صفحة خطأ كنت ستعرضها أصلاً فلا.
المبالغة تبدو هكذا: خدمة داخلية بنسخة واحدة وحركة متوقعة، أو خدمة إخفاقاتها نادرة وسريعة أصلاً. القاطع يضيف حالة وإعدادات وطريقة جديدة للخطأ — رأينا قاطعاً سيئ الضبط يبقى مفتوحاً بعد تعافي الخدمة بوقت طويل. المهل وإعادة المحاولة المحدودة أولاً، ثم أضف القاطع للخدمات التي أثبتت أنها تفشل بعنف.
أي آلية تعالج أي فشل؟
هذه الآليات الأربع ليست بدائل لبعضها. كل واحدة تعالج فشلاً مختلفاً، واستخدام إحداها دون البقية يترك ثغرة.
المهلة لكل محاولة تعالج طلباً معلّقاً يحجز عاملاً أو اتصالاً إلى الأبد، ولا تقول شيئاً عن نجاح العمل من عدمه.
إعادة المحاولة المحدودة مع التشويش تعالج الإخفاقات العابرة وتزامن العملاء، ولا تفعل شيئاً تجاه الآثار الجانبية المكرّرة في الكتابات الغامضة.
مفتاح عدم التكرار يعالج الكتابة المزدوجة حين تتبع إعادةُ المحاولة استجابةً ضائعة، ولا يفعل شيئاً تجاه تضخيم الحِمل أو خدمة معطّلة تماماً.
قاطع الدائرة يعالج النداءات المهدورة وزمن الاستجابة أمام خدمة معطّلة، ولا يفعل شيئاً للأعطال العابرة المفردة ولا لصحة كتابة واحدة.
بترتيب العائد مقابل الجهد: أضف مهلاً لكل محاولة أولاً، فهي سطر واحد وتحدّ أسوأ الحالات. ثم أضف مفاتيح عدم التكرار لكل كتابة غير متكررة، لأنك بدونها لا تستطيع إعادة محاولة الإخفاقات الغامضة بأمان إطلاقاً. ثم أصلح جدول إعادة المحاولة بالتشويش والميزانية. وأضف القواطع أخيراً، للخدمات التي استحقتها.
الأسئلة الشائعة
س: هل أستطيع استخدام بصمة جسم الطلب كمفتاح عدم تكرار؟
ج: لا، لأن طلبين متطابقين ومشروعين — كعميل يشتري المنتج نفسه مرتين عن قصد — سيتصادمان ويُبتلع الثاني بصمت. ولّد UUID لكل عملية منطقية وخزّن بصمة الجسم بشكل منفصل لكشف إعادة استخدام المفتاح بحمولة مختلفة.
س: هل إعادة محاولة طلب GET آمنة بدون مفتاح عدم تكرار؟
ج: نعم. GET وHEAD وPUT وDELETE مُعرّفة في مواصفة HTTP كعمليات لا يتغير أثرها بالتكرار، فتكرارها ينتج حالة الخادم نفسها. POST وPATCH هما من يحتاج المفتاح، وPUT المنفّذ بآثار جانبية مثل العدّادات يحتاجه عملياً أيضاً.
س: كم عدد إعادات المحاولة المناسب؟
ج: اثنتان أو ثلاث في طبقة واحدة. المحاولات الإضافية نادراً ما تحوّل الفشل إلى نجاح، وإعادة المحاولة في طبقات متداخلة تتضاعف: ثلاث محاولات في عميل HTTP ملفوف بثلاث محاولات في طابور المهام تصل إلى ستة عشر طلباً من استدعاء منطقي واحد.
س: هل يبطّئ التشويش الكامل إعادة المحاولة في المتوسط؟
ج: قليلاً لعميل واحد، لأن التأخير المتوقع نصف السقف بدل القيمة الكاملة. لكنه أسرع إجمالاً عبر الأسطول، لأن الوصول غير المتزامن يمنح الخدمة فرصة التعافي بدل إسقاطها مع كل موجة متزامنة.
س: أين يجب أن يعيش مفتاح عدم التكرار لمهمة خلفية في طابور؟
ج: في سجل المهمة نفسه، يُكتب لحظة إدراجها في الطابور. توليده وقت الاستدعاء يعني أن مهمة انهارت وأُعيد تسليمها ستنتج مفتاحاً جديداً، وهو تماماً سيناريو الكتابة المزدوجة الذي جاء المفتاح لمنعه.
قراءات إضافية
هندسة الواجهات
الوضع الداكن دون وميض في Next.js 16: كيف نستخدم سكربت سمة حاجبًا وcolor-scheme وزر تبديل آمنًا مع الـ Hydration
وميض الوضع الداكن عند إعادة تحميل الصفحة مشكلة توقيت: الخادم لا يستطيع قراءة localStorage. نشرح سكربت السمة الحاجب الذي نستخدمه في Next.js 16، ولماذا لا نضع السمة في cookie حفاظًا على الرسم الثابت، وكيف نبني زر تبديل يمر بالـ hydration دون أي عدم تطابق.
هندسة الواجهات
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط
واجهت بطاقات المشاركة المولّدة لدينا ثلاث مفاجآت: خط WOFF2 لم يستطع المحرّك قراءته، وملف README لـ Satori يصف محرّكًا أحدث من المضمّن في Next.js، وصفحة ظلّت تعرض صورة الـ layout. هذه ملاحظاتنا الميدانية حول opengraph-image.tsx في Next.js 16 وأولوية البيانات الوصفية وفحص og:image قبل مشاركة أي رابط.