مشروع: جعل النظام مرناً
مشروع: جعل النظام مرناً
على مدار هذا البرنامج التعليمي، درست كل نمط من أنماط المرونة بشكل منفصل: التكرار، والتحويل التلقائي للفشل، وقواطع الدارة، والحواجز العازلة، وتحديد المعدلات، وإعادة المحاولة مع التراجع التدريجي، وفحوصات الصحة، والمراقبة، والتدهور التدريجي. الأنظمة الحقيقية تتطلب تطبيقها جميعاً معاً، في الأماكن الصحيحة، مع المقايضات الصحيحة. يستعرض هذا الدرس الختامي منصة تجارة إلكترونية واقعية، يحدد كل نقطة فشل منفردة فيها، ثم يطبق مجموعة الأدوات الكاملة لإنتاج معمارية مرنة جاهزة للإنتاج — بأرقام ملموسة ومبررات عند كل خطوة.
النظام الأولي: بنية هشة
تخيّل متجراً إلكترونياً متوسط الحجم: ShopFast. يخدم 50,000 مستخدم نشط يومياً، يعالج 200 طلب شراء في الدقيقة عند الذروة، ويمتلك هدف SLO بنسبة توافر 99.9% (ميزانية 43 دقيقة توقف شهرياً). تبدو البنية الحالية كما يلي:
- خادم تطبيق ويب واحد (Node.js) خلف وكيل عكسي Nginx منفرد.
- قاعدة بيانات PostgreSQL أساسية واحدة. لا نسخ احتياطية.
- نسخة Redis واحدة للجلسات وبيانات السلة.
- واجهة برمجية خارجية (Stripe) تُستدعى بشكل متزامن عند كل عملية دفع.
- خادم SMTP واحد لتأكيدات الطلبات، يُستدعى أيضاً بشكل متزامن.
- لا فحوصات صحة، ولا قواطع دارة، ولا تحديد للمعدلات، ولا مراقبة تتجاوز تنبيهات المعالج على مستوى نظام التشغيل.
كل مكوّن هو نقطة فشل منفردة (SPOF). توقف قاعدة البيانات → توقف الموقع بأكمله. انتهاء مهلة Stripe → تجمّد الدفع → استنزاف مجمع عمليات خادم الويب → توقف الموقع بأكمله. ارتفاع مفاجئ في الحركة → لا حماية → تشبّع مجمع اتصالات قاعدة البيانات → أخطاء 500 لجميع المستخدمين. النظام ليس مرناً؛ إنه هش.
الخطوة 1: القضاء على نقاط الفشل المنفردة بالتكرار
طبّق تكراراً بمعدل N+1 على كل مكوّن عديم الحالة وتكراراً نشطاً-سلبياً على كل مكوّن يحمل حالة:
- موازن الحمل: استبدل Nginx المنفرد بموازني حمل في وضع نشط-سلبي خلف عنوان IP افتراضي (keepalived / cloud NLB). فحص صحة كل 5 ثوانٍ؛ تحويل بأقل من 10 ثوانٍ.
- خوادم التطبيقات: تشغيل 3 خوادم كحد أدنى (2 يخدمان الحركة، 1 يستوعب العطل مع هامش). موازن الحمل يوزع باستخدام أقل اتصال.
- PostgreSQL: إضافة نسخة احتياطية متزامنة في نفس منطقة التوافر (RPO ≈ 0)، بالإضافة إلى نسخة غير متزامنة عبر منطقتين للقراءة والتعافي من الكوارث (RPO ≈ 1-5 ثوانٍ). تحويل تلقائي عبر Patroni؛ SLA الترقية < 30 ثانية.
- Redis: وضع Sentinel مع نسخة أساسية وتكراريتين. ترقية تلقائية عند فشل الأساسية؛ إعادة محاولة من جانب العميل مع تراجع أسي.
الخطوة 2: إضافة طابور رسائل — فك اقتران الاستدعاءات الخارجية
الاستدعاءات المتزامنة لـ Stripe وSMTP هي أكبر خطر موثوقية بعد قاعدة البيانات. افصلها باستخدام طابور غير متزامن (RabbitMQ أو بديل مُدار مثل SQS):
- معالج الدفع: التحقق من صحة السلة، خصم المخزون، كتابة سجل
ORDERS.pendingفي PostgreSQL، ونشر حدثcheckout.requestedفي الطابور. إعادة HTTP 202 للعميل فوراً. - عامل الدفع المستقل يستهلك
checkout.requested، يستدعي Stripe، ويحدّث سجل الطلب. إذا فشل Stripe، تبقى الرسالة في الطابور لإعادة المحاولة — لا تحجب خيط عامل ويب. - عامل البريد الإلكتروني يستهلك
order.confirmedويستدعي SMTP. إعادة المحاولة التلقائية عند الفشل العابر؛ لا يمس مسار طلب HTTP أبداً.
النتيجة: انقطاع Stripe لم يعد يستنزف خيوط عمال الويب. نقطة نهاية الدفع تبقى متاحة حتى عند تدهور Stripe؛ الطلبات تُوضع في الطابور وتُعالَج فور التعافي.
الخطوة 3: قواطع الدارة على كل تبعية خارجية
حتى مع الطابور، عامل الدفع لا يزال يستدعي Stripe. أضف قاطع دارة بهذه المعاملات:
- العتبة: الفتح بعد 5 حالات فشل في نافذة 10 ثوانٍ.
- مدة الحالة المفتوحة: 30 ثانية — لا تحاول استدعاء Stripe. زيادة مقياس
payment.circuit_openوتنبيه المسؤول المناوب. - فحص نصف المفتوح: السماح بطلب واحد؛ إغلاق الدارة عند النجاح، إعادة الفتح عند الفشل.
- الاحتياطي: عند الفتح، تعيين الطلب كـ
PAYMENT_DEFERREDوجدولة مهمة إعادة المحاولة بعد 60 ثانية. إبلاغ المستخدم بالبريد الإلكتروني بأن معالجة الدفع متأخرة قليلاً.
طبّق النمط نفسه على مزوّد SMTP: إذا فشلت 3 إرسالات متتالية، افتح الدارة ووجّه الرسائل عبر مزوّد ثانوي (مثل SendGrid احتياطياً لـ Mailgun).
الخطوة 4: الحواجز العازلة — عزل نطاقات الفشل
قسّم مجمعات الخيوط والاتصالات حتى لا تُجوّع ارتفاعات حركة مزية ما ميزات أخرى:
- مجمع الدفع: 50 اتصال قاعدة بيانات مخصص. عمال الدفع محدودون بـ 50 استدعاء Stripe متزامن.
- مجمع كتالوج المنتجات: 30 اتصال مخصص للاستعلامات القرائية. ارتفاع حركة الدفع لا يؤثر على التصفح.
- مجمع الإدارة: 10 اتصالات. أدوات الإدارة لا تتنافس مع حركة العملاء أبداً.
استخدم قاعدة بيانات Redis منطقية منفصلة (أو نسخة Redis ثانية) للجلسات مقابل ذاكرة التخزين المؤقت للمنتجات. إذا أُفرغت Redis للمنتجات، تبقى الجلسات غير متأثرة.
الخطوة 5: تحديد المعدلات — الحماية من ارتفاعات الحركة
نفّذ تحديد المعدلات في طبقة موازن الحمل / بوابة API:
- لكل مستخدم: 60 طلباً/دقيقة لنقطة نهاية الدفع (دلو رمز مدعوم بـ Redis).
- عالمياً: إذا تجاوزت الطلبات/الثانية الإجمالية 1.5× خط الأساس عند النسبة المئوية 95، إعادة HTTP 429 مع
Retry-After: 5لنسبة من الطلبات (تخليص متناسب)، للحفاظ على الطاقة للعملاء المدفوعين. - حماية من البوتات: تحدي الطلبات ذات وكلاء مستخدم مشبوهين أو >100 طلب/ثانية من IP واحد.
الخطوة 6: المهل الزمنية في كل طبقة
حدد مهلاً زمنية صريحة في كل مكان، لأن غياب المهلة يعني انتظاراً غير محدود يمكن أن يتحول إلى انقطاع كامل:
- موازن الحمل ← خادم التطبيق: اتصال 1 ثانية، قراءة 10 ثوانٍ.
- خادم التطبيق ← PostgreSQL: اتصال 2 ثانية، استعلام 5 ثوانٍ (تجاوز إلى 60 ثانية لاستعلامات تقارير الإدارة).
- خادم التطبيق ← Redis: اتصال 0.5 ثانية، أمر 1 ثانية.
- عامل الدفع ← Stripe: اتصال 3 ثوانٍ، قراءة 15 ثانية.
- عامل البريد ← SMTP: اتصال 5 ثوانٍ، قراءة 10 ثوانٍ.
اقرن المهل الزمنية بـالتراجع الأسي مع الاهتزاز العشوائي عند إعادة المحاولة: قاعدة 500 مللي ثانية، مضاعف 2×، حد أقصى 30 ثانية، اهتزاز ±20%. حدد المحاولات بـ 3 للمسارات الموجهة للمستخدم، وغير محدود لعمال الخلفية (مع طابور رسائل ميتة بعد 10 إخفاقات).
الخطوة 7: فحوصات الصحة والتدهور التدريجي
كل مكوّن يُظهر نقطة نهاية صحة منظّمة:
GET /health/live— يعيد 200 إذا كانت العملية تعمل (الحيوية). موازن الحمل يستطلع كل 5 ثوانٍ؛ بعد إخفاقين متتاليين يوقف التوجيه لتلك النسخة.GET /health/ready— يعيد 200 فقط إذا كانت قاعدة PostgreSQL الأساسية وRedis الأساسية والوسيط جميعها قابلة للوصول في 500 مللي ثانية (الاستعداد). نسخة خادم تطبيق جديدة لا تستقبل حركة حتى ينجح هذا الفحص.
استراتيجية التدهور التدريجي لـ ShopFast:
- Redis معطّل: تقديم كتالوج المنتجات من PostgreSQL (أبطأ لكن دقيق). تعطيل ودجت "شوهدت مؤخراً" المخصص (تعيين علامة الميزة إلى إيقاف). الجلسات تحتاط إلى ملفات تعريف ارتباط موقّعة. السلة تُحفظ في PostgreSQL.
- النسخة القرائية معطلة: توجيه جميع الاستعلامات إلى الأساسية. تنبيه المسؤول المناوب. قبول الحمل الأعلى على الأساسية كمقايضة مؤقتة بدلاً من إعادة أخطاء.
- Stripe متدهور: الدارة مفتوحة ← الطلبات تُعلَّم PAYMENT_DEFERRED ← المستخدم يرى "تم تسجيل طلبك؛ ستتم معالجة الدفع خلال 5 دقائق." الطابور يعيد المحاولة تلقائياً.
- وسيط الطابور معطّل: الاحتياط إلى استدعاء Stripe المتزامن بمهلة 10 ثوانٍ ومحاولة واحدة إعادة. إذا فشل أيضاً، عرض خطأ. عطل الطابور هو الملاذ الأخير قبل عرض أخطاء.
الخطوة 8: المراقبة والتنبيه ولوحات التحكم
أدوات كل طبقة بـمقاييس Prometheus تُجمع كل 15 ثانية وتُعرض في Grafana. الحد الأدنى من المقاييس لـ ShopFast:
http_request_duration_seconds— p50 وp95 وp99 لكل نقطة نهاية. تنبيه حرق SLO يُطلق عندما يتجاوز p99 500 مللي ثانية لأكثر من 5 دقائق.http_requests_total{status="5xx"}— معدل الأخطاء. تنبيه عند >1% على مدى دقيقتين.payment_circuit_state(0=مغلق، 1=مفتوح) — استدعاء المسؤول المناوب فور الفتح.queue_depth— تنبيه إذا تجاوز طابور الدفع 5,000 رسالة غير معالجة (يشير إلى تجويع العمال).postgres_replication_lag_bytes— تنبيه إذا تجاوز التأخر 10 ميغابايت (خطر على RPO).redis_connected_clients— تنبيه إذا اقترب من حدmaxclients.
حدد تنبيهات معدل حرق SLO باستخدام نافذتين: نافذة سريعة (1 ساعة، معدل حرق > 14.4×) ونافذة بطيئة (6 ساعات، معدل حرق > 6×). إطلاق كلتيهما في آنٍ واحد يعني أن ميزانية الأخطاء الشهرية تحترق بالسرعة الكافية للنفاد في < 1 ساعة — استدعِ شخصاً الآن.
الخطوة 9: قياس التحسّن
بعد تطبيق جميع الأنماط، قدّر التحسن في التوافر النظري. توافر النظام هو حاصل ضرب توافر كل مكوّن (للمكونات المتسلسلة) ويتحسن بشكل ملحوظ عند توازي المكونات:
- قبل: خادم تطبيق واحد بنسبة 99.5% × قاعدة بيانات واحدة بنسبة 99.9% × Redis واحد بنسبة 99.9% × Stripe المتزامن بنسبة 99.9% ≈ توافر مركّب 99.2%. أي ~58 ساعة توقف في السنة — 14× فوق الميزانية.
- بعد: ثلاثة خوادم تطبيق (أي 1 من 3 يجب أن يكون متاحاً: 1 − 0.005³ ≈ 99.999%) × قاعدة بيانات مع نسخة احتياطية متزامنة (تحويل < 30 ثانية؛ توافر شهري ≈ 99.97%) × Redis Sentinel (≈ 99.95%) × طابور الدفع غير المتزامن (مسار الدفع لم يعد يعتمد على Stripe بشكل متزامن) ≈ > 99.9% توافر مركّب. أي ضمن ميزانية الـ 43 دقيقة الشهرية.
قائمة تدقيق المرونة
عند مراجعة أي تصميم نظام للمرونة، اتبع قائمة التدقيق هذه:
- مسح نقاط الفشل المنفردة: ارسم المعمارية. دوّر كل مكوّن يُوقف النظام كله عند فشله. اقضِ على كل واحد بالتكرار أو التدهور التدريجي.
- تدقيق التبعيات الخارجية: أدرج كل واجهة برمجية خارجية. كل واحدة يجب أن تُستدعى بشكل غير متزامن (طابور) أو محمية بقاطع دارة مع احتياطي مُختبَر.
- خريطة المهل الزمنية: كل استدعاء شبكة له مهلة. كل مهلة اختُبرت (حقن كمون اصطناعي).
- سياسة إعادة المحاولة: كل إعادة محاولة لها سقف، تستخدم التراجع الأسي مع الاهتزاز، وتكتب في طابور رسائل ميتة بعد الاستنزاف.
- التحقق من الحواجز العازلة: ارتفاع حركة مزية ما لا يُجوّع مزية أخرى. تأكّد بمجمعات اتصال ومستهلكي طوابير منفصلة.
- تغطية فحوصات الصحة: تحققات الحيوية والاستعداد موجودتان ودقيقتان — لا تعيد 200 بشكل تافه دون فحص التبعيات فعلاً.
- أنماط التدهور موثّقة: لكل نقطة فشل منفردة، تجربة المستخدم المتدهورة محددة بشكل صريح ومُختبرة ومتصل بها (علامات ميزات، مسارات احتياطية).
- ميزانية الأخطاء مُتتبّعة: بيانات SLI تُجمع، معدل حرق SLO يُطلق التنبيهات، وفريق المناوبة يراجع استهلاك الميزانية أسبوعياً.
- اختبار الفوضى: أوقف خادم تطبيق في بيئة التهيئة. أشبع مجمع اتصالات قاعدة البيانات. أدخل كمون 5 ثوانٍ على Stripe API. تحقق من تدهور النظام بأناقة، لا بكارثية.
الملخص
لقد تتبّعت تمريناً كاملاً لتقوية المرونة: حددت كل نقطة فشل منفردة في نظام واقعي، طبّقت التكرار، فككت اقتران الاستدعاءات الخارجية المتزامنة بطابور، رسّخت قواطع الدارة والحواجز، حددت مهلاً زمنية عند كل نقطة تبادل، أنشأت فحوصات صحة مع تدهور تدريجي، وربطت المراقبة مع تنبيهات معدل حرق SLO. النتيجة نظام يلبي هدف SLO بنسبة 99.9% — والأهم، نظام أنماط فشله معروفة ومُختبَرة ومعالَجة بدلاً من اكتشافها في حادثة إنتاج الساعة 3 صباحاً. هندسة المرونة لا تتعلق ببناء أنظمة لا تفشل أبداً. إنها تتعلق بجعل الفشل رخيصاً ومرئياً وقابلاً للتعافي.