مستوى المقال: متوسط. الكلام ده موجّه لأي حد بيكتب كود بيتصل بخدمة تانية (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))