فشل استدعاء واجهة برمجة تطبيقات الدفع (API) الخاصة بك في الساعة الثانية صباحًا. هل كان السبب خللًا في الشبكة، أم تجاوزًا للحد الأقصى للمعدل، أم خادمًا متوقفًا؟ تحدد الإجابة ما إذا كانت إعادة المحاولة ستنقذ المعاملة أم ستخصم من العميل مرتين.
تُعد إعادة المحاولة نمط المرونة الأكثر شيوعًا في الأنظمة الموزعة، لكنها من أكثر الأنماط تعرضًا لسوء التنفيذ. قد تحوّل حلقة حول استدعاء HTTP انقطاعًا مدته 30 ثانية إلى انقطاع مدته 30 دقيقة، عندما يضغط آلاف العملاء على خادم متعطل في اللحظة نفسها. أما عند تطبيقها بشكل صحيح، فتمتص الإخفاقات العابرة دون أن يلاحظ المستخدمون شيئًا.
يغطي هذا الدليل منطق إعادة المحاولة في أنظمة الإنتاج: رموز الحالة القابلة لإعادة المحاولة، والتراجع الأسي مع التذبذب الكامل، ورأس Retry-After، ومفاتيح الثباتية، وميزانيات إعادة المحاولة، وقواطع الدائرة. كما يوضح اختبار هذا السلوك عبر محاكاة أخطاء 429 و503 باستخدام خوادم Apidog الوهمية. فنمط إعادة المحاولة الذي لم تختبره ضد خادم فاشل هو تخمين، وليس تصميمًا. تتعلم الفرق التي تبني منطق إعادة محاولة واجهة برمجة تطبيقات التكنولوجيا المالية هذا بالطريقة المكلفة؛ لست مضطرًا لذلك.
لماذا تزيد عمليات إعادة المحاولة الساذجة من سوء الانقطاعات
تخيل خدمة تتعامل مع 1,000 طلب في الثانية، ثم تتعطل لمدة خمس ثوانٍ. إذا أعاد كل عميل المحاولة فورًا ثلاث مرات، فسيتحول الحمل إلى 4,000 طلب في الثانية على خادم يعاني أصلًا، وقد ينهار بالكامل. ثم يعيد العملاء المحاولة مرة أخرى.
تُسمى هذه الحلقة عاصفة إعادة المحاولة، بينما يُعرف الاندفاع المتزامن عند عودة الخادم باسم القطيع الهائج. يشير كتاب SRE من Google إلى هذا النمط في فصل معالجة الأعطال المتتالية: إعادة المحاولة دون تراجع تزيد الحمل عندما يكون النظام في أمسّ الحاجة إلى تخفيفه.
تتسبب مشكلتان في معظم عواصف إعادة المحاولة:
- عدم وجود تأخير: تؤدي إعادة المحاولة الفورية إلى مضاعفة الحمل خلال أسوأ نافذة.
- التأخير الثابت: إذا انتظر كل عميل ثانية واحدة بالضبط، فسيعود الجميع في اللحظة نفسها.
الحل ليس الامتناع عن إعادة المحاولة، بل إعادة المحاولة بشكل انتقائي، مع تأخيرات عشوائية متزايدة وسقف صارم للحمل الإضافي.
أعد محاولة هذه الإخفاقات، ولا تعيد محاولة تلك أبدًا
قبل حساب التراجع، أنشئ جدول قرار. إعادة محاولة طلب رفضه الخادم لأنه غير صالح تهدر السعة وتلوث السجلات، بينما إعادة محاولة الخطأ العابر هي الهدف الأساسي.
أعد محاولة هذه الحالات:
| الإشارة | المعنى |
|---|---|
| 429 Too Many Requests | تم بلوغ حد المعدل؛ تراجع وأرسل الطلبات ببطء أكبر. |
| 502 Bad Gateway | أعادت بوابة علوية استجابة غير صالحة؛ غالبًا ما يكون الخطأ عابرًا. |
| 503 Service Unavailable | الخادم مثقل أو قيد إعادة التشغيل. |
| 504 Gateway Timeout | كانت تبعية علوية بطيئة جدًا. |
| فصل الاتصال، أخطاء DNS، مهلة المقبس | قد لا يكون الطلب قد وصل إلى الخادم. |
تحتاج مهلة 504 إلى عناية خاصة: ربما عالج المصدر الطلب رغم توقف البوابة عن الانتظار. يصبح هذا التمييز مهمًا عند استخدام الثباتية.
لا تعِد محاولة هذه الحالات تلقائيًا:
| الإشارة | المعنى |
|---|---|
| 400 Bad Request | حمولة الطلب غير صحيحة، وستظل غير صحيحة في المحاولة التالية. |
| 401 Unauthorized | بيانات الاعتماد خاطئة أو منتهية؛ حدّث الرمز المميز بدل الدخول في حلقة. |
| 403 Forbidden | لا تملك الإذن المطلوب؛ إعادة المحاولة لن تمنحك إياه. |
| 422 Unprocessable Entity | فشل التحقق من الصحة؛ أصلح البيانات، لا التوقيت. |
القاعدة العملية: أعد المحاولة عندما يرتبط الفشل بحالة الخادم أو الشبكة، وافشل بسرعة عندما يرتبط بطلبك. تقع 429 في المنتصف: فهي قابلة لإعادة المحاولة، لكنها تشير أيضًا إلى أن معدل طلباتك يحتاج إلى معالجة عبر حل تحديد معدل.
صيغة التراجع الأسي وأهمية التذبذب
يعني التراجع الأسي أن تنتظر كل إعادة محاولة مدة أطول من سابقتها:
delay = base * 2^retry_count
مع قاعدة مقدارها 500 مللي ثانية، تصبح التأخيرات: 0.5 ثانية، ثم 1، ثم 2، ثم 4، ثم 8 ثوانٍ. أضف سقفًا مثل 30 ثانية:
delay = min(cap, base * 2^retry_count)
يخفف ذلك الضغط، لكنه لا يمنع التزامن. إذا فشل 5000 عميل في اللحظة نفسها، فسيعودون جميعًا عند 0.5 ثانية، ثم ثانية، ثم ثانيتين. ستظل هناك موجات من الطلبات.
يكسر التذبذب هذا التزامن بإضافة العشوائية. أظهر تحليل مدونة AWS Architecture في تحليل التراجع الأسي والتذبذب أن التذبذب الكامل حقق أقل عدد إجمالي من الاستدعاءات وأقصر أوقات إنجاز تقريبًا:
delay = random_between(0, min(cap, base * 2^retry_count))
يوزع التذبذب الكامل العملاء بالتساوي عبر نافذة التأخير. اختبر تحليل AWS أيضًا التذبذب المتساوي والتذبذب غير المترابط؛ وتصدر التذبذب الكامل وغير المترابط النتائج، بينما يظل التذبذب الكامل الأبسط في التنفيذ. استخدمه كنمط افتراضي ما لم تُظهر القياسات خلاف ذلك.
احترم رأس Retry-After
التراجع هو تقدير العميل للمدة المناسبة للانتظار، لكن الخادم قد يملك معلومات أدق. يحمل رأس Retry-After، المستخدم عادةً مع 429 و503، عددًا من الثواني أو تاريخ HTTP:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
عند وجود الرأس، استخدمه بدل التأخير المحسوب؛ فالخادم يعرف متى تُعاد تهيئة نافذة المعدل أو تنتهي الصيانة. مع ذلك، طبّق دائمًا حدًا أقصى وعددًا أقصى للمحاولات، حتى لا يؤدي رأس مثل Retry-After: 86400 إلى تعليق عامل الخدمة ليوم كامل.
الثباتية: شرط إعادة محاولة طلبات POST
تكمن المشكلة في أن مهلة POST /v1/payments قد تحدث بعد أن يعالج الخادم الطلب. إذا أعدت المحاولة دون حماية، فقد تنشئ دفعة ثانية.
الحل هو مفتاح الثباتية: معرّف فريد ينشئه العميل، غالبًا UUID، ويرسله في كل محاولة للعملية المنطقية نفسها. يخزن الخادم المفتاح مع الاستجابة الأولى، ثم يعيد الاستجابة المخزنة عند تكرار المفتاح. تعمل طلبات Stripe الثابتة بهذه الطريقة.
قاعدتان أساسيتان:
- عملية واحدة، مفتاح واحد: تستخدم كل محاولات دفعة منطقية المفتاح نفسه، بينما تحصل العملية الجديدة على مفتاح جديد.
- أنشئ المفتاح قبل الحلقة: لا تنشئ مفتاحًا جديدًا داخل حلقة إعادة المحاولة.
إذا كانت واجهة API لا تدعم مفاتيح الثباتية، فلا تعِد محاولة عمليات الكتابة غير الثابتة تلقائيًا. اعرض الفشل ودع إنسانًا أو مهمة تسوية تتخذ القرار.
ميزانيات إعادة المحاولة وقواطع الدائرة
يحدد التراجع موعد إعادة المحاولة، لكنه لا يحد عدد المحاولات الكلي. وقد تتضاعف المحاولات عبر الطبقات: إذا أعادت بوابة API المحاولة ثلاث مرات وأعاد عميل الخدمة المحاولة ثلاث مرات، فقد تتحول نقرة واحدة إلى تسعة طلبات.
ميزانيات إعادة المحاولة
بدلًا من «ثلاث محاولات لكل طلب»، حدّد أن المحاولات قد تضيف مثلًا 10% فقط من الحمل خلال نافذة منزلقة. عند استنفاد الميزانية، تفشل الطلبات فورًا. يدعم Linkerd وEnvoy هذا النمط كإعداد من الدرجة الأولى.
قواطع الدائرة
تتبع معدل فشل كل تبعية. عند تجاوز الحد، تُفتح الدائرة وتفشل المكالمات فورًا دون الوصول إلى الشبكة. بعد فترة تهدئة، تسمح الدائرة بعدد قليل من الطلبات الاستكشافية. إذا نجحت، تُغلق مجددًا.
يخفف التراجع الحمل تدريجيًا، بينما يلغي قاطع الدائرة المكالمات أثناء الفشل المستمر. لذلك يجمع التصميم الجاد بين الاثنين.
مثال جاهز للإنتاج بلغة بايثون
يجمع المثال التالي تصفية الحالات القابلة لإعادة المحاولة، والتذبذب الكامل، ودعم Retry-After، ومفتاح الثباتية، والحد الأقصى للمحاولات:
import random
import time
import uuid
import requests
RETRYABLE = {429, 502, 503, 504}
BASE = 0.5 # seconds
CAP = 30.0 # ceiling on any single delay
MAX_RETRIES = 5
def create_payment(payload):
idempotency_key = str(uuid.uuid4()) # one key per logical payment
headers = {"Idempotency-Key": idempotency_key}
for retry_count in range(MAX_RETRIES + 1):
try:
resp = requests.post(
"https://api.acmepay.com/v1/payments",
json=payload, headers=headers, timeout=10,
)
if resp.status_code < 400:
return resp.json()
if resp.status_code not in RETRYABLE:
resp.raise_for_status() # 400/401/403/422: fail fast
retry_after = resp.headers.get("Retry-After")
except (requests.ConnectionError, requests.Timeout):
retry_after = None # network fault: fall through to backoff
if retry_count == MAX_RETRIES:
raise RuntimeError("payment failed after all retries")
if retry_after and retry_after.isdigit():
delay = min(CAP, float(retry_after))
else:
delay = random.uniform(0, min(CAP, BASE * 2 ** retry_count))
time.sleep(delay)
لاحظ أن المفتاح يُنشأ مرة واحدة خارج الحلقة، وأن Retry-After يتقدم على التراجع المحسوب مع احترام الحد الأقصى. أما الحالات غير القابلة لإعادة المحاولة فتفشل فورًا. في JavaScript، تقدم مكتبة axios-retry النمط نفسه عبر خطافَي retryCondition وretryDelay.
كيفية اختبار سلوك إعادة المحاولة قبل الإنتاج
تشحن فرق كثيرة كود إعادة محاولة لم ينفذ فرع الفشل ولو مرة واحدة. اختبر المسار السعيد، ثم اختبر الحالات الفاشلة عمدًا باستخدام ميزتَي Apidog.
محاكاة الأعطال باستخدام الخوادم الوهمية: عرّف نقطة نهاية مثل /v1/payments وبرمج استجاباتها. اجعلها تعيد 503 لأول استدعاءين و200 في الثالث، أو 429 مع Retry-After: 5، أو أضف تأخيرًا قدره 15 ثانية لاختبار مهلة العميل. وجّه العميل إلى عنوان URL الوهمي واختبر الحلقة دون حادث إنتاج.
اختبار سلوك العميل: اربط الطلبات بتأكيدات وفحوصات زمنية. تحقق من أن الاستدعاء ينجح في النهاية، وأن الزمن الإجمالي يقع ضمن نطاق التراجع المتوقع، وأن موردًا واحدًا فقط أُنشئ، بما يثبت عمل مفتاح الثباتية. ثم شغّل السيناريو في CI مع كل التزام.
هذا هو الفرق بين «أضفنا عمليات إعادة محاولة» و«تحققنا من قدرة العميل على تجاوز تبعية محدودة المعدل أو متوقفة جزئيًا». نزّل Apidog مجانًا، وشغّل خادمًا وهميًا فاشلًا مقابل عميلك خلال نحو عشر دقائق.
الأسئلة الشائعة
هل يجب إعادة محاولة 429؟
نعم. اقرأ رأس Retry-After وانتظر المدة المحددة على الأقل، ثم استخدم التراجع الأسي مع التذبذب إذا كان الرأس مفقودًا. إذا تكررت 429، أصلح معدل الطلبات عبر التقييد من جانب العميل أو التخزين المؤقت بدل اعتبار إعادة المحاولة حلًا دائمًا.
ما التذبذب الكامل؟
يختار التذبذب الكامل كل تأخير عشوائيًا وبشكل متساوٍ بين الصفر والحد الأقصى الأسي:
random(0, min(cap, base * 2^n))
يمنع ذلك موجات إعادة المحاولة المتزامنة. وفي محاكاة AWS، تفوق على التراجع العادي والتذبذب المتساوي من حيث عدد الاستدعاءات والوقت اللازم للإكمال، ولذلك أصبح نمطًا شائعًا في حزم AWS SDK.
هل من الآمن إعادة محاولة طلبات POST؟
نعم فقط عندما تصبح العملية ثابتة عمليًا، مثل استخدام مفتاح ثباتية يزيل الخادم التكرار بناءً عليه. من دون المفتاح، قد تؤدي إعادة المحاولة بعد انتهاء المهلة إلى تكرار دفعة أو طلب أو سجل، لأن الخادم ربما عالج الطلب أصلًا. تنطبق أنماط استعادة الأخطاء لوكيل الذكاء الاصطناعي على هذه الحالة أيضًا: عمليات كتابة بمفاتيح، ومحاولات محدودة، وقاطع دائرة.
كم مرة يجب إعادة المحاولة؟
تعالج ثلاث إلى خمس محاولات معظم الإخفاقات العابرة. بعد ذلك تستقر معدلات النجاح، بينما يستمر الحمل والزمن في الارتفاع. اجمع الحد الأقصى لكل طلب مع ميزانية إعادة محاولة عالمية، مثل السماح بإضافة 10% فقط من حركة المرور. وإذا ظلت التبعية متوقفة بعد آخر محاولة، فهذه حالة لقاطع الدائرة، لا لحلقة إعادة محاولة أطول.
Top comments (0)