إدارة الأخطاء والمهلات وإعادة المحاولة في RPC بشكل احترافي
تصميم RPC resilient ليس مجرد إرسال واستقبال طلبات بين الخدمات؛ بل هو فن إدارة الفشل: كيف تتعامل مع الأخطاء؟ متى تنهي الطلب بسبب timeout؟ كيف تعيد المحاولة بشكل آمن؟ ومتى تلجأ إلى fallback بدل الاستمرار في الضغط على نظام متعطل؟
في هذا الدليل سنتعمق في RPC error handling في الأنظمة الموزعة، وكيفية التعامل مع timeouts وretries وfallbacks بشكل احترافي، مع ربط المفاهيم بالتصميم المعماري الجيد والـ patterns الشائعة.
لماذا إدارة الأخطاء في RPC موضوع حساس في الأنظمة الموزعة؟
في استدعاء دالة محلية (in-process function call) الفشل واضح: إما Exception أو كود خطأ. لكن في Remote Procedure Call – RPC هناك طبقة شبكة، بروتوكول، وخدمة أخرى قد تكون:
- بطيئة جداً (High Latency)
- متوقفة مؤقتاً (Partial Outage)
- تستجيب بأخطاء منطقية (Business Errors)
- أو حتى تستجيب بشكل غير متوقع أو غير متوافق مع النسخة الحالية
النتيجة: إذا لم تصمم RPC error handling بعناية، يمكن أن يتحول فشل خدمة واحدة إلى سلسلة انهيارات (Cascading Failures) عبر النظام كله.
لإطار أوسع حول RPC نفسها وأنواعها يمكنك الرجوع إلى: ما هو RPC؟ شرح مبسط للتواصل بين الخدمات عن بُعد و أنواع RPC وأهميتها في بناء الأنظمة الحديثة.
أنواع الأخطاء في استدعاءات RPC
قبل أن تصمم آلية التعامل مع الخطأ، تحتاج أولاً لتصنيف نوعه:
1. أخطاء الشبكة (Network Errors)
- عدم القدرة على الوصول للسيرفر (Connection Refused / DNS Failure)
- انقطاع الاتصال أثناء الطلب أو الاستجابة
- حزم ضائعة، أو وقت استجابة مرتفع جداً
هذه الأخطاء غالباً قابلة لإعادة المحاولة (Retryable) إذا صممت آليتك بحذر.
2. أخطاء الـ Timeout
- العميل ينتظر استجابة لمدة محددة ثم يقرر إنهاء الطلب.
- السبب قد يكون بطء في السيرفر، ازدحام، أو مشكلة في الشبكة.
هي أيضاً غالباً قابلة لإعادة المحاولة لكن مع حذر مضاعف، لأن إعادة المحاولة على خدمة بطيئة قد تزيد الطين بلة.
3. الأخطاء المنطقية (Business / Application Errors)
- رصيد غير كافٍ
- Token انتهت صلاحيته
- المورد المطلوب غير موجود
هذه الأخطاء ليست مشكلة شبكة؛ إعادة المحاولة لن تحلها غالباً. هنا لا معنى لـ retry إلا إذا تغيرت المعطيات.
4. الأخطاء غير المتوقعة / Bugs
- Null Pointer Exception
- Serialization/Deserialization Error
- Version Mismatch بين العميل والسيرفر
هنا يجب تسجيل المشكلة بدقة، وإرسال استجابة واضحة للعميل، وربما تعطيل بعض المسارات مؤقتاً أو استخدام fallback إذا كان متاحاً.
تصميم استجابات الأخطاء في RPC بشكل قياسي
من أكبر أسباب الفوضى عدم وجود بروتوكول أخطاء موحد. في واجهات RPC الجيدة يتم تعريف:
- حقل لحالة النجاح/الفشل (success / error)
- كود خطأ موحّد (Error Code)
- رسالة بشرية (Message) لأغراض الـ debugging
- تفاصيل إضافية (Metadata) – اختيارية
مثال مبسط (JSON) لاستجابة RPC فاشلة:
{
"success": false,
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "User has not enough balance to complete the operation",
"details": {
"current_balance": 20,
"required_balance": 50
},
"retryable": false
}
}
وجود حقل مثل retryable يساعد العميل على اتخاذ قرار ما إذا كان يجب أن يحاول مرة أخرى أم لا.
الـ Timeouts: الخط الدفاعي الأول ضد التعليق (Hanging)
في RPC error handling، إدارة الـ timeouts هي أساس الحماية من الطلبات العالقة التي تستهلك موارد بدون فائدة.
مستويات timeout في استدعاءات RPC
- Timeout على مستوى العميل (Client-side Timeout)
يحدد أقصى وقت ينتظره العميل لاستجابة RPC قبل أن يعلن فشل الطلب. - Timeout على مستوى السيرفر (Server-side Timeout)
يحدد أقصى وقت مسموح لتنفيذ العملية داخل السيرفر نفسه. - Timeout على مستوى الشبكة / الـ Proxy / الـ Gateway
مثل Nginx, API Gateway, Load Balancer... إلخ.
التصميم الاحترافي يتطلب ضبط هذه الـ timeouts بشكل متناغم:
- Timeout العميل يجب أن يكون أقل قليلاً من Timeout الـ Gateway
- Timeout الـ Gateway يجب أن يكون أكبر من Timeout السيرفر الداخلي إذا كان هناك أكثر من طبقة
نصائح عملية لضبط timeouts
- تجنب القيم الافتراضية (Default) العشوائية من المكتبة/الـ framework.
- اضبط timeout بناءً على:
- SLAs / SLOs المتفق عليها
- التجارب والأرقام الفعلية للـ latency
- أهمية العملية: عمليات مالية حساسة vs طلبات إحصائية ثانوية
- استخدم Timeout لكل عملية (Per-Operation Timeout) وليس Timeout عام لكل النظام.
إعادة المحاولة (Retries): سلاح ذو حدين
الـ Retry يعتبر Pattern أساسي في الأنظمة الموزعة، وقد تناولناه معمقاً في: Retry Pattern في الأنظمة الموزعة: كيف تعالج فشل الطلبات بدون انهيار النظام. هنا سنركز على زاوية RPC error handling.
متى يكون retry فكرة جيدة؟
- عند حدوث أخطاء شبكة عابرة (Transient Network Issues)
- عند حصول Timeout لكن العملية غالباً خفيفة وغير مكلفة
- عند أخطاء 5xx من السيرفر والتي تشير لأعطال مؤقتة
متى يكون retry خطيراً؟
- في العمليات غير idempotent مثل:
- خصم رصيد
- إرسال تحويل بنكي
- إنشاء طلب (Order) جديد
- عندما يكون الفشل سببه خطأ منطقي (مثل Validation Error)
- عندما تكون الخدمة تحت ضغط مرتفع مسبقاً – retries قد تسبب انهياراً كاملاً
تصميم آلية Retry صحية
لبناء Retry محترف في RPC تحتاج للعناصر التالية:
- سياسة Retry واضحة (Retry Policy)
- عدد المحاولات القصوى (Max Retries)
- زمن الانتظار بين المحاولات (Backoff)
- أي أنواع الأخطاء تستحق retry وأيها لا
- Exponential Backoff
- زيادة زمن الانتظار بين المحاولات تدريجياً: 100ms، 200ms، 400ms، 800ms...
- مع إضافة Jitter (عشوائية بسيطة) لتجنب تزامن المحاولات من آلاف العملاء في نفس اللحظة.
- احترام الوقت الكلي للعملية (Overall Deadline)
حتى لو استخدمت Retry، يجب ألا تتجاوز مدة العملية ككل Deadline محدد من العميل (مثلاً 2 ثانية).
- سجل سبب آخر فشل (Last Error)
كي تمكن العميل من فهم لماذا فشل الطلب في النهاية بعد عدة محاولات.
Idempotency: حجر الأساس لإعادة المحاولة بأمان
من أهم مبادئ RPC error handling للعمليات الحرجة أن تكون Idempotent: استدعاء العملية مرة أو عشر مرات بنفس المعطيات يعطي نفس النتيجة الفعلية على النظام.
طرق عملية لتحقيق ذلك:
- استخدام Request ID فريد يتم تمريره في كل استدعاء RPC
- سيرفر RPC يتحقق:
- إن تم تنفيذ نفس Request ID مسبقاً – يعيد نفس النتيجة المخزنة بدون تنفيذ جديد
- إن لم ينفذ – ينفذ العملية مرة واحدة ويسجّل النتيجة
- تسجيل العمليات في جدول عمليات (Operation Log) يمكن التحقق منه
هذا التصميم يجعل الـ retries آمنة حتى للعمليات المالية، ويقلل جداً من مشاكل التكرار (Duplicate Processing).
Fallbacks: ماذا تفعل عندما لا يفيد retry والـ timeout؟
أحياناً تكون الخدمة الحرجة معطلة بوضوح، ولا جدوى من المزيد من المحاولات. هنا يأتي دور Fallback: خطة بديلة عندما تفشل القناة الأساسية.
أنواع Fallbacks في RPC
- Cache Fallback: إرجاع بيانات قديمة نسبياً من Cache عندما لا يمكن الوصول للخدمة.
- Default Response: رد افتراضي محدود الوظائف (مثل إيقاف بعض المزايا مؤقتاً).
- Degraded Mode: تشغيل النظام في وضع مختزل (مثلاً، إظهار الأسعار بدون خصومات لأن خدمة التسعير معطلة).
- Redirect to Another Service: توجيه الطلب لخدمة بديلة أو Replica في منطقة أخرى.
مهم جداً أن يكون Fallback واضحاً وموثقاً؛ لأن الاعتماد على بيانات Cache قديمة أو ردود افتراضية قد يؤثر على القرارات التجارية.
Circuit Breaker + Retry + Timeout: ثلاثي الحماية في RPC
التصميم الاحترافي يتعامل مع RPC error handling كجزء من مجموعة Patterns متكاملة:
- Timeout: منع الطلبات من التعليق لفترة طويلة.
- Retry: إعطاء فرصة إضافية للأخطاء العابرة.
- Circuit Breaker: إيقاف إرسال المزيد من الطلبات لخدمة فاشلة بشكل واضح لفترة محددة.
Circuit Breaker يحمي الخدمات السليمة من الانهيار بسبب الإصرار على الاتصال بخدمة متعطلة. بمجرد فتح الدائرة (Open State) يتم:
- رفض الطلبات المحلية فوراً مع استجابة سريعة (Fast Fail)
- أو محاولة Fallback
ثم بعد مدة (Half-Open) يتم تجربة بعض الطلبات لمعرفة ما إذا تعافت الخدمة أم لا.
مستوى البروتوكول مقابل مستوى التطبيق في إدارة الأخطاء
في RPC يوجد طبقتان أساسيتان للتعامل مع الأخطاء:
- أخطاء على مستوى البروتوكول (Transport/Protocol Level)
- Connection Timeout
- SSL Handshake Failed
- HTTP 5xx generic errors
- أخطاء على مستوى التطبيق (Application Level)
- Validation Errors
- Business Rules Violations
- Resource Not Found
من المهم عدم خلط المستويين:
- لا تستخدم 200 OK مع error داخل الـ body لأخطاء بروتوكولية خطيرة.
- ولا تستخدم 500 لكل خطأ منطق؛ ميز بين 400-422 لأخطاء العميل، و500-5xx لأخطاء السيرفر.
حتى لو كان RPC لا يستخدم HTTP مباشرة (مثل gRPC)، فغالباً ما يوجد ما يعادل status codes يجب استغلاله بوضوح.
المراقبة (Observability) جزء أساسي من RPC error handling
لا يمكن تحسين ما لا تراقبه. إدارة الأخطاء والمهلات وإعادة المحاولة في RPC تحتاج:
- Logging منظم لكل:
- Timeouts
- Retries (مع عدّاد المحاولات)
- Fallback Activations
- Circuit Breaker State Changes
- Metrics مثل:
- نسبة النجاح/الفشل لكل Endpoint
- متوسط زمن الاستجابة (Latency)
- عدد الـ Timeouts لكل دقيقة
- Tracing عبر Distributed Tracing لتتبع رحلة طلب RPC بين الخدمات المختلفة.
من دون هذه البيانات لن تعرف:
- هل Timeouts الحالية مناسبة؟
- هل Retry يزيد فعلاً من موثوقية الخدمة أم يسبب ضغطاً إضافياً؟
- أين عنق الزجاجة (Bottleneck) الحقيقي؟
أمثلة تصميمية عملية
مثال 1: خدمة دفع (Payment Service) تستدعي خدمة Fraud Check عبر RPC
- Timeout: 500ms لاستجابة Fraud Check.
- Retry Policy: محاولتان إضافيتان بــ Exponential Backoff (100ms، 200ms).
- Idempotency:
- Request ID موحد من Payment Service إلى Fraud Service.
- تخزين النتائج لوقت قصير في Cache.
- Fallback:
- إذا فشل Fraud Check بالكامل:
- إما رفض العملية احترازياً
- أو قبولها مع وضعها في قائمة مراجعة يدوية – حسب سياسة العمل
- Circuit Breaker:
- إذا تجاوزت نسبة فشل Fraud Check 50% خلال آخر N طلب، يتم فتح الدائرة.
- خلال فترة الفتح، يتم تفعيل Fallback بشكل مباشر بدون إرسال RPC.
مثال 2: خدمة إحصائية ثانوية (Analytics) عبر RPC
- الاستدعاء ليس حرجاً لرحلة المستخدم.
- في حالة الفشل:
- لا تقوم بـ Retry أثناء مسار الطلب الأساسي.
- يمكن جدولة Retry لاحقاً عبر Queue.
- Timeout قصير جداً (مثلاً 100ms)، وإذا فشل لا يؤثر على منطق العمل الأساسي.
ربط RPC error handling مع أنماط أخرى في الأنظمة الموزعة
إدارة الأخطاء والمهلات وإعادة المحاولة في RPC لا تعيش في فراغ؛ بل تتداخل مع:
فهم هذه الصورة الكبيرة يساعدك على اتخاذ قرارات أفضل: متى تستخدم RPC متزامن، ومتى تنتقل إلى Messaging، ومتى تعتمد على Caching أو Fallbacks.
خلاصة: مبادئ ذهبية لتصميم RPC resilient
- صنّف الأخطاء بوضوح بين شبكة، Timeout، منطق، وبروتوكول.
- استخدم Timeouts مدروسة على كل مستوى، ولا تعتمد على الافتراضيات.
- طبّق Retry بسياسة واعية (Exponential Backoff + Jitter + Max Retries).
- اجعل العمليات الحرجة Idempotent قدر الإمكان.
- صمم Fallbacks واضحة لمسارات الفشل المتوقعة.
- ادمج Circuit Breaker مع Timeout وRetry لمنع الانهيارات المتسلسلة.
- احرص على Observability قوية بــ Logging وMetrics وTracing.
بتطبيق هذه المبادئ في RPC error handling ستنتقل من نظام يتعطل مع أول مشكلة شبكة، إلى نظام resilient يتحمل الفشل، ويتعافى منه بشكل منظم وقابل للتنبؤ، وهذا هو الفارق الحقيقي بين نظام بسيط ونظام إنتاجي جاهز للعالم الحقيقي.