المرونة والمراسلة والرصد

لماذا تهمّنا المرونة؟

18 دقيقة الدرس 1 من 12

لماذا تهمّنا المرونة؟

يعيش التطبيق ذو العملية الواحدة ويموت كوحدة واحدة. حين يتعطّل، يتعطّل وحده. تحطّم الخدمات المصغّرة (Microservices) هذا النموذج البسيط: خدمة الطلبات لديك تستدعي خدمة المخزون، التي تستدعي بدورها واجهة API لمستودع، التي تستدعي مزوّد شحن خارجيًا. كل قفزة عبر الشبكة هي فرصة لأن يسوء شيء ما — وفي الإنتاج، شيء ما يسوء دائمًا في نهاية المطاف.

هذا الدرس يتناول ما يحدث حين يقع ذلك، ولماذا يُعدّ التصميم للفشل أمرًا لا خيار فيه في النظام الموزَّع.

واقع الأنظمة الموزّعة

لا تزال مغالطات الحوسبة الموزّعة التي وضعها بيتر دويتش عام 1994 وثيقة الصلة تمامًا. أولى هذه المغالطات:

  1. الشبكة موثوقة.
  2. زمن الاستجابة صفر.

كلتاهما خطأ. تُسقَط الحزم، وتنتهي مهل TCP، ويُحلّل DNS عنوانًا ميتًا، وتُعاد تشغيل موازنات الحمل، وتفشل بطاقات الشبكة. الخدمة التي تتجاهل هذه الحقائق ستفشل بطرق غير قابلة للتنبؤ في أسوأ وقت ممكن — عادةً حين تكون حركة المرور مرتفعة والمهندس المناوب نائمًا.

المرونة لا تعني منع الأعطال — بل تعني تحديد نطاق تأثيرها. لا يمكنك منع كل تبعية خارجية من المرور بيوم سيء. غير أنك تستطيع تصميم خدمتك بحيث تتراجع بأدب (تُخفَّض خدمتها) حين تتدهور تبعية ما، بدلًا من أن تنهار كليًا.

الأعطال المتتالية: كيف تقتل خدمة واحدة بطيئة كل شيء

أخطر أنماط الفشل في الخدمات المصغّرة ليست الانهيار الكامل — بل التبعية البطيئة. تأمّل التسلسل التالي:

  1. تبدأ ShippingService بالاستجابة في 30 ثانية بدلًا من 50 مللي ثانية.
  2. لدى OrderService عميل HTTP افتراضي بدون مهلة قراءة. كل طلب جارٍ يُقيّد خيطًا (thread) لمدة 30 ثانية.
  3. تجمّع خيوطك (مثلًا 200 خيط Tomcat) يمتلئ في ثوانٍ. الطلبات الجديدة تُوضع في قائمة انتظار ثم تُرفض.
  4. يرى ApiGateway الموجود أعلى السلسلة أن OrderService يستغرق وقتًا طويلًا. تبدأ خيوطه هي الأخرى في الانشغال.
  5. في غضون دقيقة، أصبح كامل المنصة غير متاح — بسبب نقطة نهاية خارجية واحدة بطيئة.

هذا ما يُسمّى العطل المتتالي (Cascading Failure)، ويُعرف أيضًا بـتضخيم الأعطال. أدّت التبعية البطيئة دور مُستنزِف للموارد، وعدم وجود حدود حماية سمح للضرر بالانتشار عبر حدود الخدمات.

استنفاد الخيوط هو الآلية الأكثر شيوعًا للتضخيم. يستخدم Spring Boot المدمَج مع Tomcat تجمّع خيوط محدودًا. إذا كان كل خيط عالقًا ينتظر استدعاءً بطيئًا لخدمة أدنى، تصبح الخدمة غير مستجيبة تمامًا لجميع الطلبات — بما فيها فحوصات الصحة (health checks) ونقاط نهاية الإدارة — حتى لو كان كودك الخاص لا غبار عليه.

مثال عملي على Spring Boot

إليك أبسط صورة ممكنة للمشكلة. لنفترض أن OrderController يستدعي InventoryClient:

@RestController @RequiredArgsConstructor public class OrderController { private final InventoryClient inventoryClient; @GetMapping("/orders/{id}") public ResponseEntity<OrderDto> getOrder(@PathVariable Long id) { // إذا أقفلت inventoryClient.check() لمدة 30 ثانية، // سيُحتجز هذا الخيط 30 ثانية. boolean inStock = inventoryClient.check(id); return ResponseEntity.ok(buildDto(id, inStock)); } }

أما InventoryClient فهو مجرد استدعاء RestTemplate أو WebClient دون تهيئة أي مهلة:

@Component public class InventoryClient { private final RestTemplate restTemplate; // RestTemplate بدون مهلة — تبعية بطيئة ستُقيّد // الخيط المُستدعي إلى أجل غير مسمى. public InventoryClient() { this.restTemplate = new RestTemplate(); } public boolean check(Long productId) { String url = "http://inventory-service/api/stock/" + productId; return Boolean.TRUE.equals( restTemplate.getForObject(url, Boolean.class) ); } }

هذا الكود يُصرَّف ويجتاز اختبارات الوحدة ويعمل بشكل مثالي في بيئة التطوير حيث تستجيب inventory-service دائمًا في أجزاء من المللي ثانية. لكنه كارثة كامنة في الإنتاج.

الفشل الأنيق: الهدف المنشود

الفشل الأنيق يعني إعادة استجابة مفيدة ومُخفَّضة بدلًا من الانتظار إلى ما لا نهاية أو نشر الاستثناء للمستدعي. بحسب السياق، قد يبدو الاحتياط الأنيق هكذا:

  • إرجاع DTO للطلب يحتوي على stockStatus: "UNKNOWN" وإظهار رسالة في واجهة المستخدم تقول "التوفر غير متاح حاليًا — حاول مرة أخرى قريبًا."
  • تقديم قيمة مخزون مُخزَّنة مؤقتًا لا يتجاوز عمرها 5 دقائق.
  • إرجاع HTTP 503 مع رأسية Retry-After حتى يعلم العميل أن يؤجّل طلبه.
  • وضع الطلب في قائمة انتظار للمعالجة غير المتزامنة بدلًا من الإجابة الفورية.

لا يعني أيٌّ من هذه الخيارات "التظاهر بأن المشكلة غير موجودة." جميعها تستلزم قرارات تصميمية واعية: ما هي الحالة المُخفَّضة المقبولة لهذه العملية بالذات؟

حدّد التدهور المقبول لكل عملية، لا لكل خدمة. إعادة نتائج بحث عن منتج قد تكون قديمة قليلًا مختلفة تمامًا عن إرجاع تأكيد دفع بنتائج قديمة. بعض العمليات يتحمّل احتياطًا مؤقتًا من التخزين المؤقت؛ وبعضها يجب أن يفشل بوضوح وسرعة. قرّر أي الفئتين تنتمي إليها كل نقطة نهاية قبل كتابة كود المرونة.

الأسباب الأربعة الجذرية للأعطال المتتالية

فهم سبب حدوث هذه الأعطال يساعدك على معرفة الإجراء الوقائي الصحيح:

  • غياب المهل الزمنية: لا حدّ أعلى لمدة انتظار استدعاء شبكي لخيط. الحل: دائمًا هيّئ مهل الاتصال والقراءة على كل عميل HTTP (يُغطَّى في الدرس 3).
  • تجمّعات خيوط/اتصالات غير محدودة: تجمّع موارد يستمر في النمو تحت الحمل حتى تنفد ذاكرة JVM أو واصفات الملفات. الحل: تجمّعات محدودة بسياسة رفض واضحة.
  • غياب الضغط الخلفي (Backpressure): المستدعيون أعلى السلسلة يواصلون إرسال الطلبات حتى وإن كانت التبعية أدناه مثقلة بالفعل. الحل: قواطع الدائرة (Circuit Breakers) وتحديد المعدل (Lessons 2 و4).
  • الارتباط الوثيق في التوفر: خدمة لا تستطيع الاستجابة إطلاقًا دون أن تكون التبعية أدناه بصحة كاملة. الحل: الحواجز (Bulkheads) في الدرس 3 والرسائل غير المتزامنة في الدرسين 5 و6.

الأبعاد الأمنية لضعف المرونة

كثيرًا ما تُعامَل المرونة باعتبارها مصدر قلق تتعلق بالموثوقية فحسب، لكنها تنعكس مباشرةً على الأمان أيضًا:

