هندسة الفوضى والمرونة

تجارب الفوضى: طبقة التطبيق

18 دقيقة الدرس 5 من 27

تجارب الفوضى: طبقة التطبيق

تختبر تجارب الفوضى على مستوى البنية التحتية (إيقاف العُقد، استنزاف الحاويات) مرونة المنصة. أما تجارب الفوضى على مستوى التطبيق فتختبر ما إذا كانت شيفرة الخدمة قادرة على التعامل مع الواقع الفوضوي للأنظمة الموزعة: التبعيات البطيئة، والإخفاقات الجزئية، واستجابات الخطأ غير المتوقعة. هذه هي الأعطال التي تُوقظك فعلاً في الثالثة صباحاً — ليس خادماً متوقفاً تماماً، بل بوابة دفع تُعيد استجابة 503 في 60% من الطلبات بينما تنجح الـ40% الأخرى.

يتناول هذا الدرس ثلاث فئات من الأعطال على مستوى التطبيق: حقن زمن الاستجابة، وإخفاقات التبعية، وحقن الأخطاء. تستهدف كل فئة نمط إخفاق مختلف، وتتطلب منهجاً تجريبياً مختلفاً.

1. حقن زمن الاستجابة

التأخير هو القاتل الصامت في الأنظمة الموزعة. الخدمة المُجاورة التي تستغرق 3 ثوانٍ بدلاً من 30 ميلي ثانية لا تُعيد خطأً — تنتظرها شيفرتك بسعادة، محتجزةً goroutine واتصالاً بقاعدة البيانات وفتحة في thread pool. تحت الحمل العالي، يتفاقم هذا ليصبح تراكماً في قوائم الانتظار ثم انهياراً متسلسلاً. هذا هو نمط "الإخفاق الرمادي": كل شيء يبدو مُشتغلاً، لكن النظام ينزف.

الهدف من حقن زمن الاستجابة هو التحقق من أن كل طلب شبكي في خدمتك يمتلك مهلة زمنية، وأن هذه المهلة أقصر من SLO الخاص بك، وأن التبعية البطيئة لا تستنزف مواردك.

فخ زمن الاستجابة في الذيل: قد يكون زمن الاستجابة عند p99 من خدمة مجاورة أضعافاً مضاعفة مقارنةً بـp50. إذا ضبطت مهلتك عند "المتوسط + هامش أمان"، فستنتهي مهلة 1% من الطلبات — لكن تحت الحمل، 1% من ملايين الطلبات في الثانية يعني حريقاً.

حقن التأخير مع Toxiproxy: Toxiproxy وكيل TCP قابل للبرمجة مُصمم خصيصاً للفوضى. تضعه بين خدمتك وتبعياتها، ثم تتحكم في الأعطال عبر HTTP API دون لمس أي من الخدمتين.

# تشغيل Toxiproxy (Docker، للبيئة المحلية أو التجريبية) docker run --rm -d \ --name toxiproxy \ -p 8474:8474 \ -p 5432:5432 \ ghcr.io/shopify/toxiproxy # إنشاء وكيل: المنفذ المحلي 5432 → Postgres الحقيقي على db:5432 toxiproxy-cli create postgres \ --listen 0.0.0.0:5432 \ --upstream db:5432 # حقن 800ms تأخير على الكتابة (مع تذبذب ±200ms) toxiproxy-cli toxic add postgres \ --type latency \ --attribute latency=800 \ --attribute jitter=200 \ --toxicName slow_db # شغّل اختبار الحمل والـtoxic نشط، ثم أزله toxiproxy-cli toxic remove postgres --toxicName slow_db

في Kubernetes مع Istio: يمكن لـVirtualService الذي يحتوي على مواصفة fault.delay حقن التأخير عبر طبقة الشبكة دون لمس أي شيفرة تطبيق.

apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: payments-chaos spec: hosts: - payments-svc http: - fault: delay: percentage: value: 10 # 10% من الطلبات fixedDelay: 2s # إضافة تأخير 2 ثانية route: - destination: host: payments-svc

طبّق هذا أثناء اختبار الحمل وراقب: هل تحترم خدمتك مهلتها الخاصة؟ هل تتخلى عن الطلبات بأناقة، أم تقوم بتكديسها حتى ينفد الذاكرة؟

2. إخفاقات التبعية

لكل خدمة تبعيات: قواعد بيانات، ذاكرات تخزين مؤقت، طوابير رسائل، واجهات برمجية خارجية. تُجيب تجارب إخفاق التبعية على السؤال: "ماذا تُعيد خدمتنا حين لا تتوفر X تماماً؟"

الجواب المتوقع للتبعيات غير الحرجة: تتدهور الخدمة بأناقة، مُعيدةً استجابة جزئية أو بيانات مُخزنة مؤقتاً، مع التسجيل والتنبيه. الجواب المتوقع للتبعية الحرجة: تُعيد الخدمة خطأً واضحاً بسرعة (fast-fail)، لا بعد سلسلة مهلات تمتد 30 ثانية.

