مستوى المقال: متوسط. الكلام ده موجّه لأي حد بيكتب كود بيتصل بخدمة تانية (API أو قاعدة بيانات) وحاطط فيه إعادة محاولة (retry) وعايزها تشتغل صح تحت الضغط.
عاصفة إعادة المحاولة: ليه الـ retry بيوقّع خدمتك وهي بتحاول تقوم
لو خدمتك وقعت لثانيتين ورجعت، وبعد ما رجعت وقعت تاني على طول، المشكلة غالبًا مش في الخدمة — المشكلة في طريقة إعادة المحاولة عندك. المقال ده هيوريك إزاي تكتب retry ميقعش السيرفر، بكود Python بيشتغل وأرقام.
المشكلة باختصار
عندك 1000 عميل (client) بيكلّموا خدمة واحدة. الخدمة اتعبت لثانيتين، فكل العملاء فشلوا في نفس اللحظة تقريبًا. لو كل واحد فيهم بيعيد المحاولة بعد فاصل ثابت — يعني كل ثانيتين — يبقى الـ 1000 عميل هيضربوا الخدمة كلهم مع بعض في نفس اللحظة، وكل ثانيتين تاني. ده اسمه عاصفة إعادة المحاولة (retry storm) أو الـ thundering herd.
النتيجة إن الخدمة كانت هتقوم، بس موجة الطلبات المتزامنة بتوقّعها تاني قبل ما تلتقط أنفاسها. فتدخل في حلقة: بتقع، بتقوم، الموجة بتضربها، بتقع. الأخطر إن ده بيحصل بالظبط في أسوأ وقت — وقت ما الخدمة أضعف ما تكون.
المفهوم بمثال بسيط الأول
تخيّل باب مطعم واحد وبرّه 200 زبون مستنيين. لو كلهم زقّوا الباب في نفس اللحظة، الباب هيتزنق ومحدش هيدخل. لكن لو كل واحد استنى مدة عشوائية مختلفة قبل ما يجرّب تاني، هيدخلوا بالتدريج والباب هيفضل شغّال. الفكرة كلها هنا: وزّع المحاولات على الزمن بدل ما تخليها متزامنة.
علميًا، الحل بيتكوّن من جزئين. الأول Exponential Backoff: كل محاولة فاشلة تضاعف مدة الانتظار (0.1، 0.2، 0.4، 0.8 ثانية...) عشان تخفّف الضغط تدريجيًا. الثاني Jitter: تضيف عشوائية على مدة الانتظار عشان تكسر التزامن بين العملاء. الاتنين مع بعض هما اللي بيحلّوا المشكلة، مش واحد لوحده.
ليه الـ Backoff لوحده مش كفاية
الطريقة الشائعة إنك تعمل exponential backoff من غير jitter. الطريقة دي بتفشل، لأن كل العملاء بدأوا الفشل في نفس اللحظة، فمضاعفة الانتظار بتحصل عندهم كلهم بنفس الإيقاع. يعني هيتزنقوا مع بعض عند 0.1s، وبعدين عند 0.2s، وبعدين 0.4s. خفّفت عدد الموجات، بس كل موجة لسه متزامنة.
الحل اللي بتوصّي بيه AWS اسمه Full Jitter: بدل ما تنام مدة ثابتة، تنام مدة عشوائية بين صفر والحد الأقصى المحسوب أسّيًا.
import random, time
def call_with_retry(do_request, max_attempts=6, base=0.1, cap=10.0):
for attempt in range(max_attempts):
try:
return do_request()
except TransientError: # timeout / 503 / 429 بس
if attempt == max_attempts - 1:
raise
# Full Jitter: نام مدة عشوائية بين 0 والحد الأقصى الأسّي
ceiling = min(cap, base * (2 ** attempt))
time.sleep(random.uniform(0, ceiling))السطر المهم هو random.uniform(0, ceiling). هو ده اللي بيكسر التزامن. من غيره، الـ retry بيتحوّل لسلاح ضد خدمتك بدل ما يحميها.
الأرقام: قبل وبعد
الافتراض: 1000 عميل، والخدمة تتحمّل 300 طلب/ثانية بأمان، وكلهم فشلوا في نفس اللحظة.
- retry بفاصل ثابت: موجات من حوالي 1000 طلب/ثانية كل ثانيتين — يعني أضعاف السعة، فالخدمة تفضل واقعة.
- Full Jitter: نفس الـ 1000 محاولة بس موزّعة على النافذة الزمنية، فالذروة بتنزل تحت الـ 300 والخدمة بتلحق تتعافى.
الأرقام دي تقديرية للتوضيح، بس السلوك ده بالظبط هو اللي وثّقته AWS في تجاربها: الـ Full Jitter بيقلّل التنافس بين العملاء وبيقصّر إجمالي الوقت اللازم لإنهاء الشغل مقارنةً بالـ backoff من غير عشوائية.
الـ trade-offs وحاجات تنتبه لها
- بتكسب: استقرار وقت التعافي، وتقليل احتمال الانهيار المتسلسل. بتخسر: زمن استجابة أطول ومتغيّر للعميل الواحد، لأنه ممكن ينام لحد الـ
cap(10 ثواني في المثال). - لازم تحط سقف (
cap) للانتظار، وإلا الانتظار الأسّي هيوصل لأرقام سخيفة (2^15 ثانية بعد 15 محاولة). - أعِد المحاولة على الأخطاء المؤقتة بس (timeouts، 503، 429). متعيدش على 400 أو 404 — دول مش هيتصلحوا بالإعادة، هتضيّع وقت ومحاولات.
- احترم هيدر
Retry-Afterلو الخدمة بعتته؛ هو أدق من أي تخمين من عندك. - ضيف retry budget: سقف لنسبة الطلبات المسموح تكون إعادة (مثلًا 10% من إجمالي الترافيك). ده بيمنع الإعادة إنها تتحوّل لهجوم DDoS على نفسك وقت الأعطال الكبيرة.
متى لا تستخدم هذه الطريقة
لو العملية مش idempotent (زي «اخصم فلوس») متعيدهاش من غير مفتاح idempotency، وإلا هتخصم مرتين. ولو عندك عميل واحد بيكلّم خدمة واحدة (مش آلاف)، الـ jitter مش هيفرق كتير والـ backoff البسيط كفاية. وعمومًا، إعادة المحاولة مش بديل عن إصلاح السبب لو الخدمة بتقع باستمرار — هي بتشتري وقت، مش بتصلّح عطل دائم.
الخطوة التالية
افتح أقرب مكان في كودك بتعمل فيه retry دلوقتي. لو لقيت time.sleep(2) ثابتة، أو backoff من غير عشوائية، غيّرها لـ Full Jitter زي المثال فوق، وحط cap وmax_attempts. بعدها جرّب توقّع الخدمة تحت حِمل وشوف منحنى الطلبات اتفرد على الزمن ولا لسه متزامن.
المصادر
- AWS Architecture Blog — Marc Brooker، «Exponential Backoff And Jitter»: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
- Amazon Builders' Library — «Timeouts, retries, and backoff with jitter»: https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/
- Google SRE Book — «Addressing Cascading Failures» (backoff عشوائي وretry budgets): https://sre.google/sre-book/addressing-cascading-failures/
- Google SRE Book — «Handling Overload»: https://sre.google/sre-book/handling-overload/