  • تضخيم الحرمان من الخدمة: يستطيع مهاجم يجعل تبعية واحدة بطيئة (أو يُجبرها على إرجاع أخطاء) إسقاط كامل منصتك في غياب الحماية من الأعطال المتتالية. وهذا الهجوم سهل التنفيذ بشكل خاص على الواجهات البرمجية العامة.
  • الفشل المفتوح مقابل الفشل المغلق: حين تكون خدمة المصادقة أو التفويض غير متاحة، ماذا تفعل خدمتك؟ إرجاع HTTP 200 لأنك "لا تستطيع التحقق" خطأ كارثي. الإعداد الآمن الافتراضي للعمليات الحساسة أمنيًا هو الرفض لا السماح (fail-closed). يجب أن يُضمَّن هذا التمييز في منطق الاحتياط (fallback) الخاص بقاطع الدائرة.
  • تسريب المعلومات في مسارات الخطأ: خدمة غير مرنة تنشر الاستثناءات الخام للمستدعين قد تكشف تتبّعات المكدس (stack traces) وأسماء المضيفين الداخلية أو رسائل أخطاء قاعدة البيانات. تتيح لك الاحتياطات المتحكَّم بها والمتعمَّدة إرجاع أشكال أخطاء نظيفة وعامة.
لا تفشل بشكل مفتوح أبدًا في فحوصات الأمان. إذا انتهت مهلة الاستدعاء إلى نقطة نهاية استبيان الرمز أو خدمة الصلاحيات لديك، فأرجع HTTP 503 (أو 401) — لا HTTP 200. ضع هذه القاعدة في منطق الاحتياط قبل النشر.

ما يوفّره نظام بيئة Spring Cloud

نادرًا ما ستبني مبادئ المرونة من الصفر. يتكامل Spring Cloud مع Resilience4j — مكتبة تحمّل الأعطال خفيفة الوزن وظيفية المنحى لجافا 8 وما فوق. تتوافق وحداتها الأساسية مباشرةً مع أنماط الفشل السابقة:

  • CircuitBreaker — يوقف استدعاء تبعية فاشلة وينفتح بعد عتبة محددة (الدرس 2).
  • Retry — يُعيد محاولة الاستدعاء الفاشل بتأخير قابل للتهيئة (الدرس 3).
  • TimeLimiter — يُطبّق مهلة صارمة على أي استدعاء (الدرس 3).
  • Bulkhead — يُحدّد عدد الاستدعاءات المتزامنة لتبعية ما (الدرس 3).
  • RateLimiter — يُطبّق حدود معدّل الاستدعاء (الدرس 4).

في ملف pom.xml تسحب جميع هذه القدرات عبر بادئ Spring Cloud واحد:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId> </dependency>

يوجد الإعداد في application.yml، وتُطبَّق المكوّنات كتعليقات توضيحية (annotations) أو مُغلَّفات وظيفية (functional wrappers). ستطّلع على كلا الشكلين بالتفصيل في هذا البرنامج التعليمي.

الخلاصة

الأنظمة الموزّعة تفشل. تبعية بطيئة أو غير متاحة قادرة على استنفاد تجمّع الخيوط والتسلسل في عطل شامل للمنصة في ثوانٍ إذا لم توجد حدود حماية. الفشل الأنيق يعني إرجاع استجابة مُخفَّضة بتعمّد بدلًا من الانتظار إلى ما لا نهاية أو نشر الأخطاء دون رقيب. للأسباب الأربعة الجذرية — غياب المهل الزمنية، وتجمّعات الموارد غير المحدودة، وغياب الضغط الخلفي، والارتباط الوثيق في التوفر — حلول مجرَّبة ومعروفة، وتوفّر لك منظومة Spring Cloud وتكاملها مع Resilience4j تطبيقات جاهزة للإنتاج لجميعها. يستعرض بقية هذا البرنامج التعليمي كل حل منها بعمق وتفصيل.