Dependency failure blast radius: with vs without circuit breaker Without Circuit Breaker With Circuit Breaker Client API Service Database FAILED 30s timeout → threads exhausted → cascade Client API Service Circuit Breaker Database FAILED OPEN Instant fallback → service stays healthy
بدون قاطع الدائرة، تُجمّد تبعية فاشلة الخيوط حتى تنهار الخدمة كلياً. مع القاطع، تفشل الطلبات فوراً وتظل الخدمة سليمة.

محاكاة انقطاع التبعية مع Istio (إلغاء HTTP):

apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: inventory-chaos spec: hosts: - inventory-svc http: - fault: abort: percentage: value: 100 # انقطاع كامل httpStatus: 503 route: - destination: host: inventory-svc

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

اختبر منطق الـfallback، ليس فقط معالجة الأخطاء. فرق كثير لديها قاطع دائرة مُضبوط لكن لا يوجد fallback خلفه. عندما يفتح القاطع، يجب على الخدمة إما إعادة بيانات قديمة مُخزنة، أو قيمة افتراضية منطقية، أو خطأ واضح — لا استثناء null pointer من ذاكرة تخزين غير مُهيأة.

3. حقن الأخطاء

يتجاوز حقن الأخطاء مفهوم "الخدمة متوقفة" ليختبر كيف تتعامل شيفرتك مع استجابات سيئة محددة: 500 Internal Server Error، و429 Too Many Requests، وJSON مشوهاً، وإخفاقات المصادقة، والاستجابات المبتورة. تتضمن حوادث الإنتاج الحقيقية غالباً تبعية تقنياً تعمل لكنها تستجيب بشكل خاطئ — شهادة TLS منتهية الصلاحية، أو ترحيل مخطط كسر صيغة الاستجابة، أو حد معدل مُفعّل بسبب جار ضوضاء.

بالنسبة لخدمات HTTP، يغطي fault.abort في Istio رموز الحالة. لحالات أكثر دقة — ترويسات content-type خاطئة، حمولات جزئية، استجابات ضخمة — استخدم وكيل أعطال مُخصص أو امتداد service mesh.

Chaos Monkey لـSpring Boot (SDK داخل التطبيق): للخدمات التي تمتلك شيفرتها، تحقن SDKs داخل العملية أعطالاً على مستوى الدالة، دون أي وكيل شبكة. هذا مفيد لاختبار منطق إعادة المحاولة في كود الـclient الخاص بك وفي البيئات التي لا تستطيع فيها تشغيل sidecar.

# chaos-monkey-spring-boot: التفعيل عبر خصائص التطبيق management.endpoint.chaosmonkey.enabled=true chaos.monkey.enabled=true # إعداد الهجمات: رمي RuntimeException على 30% من الاستدعاءات # لأي bean مُعلّم بـ@Service chaos.monkey.assaults.level=5 chaos.monkey.assaults.latencyActive=false chaos.monkey.assaults.exceptionsActive=true chaos.monkey.assaults.exception.type=java.lang.RuntimeException chaos.monkey.assaults.exception.arguments[0].type=java.lang.String chaos.monkey.assaults.exception.arguments[0].value=chaos: simulated error chaos.monkey.watcher.service=true
لا تُشغّل حقن الأخطاء بدون حماية في الإنتاج. دائماً احجب التجارب خلف feature flag أو namespace مُخصص للفوضى. حقن 503 بنسبة 100% على مسار حرج في الإنتاج — حتى لفترة قصيرة — قد يُسبب تأثيراً حقيقياً على العملاء قبل اكتمال التراجع. ابدأ في بيئة التجريب، تحقق من ثبات حالة المراقبة، ثم شغّل تجارب الإنتاج مع تحديد نطاق التأثير بـ1-5% من الحركة ومفتاح إيقاف آلي.

ربط التجارب بالمراقبة

تجربة الفوضى على مستوى التطبيق لا تساوي شيئاً بدون مراقبة جيدة. قبل حقن أي عطل، تأكد من توفر:

  • حالة ثابتة مُعرَّفة: زمن استجابة p99 < 200ms، معدل خطأ < 0.1%، عمق الطابور < 50.
  • لوحات مراقبة مفتوحة: مقاييس RED الخاصة بخدمتك (Rate, Errors, Duration) وصحة التبعية.
  • مفتاح إيقاف جاهز: أمر واحد أو نقرة واحدة تُزيل العطل فوراً.
  • تنبيهات مُعدَّلة بشكل مناسب: أو لا — تشغيل التجارب مع التنبيهات الحقيقية هو اختبار صالح لدليل تشغيل المناوبة.

بعد كل تجربة، وثّق ما انكسر، وما صمد، وما يحتاج إصلاحاً. النتيجة ليست الفشل — النتيجة هي الفجوة بين مرونتك المفترضة والواقع المقيس.