الدروس
متوسط ما بعد الديمو · ٣ من ٥

ما بعد الديمو — الاعتمادية والاستعداد لبيئة الإنتاج

عندما يتعطل كل شيء… هل يبقى تطبيقك واقفًا؟ من نقطة الفشل الواحدة إلى الطوابير والنشر والتراجع والمراقبة.

د
Devsamhan
٢٥ يوليو ٢٠٢٦ ٧٥ دقيقة قراءة ٨٠ نقطة تحقق ما بعد الديمو — Devsamhan
ما بعد الديمو — الاعتمادية والاستعداد لبيئة الإنتاج

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

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

ثم انتقلنا في الجزء الثاني إلى قواعد البيانات والأداء، وتحدثنا عن تصميم العلاقات والقيود، والاستعلامات، والفهارس، ومشكلة N+1، والمعاملات، والتزامن، وConnection Pooling، والتخزين المؤقت.

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

وبذلك أنهينا محطتين أساسيتين من رحلتنا من النموذج التجريبي إلى المنتج الحقيقي.

📚
خمسة أجزاء — دليل كامل لما بعد الديمو
  1. الأمن السيبراني وحماية التطبيق — انتهينا منه في الجزء الأول.
  2. قواعد البيانات والأداء — انتهينا منه في الجزء الثاني.
  3. الاعتمادية والاستعداد لبيئة الإنتاج — الجزء الحالي.
  4. كتابة كود نظيف وقابل للصيانة بدل كود السباغيتي.
  5. اختبار ما بناه الذكاء الاصطناعي وإثبات جودته بالأدلة.

في هذا الجزء سننتقل من سؤال:

هل الميزة آمنة؟ وهل تستطيع قاعدة البيانات تنفيذها بكفاءة؟

إلى سؤال أكبر:

ماذا يحدث عندما يتعطل خادم، أو يتوقف مزود خارجي، أو تمتلئ الطوابير، أو يفشل النشر، أو يصل عدد المستخدمين فجأة إلى أضعاف المعتاد؟

فالمنتج الحقيقي ليس المنتج الذي لا يتعطل أبدًا. هذا المنتج غير موجود.

المنتج الحقيقي هو الذي:

١. لماذا يختلف الإنتاج عن الديمو؟

الديمو يفترض أن كل شيء متاح

عندما نبني نموذجًا أوليًا، فإننا نختبره غالبًا في ظروف مثالية:

هذا هو المسار السعيد — Happy Path مرة أخرى.

لكن بيئة الإنتاج لا تلتزم بهذه الافتراضات. قد يحدث في أي لحظة:

لهذا لا يكفي أن تقول:

الميزة تعمل عندي.

يجب أن تسأل:

كيف تتصرف عندما لا يعمل أحد اعتمادياتها؟

الاعتمادية لا تعني منع جميع الأعطال

من السهل أن نفهم الاعتمادية على أنها محاولة بناء نظام لا يفشل أبدًا. لكن هذا الهدف مكلف وغير واقعي.

تعتمد هندسة الاعتمادية على قبول حقيقة أن الأعطال ستقع، ثم تصميم النظام بحيث يكون أثرها محدودًا وقابلًا للإدارة.

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

💡
الفكرة الأساسية

لا تحاول فقط منع الفشل، بل صمّم طريقة واضحة للفشل والتعافي.

نقطة الفشل الواحدة — Single Point of Failure

لنفترض أن تطبيقك يعمل على خادم واحد. هذا الخادم يحتوي:

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

يسمى المكوّن الذي يؤدي تعطله إلى توقف النظام كله Single Point of Failure.

ولا يقتصر ذلك على الخوادم. قد تكون نقطة الفشل:

قبل إضافة تقنيات جديدة، ارسم مسار أهم العمليات واسأل:

إذا توقف هذا المكوّن، فهل تتوقف العملية كلها؟ وهل نملك بديلًا أو طريقة تعافٍ؟

٢. التوسع: من خادم واحد إلى عدة نسخ

التوسع الرأسي والتوسع الأفقي

عندما يزداد الضغط، لديك اتجاهان رئيسيان للتوسع.

التوسع الرأسي — Vertical Scaling يعني زيادة موارد الخادم نفسه: معالج أقوى، ذاكرة أكبر، قرص أسرع، عدد اتصالات أكبر ضمن القدرة الفعلية.

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

التوسع الأفقي — Horizontal Scaling يعني تشغيل نسخ متعددة من التطبيق:

Application Instance 1
Application Instance 2
Application Instance 3
Application Instance 4

ثم توزيع الطلبات بينها.

يستطيع Kubernetes مثلًا تعديل عدد النسخ تلقائيًا بناءً على مقاييس مثل استهلاك الموارد أو مقاييس مخصصة، بينما تتيح خدمات السحابة تشغيل مجموعات تتوسع أو تنكمش مع تغير الطلب.

لكن تشغيل أربع نسخ بدل نسخة واحدة لا يجعل التطبيق قابلًا للتوسع تلقائيًا. يجب أولًا أن يكون التطبيق مناسبًا للتشغيل المتعدد.

لماذا قد يفشل التطبيق بعد إضافة نسخة ثانية؟

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

const sessions = new Map();

سجل المستخدم دخوله عبر الخادم الأول، ثم أرسل الطلب التالي إلى الخادم الثاني. الخادم الثاني لا يعرف الجلسة.

أو يحتفظ التطبيق بمهام مجدولة داخل كل نسخة:

scheduleDailyBilling();

عند تشغيل أربع نسخ، تُنفذ مهمة الفوترة أربع مرات.

أو يخزن الملفات محليًا:

/app/uploads/avatar.png

رفع المستخدم الملف إلى الخادم الأول، ثم حاول تنزيله من الخادم الثالث فلم يجده.

لكي يعمل التوسع الأفقي بصورة صحيحة، تحتاج غالبًا إلى:

موازن الحمل — Load Balancer

يقف Load Balancer أمام نسخ التطبيق، ويتلقى الطلبات ثم يوزعها على الخوادم المتاحة.

Users
   ↓
Load Balancer
   ├── Server A
   ├── Server B
   └── Server C

يمكن لموازن الحمل أيضًا فحص حالة الخوادم والتوقف عن إرسال الطلبات إلى النسخة التي يعتبرها غير سليمة. تشرح وثائق AWS الرسمية أن موازن الحمل يوزع الحركة على أهداف متعددة، ويراقب صحتها، ويرسل الطلبات إلى الأهداف السليمة فقط.

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

ما الذي لا يحله؟ موازن الحمل لا يصلح:

ولذلك يحتاج إلى Health Checks صحيحة.

ليس كل خادم يعمل هو خادم جاهز

قد تكون عملية التطبيق موجودة، لكنها غير قادرة على خدمة المستخدمين. مثلًا: التطبيق بدأ لكن Migration لم تكتمل، أو اتصالات قاعدة البيانات فشلت، أو Cache غير متاح، أو نموذج البيانات لم يُحمّل، أو الخادم دخل في Deadlock، أو ما زال يبدأ ويحمّل ملفات كبيرة.

لهذا توجد أنواع مختلفة من فحوصات الحالة.

Startup Check — هل انتهى التطبيق من بدء التشغيل؟ يفيد مع التطبيقات التي تحتاج وقتًا لتحميل نموذج ذكاء اصطناعي، أو إعدادات كبيرة، أو Cache أولي، أو ملفات أو شهادات، أو اتصالات لازمة.

Readiness Check — هل التطبيق جاهز الآن لاستقبال الطلبات؟ إذا فشل، ينبغي عادةً إيقاف توجيه الطلبات الجديدة إلى النسخة، دون افتراض أنها تحتاج إلى إعادة تشغيل فورية.

Liveness Check — هل التطبيق ما زال حيًا وقادرًا على التقدم؟ قد تبقى العملية ظاهرة في نظام التشغيل، لكنها عالقة ولا تنجز شيئًا. يمكن أن يؤدي فشل هذا الفحص إلى إعادة تشغيلها.

تميز Kubernetes رسميًا بين Liveness وReadiness وStartup Probes؛ فالأولى تساعد على معرفة متى تحتاج الحاوية إلى إعادة تشغيل، والثانية تحدد جاهزيتها لاستقبال الحركة، والثالثة تمنح التطبيقات بطيئة الإقلاع وقتًا قبل بدء الفحوص الأخرى.

لا تجعل فحص الصحة يسبب المرض

قد يكتب المساعد:

GET /health

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

ثم يصل Load Balancer إلى هذا المسار مرات كثيرة، فيصبح Health Check نفسه مصدر ضغط.

⚠️
فشل مزود البريد لا يعني قتل خادم API

قد لا ينبغي أن يفشل Liveness Check لمجرد توقف قاعدة البيانات مؤقتًا؛ لأن إعادة تشغيل كل النسخ قد تضاعف المشكلة.

يجب أن يكون الفحص سريعًا، ومحدودًا، وواضح الهدف، ومرتبطًا بالقرار الذي سيتخذ عند فشله. حدّد لكل اعتماد:

التوسع التلقائي — Auto Scaling

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

يوفر Kubernetes Horizontal Pod Autoscaler لإدارة عدد النسخ بناءً على المقاييس المحددة، كما يمكن ربط مجموعات Auto Scaling بموازن الحمل وإضافة النسخ أو إزالتها مع تغير الطلب.

لكن استخدام CPU وحده قد يكون مضللًا. قد يكون التطبيق بطيئًا بسبب انتظار قاعدة البيانات، أو انتظار API خارجي، أو نفاد الاتصالات، أو Queue متراكمة، أو Lock طويل، أو حد التزامن. في هذه الحالات قد يبقى CPU منخفضًا رغم سوء تجربة المستخدم.

الأسئلة التي يجب طرحها:

Auto Scaling قد ينقل الاختناق فقط

لنفترض أن لديك نسختين من API، وكل نسخة تفتح عشرة اتصالات بقاعدة البيانات، وقاعدة البيانات تتحمل خمسين اتصالًا للتطبيق.

إذا توسع النظام تلقائيًا إلى عشر نسخ، فقد يحاول فتح مئة اتصال. عالجت ضغط API، لكنك أسقطت قاعدة البيانات.

وقد تزيد Workers لمعالجة Queue، لكن الخدمة الخارجية تسمح بعشرة طلبات فقط في الثانية. وقد تزيد خوادم الصور، لكن التخزين لديه حد نقل.

لذلك التوسع ليس قرارًا لمكوّن واحد فقط. يجب أن تنظر إلى السلسلة كاملة:

User
→ CDN
→ Load Balancer
→ API
→ Database
→ Queue
→ Workers
→ External Services

قدرة النظام تساوي قدرة أضعف نقطة في هذا المسار.

Serverless لا يلغي هندسة الاعتمادية

تزيل منصات Serverless كثيرًا من عبء إدارة الخوادم، لكنها لا تلغي حدود التزامن، وCold Starts، ومهلات التنفيذ، وحدود الذاكرة، واتصالات قاعدة البيانات، وRate Limits، وازدواجية الرسائل، والحاجة إلى Idempotency، ومراقبة الأخطاء، أو التعامل مع تعطل الخدمات الخارجية.

Cold Start — قد تحتاج النسخة الجديدة إلى وقت من أجل بدء Runtime، وتحميل الاعتماديات، وإنشاء اتصالات، وتحميل نموذج، وقراءة الإعدادات، أو تهيئة المكتبات. قد يظهر ذلك للمستخدم كتأخير في أول طلب بعد فترة خمول أو عند توسع مفاجئ.

من الحلول بحسب المنصة والحاجة:

لا تستخدم تقنية لتدفئة الوظائف بصورة مستمرة قبل قياس المشكلة؛ فقد تزيد التكلفة دون فائدة واضحة.

٣. المعالجة غير المتزامنة والطوابير

لا تنفذ كل شيء داخل الطلب

لا ينبغي تنفيذ كل عملية داخل الطلب الذي ينتظر المستخدم نتيجته.

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

إذا نفذت كل ذلك داخل Request واحد، فقد يحدث انتهاء مهلة الطلب، أو بقاء اتصال الخادم مشغولًا، أو إعادة المستخدم الطلب، أو تكرار العملية، أو فقدان التقدم عند إعادة تشغيل الخادم، أو صعوبة معرفة المرحلة التي فشلت.

الأفضل في حالات مناسبة:

  1. يستقبل الخادم الطلب.
  2. يتحقق من المدخلات والصلاحيات.
  3. ينشئ Job.
  4. يضع رسالة في Queue.
  5. يعيد للمستخدم معرف العملية.
  6. ينفذ Worker المهمة في الخلفية.
  7. تُحدّث الحالة.
  8. يُخطر المستخدم عند الانتهاء.
User Request
    ↓
API creates Job
    ↓
Message Queue
    ↓
Worker
    ↓
Database / External Service

توفر الطوابير وسيلة لفصل منتج الرسائل عن مستهلكها، وتصف AWS SQS مثلًا الطابور بأنه وسيلة لدمج المكونات وفصلها في الأنظمة الموزعة.

متى تستخدم Queue؟

تكون مفيدة في عمليات مثل: إرسال البريد والإشعارات، ومعالجة الصور والفيديو، وإنشاء التقارير، وتحويل الملفات، وفهرسة البحث، وإنشاء Embeddings، ومعالجة Webhooks، ومزامنة الأنظمة، واستدعاءات الذكاء الاصطناعي الطويلة، وتحليل البيانات، وتنفيذ Retry منظم، أو أي عملية لا يحتاج المستخدم إلى انتظارها مباشرة.

⚠️
لا تجعل كل دالة تمر عبر Queue

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

الرسالة قد تصل أكثر من مرة

من أخطر الافتراضات:

كل رسالة ستصل مرة واحدة فقط.

في كثير من أنظمة الطوابير، يمكن تسليم الرسالة أكثر من مرة. توضح AWS أن Standard SQS Queues تستخدم تسليمًا من نوع At-Least-Once، ولذلك قد تصل أكثر من نسخة من الرسالة، وقد يصل ترتيب الرسائل بصورة مختلفة أحيانًا.

لذلك يجب أن يكون المستهلك مستعدًا للتكرار. إذا كانت الرسالة تقول Charge customer $100 ووصلت مرتين، فلا ينبغي تحصيل المبلغ مرتين. وإذا كانت تقول Create monthly invoice، فلا ينبغي إنشاء فاتورتين للشهر نفسه.

الحل يرتبط بما شرحناه في الجزء الثاني: Idempotency Key، وUnique Constraint، وسجل للرسائل المعالجة، وحالة عمل واضحة، ومعاملة ذرية، أو مفتاح أعمال يمنع التكرار.

بدل تنفيذ الرسالة مباشرة:

await chargeCustomer(message.customerId, message.amount);

استخدم معرف عملية فريدًا:

await processPayment({
  operationId: message.operationId,
  customerId: message.customerId,
  amount: message.amount
});

ويضمن النظام ألا ينفذ operationId نفسه مرتين.

Visibility Timeout والإقرار بالمعالجة

عندما يستلم Worker رسالة، قد يخفيها نظام Queue مؤقتًا عن بقية المستهلكين. إذا أكمل Worker المهمة، يحذف الرسالة أو يرسل Acknowledgment. إذا تعطل قبل ذلك، تصبح الرسالة مرئية مرة أخرى لتتم إعادة معالجتها.

في SQS مثلًا يبدأ Visibility Timeout عند تسليم الرسالة، وإذا لم تُحذف قبل انتهاء المدة تصبح مرئية مجددًا.

ماذا لو كانت المهلة أقصر من وقت المعالجة؟ لا يزال Worker الأول يعالج المهمة، وتنتهي المهلة، ويحصل Worker ثانٍ على الرسالة، فتُنفذ العملية مرتين في الوقت نفسه.

ماذا لو كانت أطول بكثير؟ إذا تعطل Worker، قد تبقى الرسالة مخفية مدة طويلة قبل إعادة المحاولة.

لذلك يجب أن تكون المهلة متوافقة مع زمن المهمة، مع إمكانية تمديدها للعمليات الطويلة، وحد أقصى منطقي، وHeartbeat أو تحديث للتقدم، وIdempotency، ومراقبة الرسائل التي تبقى قيد المعالجة مدة غير طبيعية.

Heartbeats: كيف تعرف أن العامل ما زال حيًا؟

الـHeartbeat هو إشارة دورية تقول: أنا ما زلت أعمل. يمكن استخدامها مع Workers، وJobs طويلة، واتصالات WebSocket، وخدمات موزعة، وLocks مؤقتة، أو أنظمة Leader Election.

لنفترض أن معالجة ملف تستغرق ثلاثين دقيقة. لا يكفي أن تكون حالته processing، فقد يموت Worker بعد الدقيقة الأولى وتظل الحالة عالقة أيامًا.

يمكن للـWorker تحديث:

last_heartbeat_at
progress_percent
current_stage

ثم يعتبر النظام المهمة عالقة إذا لم يصل Heartbeat خلال مدة محددة.

⚠️
Heartbeat لا يثبت أن العمل يتقدم

قد تكون العملية عالقة في Loop، لكنها ما زالت ترسل نبضات. لذلك قد تحتاج إلى متابعة آخر نبضة، وآخر تقدم حقيقي، ومدة المرحلة الحالية، وعدد العناصر المعالجة، أو آخر Checkpoint صالح.

Queue Overflow: ماذا لو وصلت الرسائل أسرع من معالجتها؟

تخيل أن المستخدمين يضيفون ألف مهمة في الدقيقة، بينما يستطيع Workers معالجة مئة مهمة فقط. سيزداد عمق Queue باستمرار:

1,000
5,000
20,000
100,000 messages

هذه ليست مشكلة Queue فقط، بل مشكلة قدرة النظام.

قد يؤدي التراكم إلى تأخر المستخدمين ساعات، وانتهاء صلاحية الرسائل، واستهلاك التخزين، وصعوبة التراجع، وضغط هائل عند عودة Workers، أو معالجة عمليات لم تعد مفيدة.

تسمح بعض أنظمة الطوابير بوضع حد أقصى لعدد الرسائل أو حجمها. توضح RabbitMQ مثلًا إمكانية تحديد الحد حسب عدد الرسائل أو إجمالي أحجامها.

الحلول المحتملة:

لا تجعل النظام يقبل أعمالًا بلا حدود ثم يفشل في تنفيذها.

Backpressure: أخبر المنتج أن المستهلك لا يستطيع المواصلة

الـBackpressure يعني ألا يستمر الجزء المنتج للعمل في إرسال كمية غير محدودة عندما لا يستطيع المستهلك مواكبتها.

يمكن تطبيقه عبر تحديد عدد الطلبات المتزامنة، وRate Limiting، وإرجاع 429 أو 503 بصورة مدروسة، وإيقاف قراءة المزيد من الرسائل مؤقتًا، وتقليل Batch Size، وتحديد Prefetch، ووضع حد لحجم Queue، أو إبلاغ المستخدم بأن العملية ستتأخر.

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

الرسالة السامة — Poison Message

قد توجد رسالة تفشل دائمًا بسبب بيانات غير صحيحة، أو نسخة Schema قديمة، أو ملف تالف، أو مورد غير موجود، أو خطأ برمجي خاص بقيمة معينة، أو اعتماد خارجي لن ينجح معه الطلب.

إذا أعدت المحاولة بلا حد: تستلم الرسالة، فتفشل، فتعود إلى Queue، فتستلم مجددًا، فتفشل مجددًا — فتستهلك الموارد وتحجب الرسائل السليمة.

لهذا نحتاج إلى حد أقصى للمحاولات، وتسجيل سبب الفشل، وتصنيف الأخطاء القابلة لإعادة المحاولة وغير القابلة لها، ثم نقل الرسالة إلى Dead Letter Queue.

Dead Letter Queue — DLQ

الـDLQ طابور منفصل تصل إليه الرسائل التي فشلت بعد عدد محدد من المحاولات. توضح وثائق SQS الرسمية أن DLQ تستقبل الرسائل التي لم تنجح معالجتها، بما يسمح بعزلها وفحص أسباب الفشل.

لكن DLQ ليست سلة مهملات ننقل إليها الرسائل ثم ننساها. يجب أن يكون لديك:

⚠️
لا تعِد جميع الرسائل دفعة واحدة

إذا أصلحت الخطأ ثم أعدت مليون رسالة من DLQ في لحظة واحدة، فقد تسقط النظام مرة أخرى. استخدم Redrive مدروسًا: دفعات محدودة، ومراقبة النتائج، وRate Limit، وإيقاف عند ارتفاع الفشل، والتحقق من أن الرسائل ما زالت ذات معنى.

٤. التعامل مع فشل الخدمات الخارجية

Retry ليس حلًا لكل خطأ

عندما يفشل طلب، قد يضيف المساعد:

retry(3);

لكن يجب أن تسأل: لماذا فشل الطلب؟ وهل ستفيد الإعادة؟

أخطاء مؤقتة قد تستحق Retry: Timeout عابر، وانقطاع شبكة قصير، و503 من خدمة مزدحمة، وتعارض مؤقت، وRate Limit يسمح بإعادة المحاولة لاحقًا.

أخطاء لا يفيد معها Retry غالبًا: كلمة مرور خاطئة، وملف غير صالح، وصلاحية مفقودة، ومورد غير موجود، وPayload يخالف Schema، ومفتاح API غير صحيح، أو قاعدة عمل ترفض العملية.

إعادة طلب غير صالح خمس مرات لا تجعله صالحًا.

Exponential Backoff وJitter

إذا أعاد آلاف العملاء الطلب بعد ثانية واحدة بالضبط، فقد يسببون موجة ضغط جديدة على الخدمة التي تحاول التعافي.

في Exponential Backoff تزداد مدة الانتظار بين المحاولات:

1 second
2 seconds
4 seconds
8 seconds

ويضاف Jitter، أي عشوائية صغيرة، حتى لا تعيد جميع النسخ الطلب في اللحظة نفسها.

توصي AWS بالتحكم في عدد المحاولات، واستخدام Exponential Backoff، وإضافة Jitter لتوزيع المحاولات زمنيًا بدل إعادة الضغط بصورة متزامنة.

ويجب تحديد عدد المحاولات، والمهلة بينها، والمهلة الإجمالية للعملية، والأخطاء القابلة للإعادة، وما الذي يحدث بعد الفشل النهائي، وهل العملية Idempotent.

⚠️
قد تكون إعادة المحاولة أخطر من الفشل

إذا كانت العملية ذات أثر جانبي ولا تعرف هل نُفذت المحاولة السابقة، فإعادتها قد تضاعف الضرر.

Timeout: لا تنتظر إلى الأبد

كل استدعاء خارجي يحتاج إلى مهلة واضحة. بدون Timeout، قد يبقى الطلب يحتفظ باتصال، أو يستهلك Thread، أو يحجز ذاكرة، أو يبقي Transaction مفتوحة، أو يمنع Worker من الانتقال إلى الرسالة التالية.

تؤكد AWS في إرشاداتها أن الانتظار الطويل لطلبات لا تنتهي قد يستنزف موارد محدودة مثل الذاكرة والخيوط والاتصالات، ولذلك يجب ضبط مهلات مناسبة.

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

Circuit Breaker: توقف عن طرق الباب المغلق

تخيل أن خدمة الدفع متوقفة. كل طلب مستخدم يحاول الاتصال بها، فيفشل، ثم يعيد المحاولة. يزيد الضغط على الخدمة المتعطلة وعلى تطبيقك نفسه.

يقوم Circuit Breaker بمراقبة الإخفاقات، وعندما تتجاوز حدًا معينًا يفتح الدائرة ويتوقف مؤقتًا عن إرسال الطلبات إلى الخدمة التي يُرجح فشلها.

له عادة حالات مفهومية:

توضح Microsoft أن Circuit Breaker يمنع التطبيق من تكرار عملية يُرجح أن تفشل، ويختلف عن Retry؛ فالأول يوقف الاستدعاءات نحو اعتماد متعطل، بينما يحاول الثاني معالجة فشل مؤقت.

الفائدة: منع استنزاف الموارد، وتقليل زمن انتظار المستخدم، وحماية الخدمة المتعثرة من مزيد من الضغط، وتفعيل Fallback، وإتاحة التعافي التدريجي.

Graceful Fallback: لا تجعل فقدان ميزة صغيرة يسقط المنتج

إذا تعطلت خدمة التوصيات، يمكنك عرض أحدث المنتجات بدل المنتجات المخصصة.

إذا تعطل نموذج الذكاء الاصطناعي، يمكنك عرض نتيجة مخزنة مناسبة، أو تحويل الطلب إلى Queue، أو استخدام نموذج احتياطي معتمد، أو تقديم الوظائف الأساسية دون التوليد، أو إبلاغ المستخدم أن الخدمة مؤقتًا غير متاحة.

إذا تعطل مزود الصور، قد تعرض Placeholder بدل إسقاط الصفحة. هذا هو Graceful Degradation: استمرار المنتج بوظائف أقل بدل توقفه بالكامل.

🚨
لا تستخدم Fallback لإخفاء فشل حساس

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

الوظائف غير الحساسة قد تتدهور بصورة مرنة، أما الأمن والملكية والمال فلا ينبغي أن تفشل بوضع يسمح بالعملية.

Model Fallback Chain في تطبيقات الذكاء الاصطناعي

قد تستخدم نموذجًا أساسيًا ثم تنتقل إلى نموذج احتياطي عند توقفه:

Primary Model
   ↓ failure
Secondary Model
   ↓ failure
Queue for later processing

لكن النموذج الاحتياطي قد يختلف في حجم السياق، وSchema الذي يعيده، وجودة اللغة، ودعم الأدوات، والسعر، ومكان معالجة البيانات، وسياسة الاحتفاظ، وحدود Tokens، أو قدرته على اتباع القيود.

لذلك يجب ألا يكون Fallback مجرد تغيير اسم النموذج. اختبر مسبقًا: توافق المخرجات، وحماية البيانات، وحدود التكلفة، وزمن الاستجابة، وTool Calling، وOutput Validation، وماذا يرى المستخدم عند الانتقال.

كما يجب منع سلسلة Fallback من التحول إلى حلقة مكلفة تجرب عددًا كبيرًا من النماذج بلا حد.

Bulkhead: لا تدع فشل خدمة يغرق البقية

استُخدم اسم Bulkhead تشبيهًا بالحواجز داخل السفينة، حيث يمنع الحاجز دخول الماء إلى قسم واحد من إغراق السفينة كاملة.

في الأنظمة يمكن فصل الموارد: مجموعة اتصالات مستقلة لكل خدمة، وQueue منفصلة للمهام الحرجة، وWorker Pool مستقل للصور، وحدود تزامن لكل مزود، وTenant كبير لا يستهلك كل الموارد، وعمليات الذكاء الاصطناعي لا تستخدم Threads الخاصة بطلبات المستخدم العادية.

تشرح Microsoft أن Bulkhead Pattern يعزل مكونات التطبيق في مجموعات، بحيث يستمر جزء من النظام حتى عند فشل جزء آخر، ويمنع انتشار العطل عبر المنظومة.

إذا كانت لديك Queue واحدة لكل شيء — رسائل تسجيل الدخول، وتقارير طويلة، وإنشاء صور، وإشعارات، وتحديثات مالية — فقد تؤدي آلاف الصور إلى تأخير رسالة حساسة. يمكن الفصل إلى طوابير وWorkers وحدود مختلفة بحسب الأولوية وطبيعة العمل.

Leader Election: من ينفذ المهمة الواحدة؟

عندما تشغل نسخًا متعددة، توجد أعمال تريد تنفيذها على كل نسخة، وأعمال يجب تنفيذها مرة واحدة فقط: إرسال تقرير يومي شامل، ومعالجة Cron Job، وإنشاء Batch مالي، وتنسيق مهمة موزعة، وتشغيل عملية تنظيف دورية، أو اختيار منسق لمجموعة Workers.

إذا شغلت المهمة داخل كل نسخة، قد تتكرر.

في Leader Election تتنافس النسخ على Lease أو Lock، وتصبح نسخة واحدة هي القائد النشط، بينما تبقى البقية احتياطية. يستخدم Kubernetes Leases لضمان وجود قائد نشط واحد لبعض مكونات التحكم عالية التوافر، وإذا لم يجدد القائد الـLease خلال المهلة، تتنافس النسخ الأخرى على القيادة.

💡
لا تستخدم Leader Election لكل شيء

في كثير من العمليات، الأفضل أن تجعل العمل Idempotent، أو موزعًا عبر Queue، أو محميًا بقيد قاعدة بيانات. فهو يضيف تعقيدًا: فقدان القيادة، وLease Expiry، وClock Skew، وانتقال المسؤولية، واحتمال أن يظن طرفان مؤقتًا أنهما القائد في تصميم سيئ.

٥. النشر والتراجع

الإيقاف المنظم — Graceful Shutdown

عند نشر إصدار جديد أو تقليل عدد النسخ، قد يحصل الخادم على إشارة إيقاف أثناء معالجة طلبات.

الإيقاف الفوري قد يؤدي إلى قطع طلبات المستخدمين، وترك رسالة بلا Acknowledgment، وإبقاء عملية في حالة غير واضحة، وعدم إغلاق الاتصالات، أو فقدان بيانات موجودة في الذاكرة.

في Graceful Shutdown ينبغي أن:

  1. تتوقف النسخة عن قبول عمل جديد.
  2. تُزال من موازن الحمل أو تصبح غير جاهزة.
  3. تكمل الطلبات والرسائل الحالية ضمن مهلة.
  4. تحفظ Checkpoints إذا لزم.
  5. تغلق الاتصالات.
  6. ثم تتوقف.

عند إنهاء Pod في Kubernetes، تُرسل عادةً إشارة SIGTERM مع فترة سماح، وبعد انتهائها يمكن إرسال SIGKILL للعمليات التي لم تتوقف.

لكن Grace Period ليست ضمانًا غير محدود. يجب أن يستطيع التطبيق إنهاء عمله ضمن المهلة، وألا يبدأ Job أطول من الزمن المتبقي إذا كان في طور الإيقاف.

Connection Draining

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

في Connection Draining: يتوقف إرسال طلبات جديدة، وتُمنح الطلبات الحالية وقتًا للانتهاء، ثم تُغلق الاتصالات المتبقية وفق سياسة واضحة.

هذا مهم مع HTTP Requests الطويلة، وWebSockets، وStreaming، ورفع الملفات، وWorkers، أو نشر تحديثات متكررة.

توفر وثائق Kubernetes شرحًا لتدفق إنهاء الـPods وطرق تنفيذ Graceful Connection Draining أثناء إزالة النسخ من نقاط الخدمة.

النشر ليس مجرد رفع الملفات

في الديمو قد يكون النشر:

build
upload
restart

لكن في الإنتاج يجب أن تفكر في:

Rolling Deployment

يستبدل النسخ القديمة تدريجيًا بنسخ جديدة:

Old Old Old
New Old Old
New New Old
New New New

يمكنه تقليل التوقف، لكنه يعني أن الإصدارين القديم والجديد قد يعملان معًا مدة معينة. توضح وثائق Kubernetes أن Rolling Update يستبدل الـPods القديمة تدريجيًا بالجديدة للمحافظة على إتاحة التطبيق أثناء التحديث.

لذلك يجب أن يكون API متوافقًا مؤقتًا، وSchema قاعدة البيانات متوافقًا مع الإصدارين، والرسائل في Queue قابلة للفهم من النسخ المختلفة، وFeature Flags مدروسة، وألا تُحذف الأعمدة التي لا يزال الإصدار القديم يحتاج إليها.

Blue/Green Deployment

تشغل بيئتين: Blue للإصدار الحالي، وGreen للإصدار الجديد. تختبر Green، ثم تحول الحركة إليها. إذا ظهرت مشكلة، يمكن إعادة الحركة إلى Blue بسرعة، ما دام التوافق قائمًا.

تصف AWS النشر الأزرق/الأخضر بأنه تشغيل بيئتين متشابهتين بإصدارين مختلفين، ثم نقل الحركة بينهما لتقليل التوقف وتسهيل العودة.

لكن التكلفة أعلى مؤقتًا، وقد تظل لديك مشكلات: قاعدة بيانات مشتركة تغيرت، وJobs تعمل في البيئتين، ورسائل Queue استهلكها الإصدار الجديد، وعمليات خارجية لا يمكن التراجع عنها، وSessions أو Cache غير متوافقة.

Canary Deployment

بدل إرسال جميع المستخدمين إلى الإصدار الجديد، ترسل نسبة صغيرة:

95% → old version
5%  → new version

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

تصف AWS ‏Canary Deployment بأنه نقل نسبة صغيرة من الحركة إلى الإصدار الجديد أولًا، ثم توسيعها بعد التحقق من نجاحه.

الميزة أن الخطأ يؤثر في مجموعة محدودة، لكن يجب أن تكون لديك مقاييس واضحة، ومدة تقييم، وحدود توقف آلي، وطريقة تمييز الإصدار في Logs وTraces، وخطة عودة.

Rollback: زر العودة ليس خطة كاملة

قد تحتوي المنصة على أمر مثل:

kubectl rollout undo deployment/app

وتدعم Kubernetes الرجوع إلى نسخة سابقة من Deployment باستخدام سجل الإصدارات المحفوظ.

لكن التراجع عن كود التطبيق أسهل من التراجع عن كل آثار الإصدار. ماذا لو قام الإصدار الجديد بحذف عمود، أو تحويل قيم، أو إرسال آلاف الرسائل، أو إنشاء مدفوعات، أو تغيير ملفات المستخدمين، أو تعديل شكل Cache، أو إنشاء بيانات لا يفهمها الإصدار السابق؟

قد تعود الحاوية القديمة، لكنها لا تستطيع العمل على Schema الجديد.

لهذا يجب أن تتضمن خطة التراجع: كود التطبيق، وقاعدة البيانات، وMigrations، وJobs، وQueues، وCache، والخدمات الخارجية، وFeature Flags، والعمليات التي لا يمكن عكسها.

هجرات قاعدة البيانات والتوافق التدريجي

من الأساليب الأكثر أمانًا تنفيذ التغيير على مراحل. لنفترض أنك تريد استبدال عمود full_name بعمودين first_name وlast_name. بدل حذف full_name مباشرة:

المرحلة الأولى: Expand — أضف الأعمدة الجديدة، واجعل الإصدار الجديد يكتب في القديم والجديد عند الحاجة، وانقل البيانات السابقة، وراقب التطابق.

المرحلة الثانية: Migrate — اجعل القراءة من الأعمدة الجديدة، وتحقق من جميع البيانات، وحدّث بقية الخدمات.

المرحلة الثالثة: Contract — توقف عن استخدام العمود القديم، واحذفه في إصدار لاحق بعد إثبات عدم الحاجة إليه.

هذا يسمح بتشغيل الإصدارين القديم والجديد مؤقتًا ويجعل Rollback أقل خطورة.

٦. النسخ الاحتياطي والتعافي

النسخة الاحتياطية ليست خطة تراجع فورية

هناك فرق بين Rollback — العودة السريعة إلى إصدار أو حالة تشغيل سابقة — وBackup and Restore — استعادة البيانات من نسخة محفوظة بعد فقدها أو تلفها.

قد تحتاج استعادة قاعدة كبيرة إلى وقت، وقد تفقد البيانات التي أُنشئت بعد آخر نسخة بحسب استراتيجية النسخ.

يدعم PostgreSQL عدة طرق للنسخ والاستعادة، منها SQL Dumps، والنسخ الأساسي، والأرشفة المستمرة مع Point-in-Time Recovery.

لكن وجود ملف Backup لا يثبت أنه مكتمل، ولا أنه غير تالف، ولا أن المفاتيح متاحة، ولا أن لديك صلاحية الوصول، ولا أن الاستعادة تنجح، ولا أن الفريق يعرف الخطوات، ولا أن الزمن المطلوب مقبول.

🚨
القاعدة المهمة

النسخة الاحتياطية التي لم تختبر استعادتها هي أمل، وليست خطة تعافٍ مثبتة.

اختبر خطة الاستعادة — RPO وRTO

يجب أن تعرف: ما الذي ننسخه؟ وكم مرة؟ وأين تُخزن النسخ؟ وهل هي مشفرة؟ ومن يستطيع الوصول إليها؟ وكم مدة الاحتفاظ؟ وما مقدار البيانات الذي يمكن أن نخسره؟ وكم يستغرق التعافي؟ وكيف نتحقق من سلامة النتيجة؟ وهل تشمل الأدوار والصلاحيات والإعدادات؟ وهل نملك بيئة آمنة لتجربة الاستعادة؟

Recovery Point Objective — RPO: ما أقصى مقدار بيانات تقبل خسارته؟ إذا كان RPO ساعة، فقد تخسر ما يصل إلى ساعة من التغييرات.

Recovery Time Objective — RTO: كم من الوقت يمكن أن يبقى المنتج متوقفًا قبل استعادته؟ قد يكون دقائق، أو ساعة، أو يومًا.

الهدفان يؤثران في تكلفة وتعقيد البنية. لا يحتاج كل موقع صغير إلى تعافٍ لحظي، لكن يجب أن يكون القرار واعيًا لا افتراضًا.

لا تختبر Rollback أثناء الكارثة لأول مرة

اكتب خطة واضحة:

  1. كيف نكتشف أن الإصدار يجب التراجع عنه؟
  2. من يملك قرار التراجع؟
  3. ما الأمر أو الإجراء؟
  4. ما الحالة المتوقعة بعده؟
  5. كيف نتعامل مع Migration؟
  6. ماذا نفعل بالرسائل التي أنشأها الإصدار الجديد؟
  7. كيف نتحقق من سلامة البيانات؟
  8. كيف نعيد فتح الحركة؟
  9. وما الأدلة التي تثبت نجاح التعافي؟

ثم نفّذ Drill في بيئة اختبار قريبة من الإنتاج.

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

٧. المراقبة والرصد

المراقبة ليست Console Logs

قد يكتب التطبيق:

console.log("Something went wrong");

لكن عندما تقع المشكلة في الإنتاج، تحتاج إلى معرفة: أي مستخدم أو Tenant تأثر؟ وأي طلب؟ وأي نسخة من التطبيق؟ وما المسار؟ وما الاعتماد الخارجي؟ وكم استغرقت العملية؟ وما الخطأ؟ وهل تكرر؟ وهل بدأت المشكلة بعد نشر معين؟

الاعتمادية تحتاج إلى Observability، لا مجرد طباعة رسائل.

الأعمدة الثلاثة: Logs وMetrics وTraces

١. Logs — سجلات أحداث منفصلة تفيد في التفاصيل والسياق. يجب أن تكون Structured قدر الإمكان:

{
  "level": "error",
  "event": "payment_provider_timeout",
  "request_id": "req-123",
  "payment_id": "pay-456",
  "provider": "example",
  "duration_ms": 3000
}
🚨
لا تسجل

كلمات المرور، أو Tokens، أو مفاتيح API، أو بيانات دفع حساسة، أو نصوص المستخدمين كاملة دون حاجة وسياسة واضحة.

٢. Metrics — قياسات رقمية عبر الزمن: عدد الطلبات، ومعدل الأخطاء، وزمن الاستجابة، وCPU، والذاكرة، واتصالات قاعدة البيانات، وQueue Depth، وعمر أقدم رسالة، وعدد الرسائل في DLQ، وCache Hit Rate، وتكلفة استدعاءات الذكاء الاصطناعي.

٣. Traces — تتبع مسار طلب واحد عبر عدة خدمات:

API
→ Authentication
→ Database
→ Payment Provider
→ Notification Queue

تعرّف OpenTelemetry الإشارات الأساسية بأن Trace يمثل مسار الطلب، وMetric قياسًا أثناء التشغيل، وLog تسجيلًا لحدث. كما تسمح Context Propagation بربط الإشارات عبر حدود الخدمات والعمليات.

Correlation ID: اربط أجزاء القصة

إذا أرسل المستخدم طلبًا واحدًا أدى إلى استعلام قاعدة بيانات، ورسالة Queue، وWorker، واستدعاء API خارجي، وإرسال إشعار — فقد تظهر خمسة Logs منفصلة.

استخدم معرفًا مشتركًا مثل:

trace_id
request_id
operation_id
job_id

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

Logs وحدها لا تخبرك أن هناك مشكلة واسعة

إذا حدث Timeout واحد، قد تجده في Logs. لكن إذا أردت معرفة هل ارتفع معدل Timeout من 0.1% إلى 20% خلال عشر دقائق، تحتاج إلى Metrics. وإذا أردت معرفة أين ضاع وقت هذا الطلب بين خمس خدمات، تحتاج إلى Trace.

لذلك لا تعتبر هذه الإشارات بدائل لبعضها. كل واحدة تجيب عن نوع مختلف من الأسئلة.

المقاييس الذهبية — Golden Signals

يقدم Google SRE أربعة مؤشرات مركزية لمراقبة الخدمات:

Latency — كم يستغرق الطلب؟ ومن المهم التفريق بين الطلبات الناجحة والفاشلة؛ فقد يفشل الطلب بسرعة ولا يعني ذلك أن الأداء جيد.

Traffic — ما حجم الطلب على النظام؟ مثل عدد الطلبات في الثانية، وعدد الرسائل، وعدد الجلسات، وحجم المعاملات.

Errors — كم نسبة الطلبات الفاشلة؟ ولا تقتصر الأخطاء على 500. قد تكون الاستجابة 200 لكن النتيجة غير صحيحة، أو العملية تحولت إلى Fallback.

Saturation — ما مدى اقتراب الموارد من حدودها؟ مثل امتلاء CPU، والذاكرة، وConnection Pool، وQueue، والقرص، وحدود API خارجية.

لا تراقب المتوسط فقط

قد يكون متوسط زمن الاستجابة 200 ms، لكن 5% من المستخدمين ينتظرون عشر ثوانٍ. المتوسط يخفي الأطراف البطيئة.

راقب نسبًا مثل P50 وP95 وP99. إذا كان P95 يساوي ثانيتين، فهذا يعني أن 95% من الطلبات انتهت في ثانيتين أو أقل، بينما كانت 5% أبطأ.

اختر المقاييس التي تعكس تجربة المستخدم، لا الأرقام الأسهل في الجمع.

Alerting: لا توقظ الإنسان لكل خطأ

قد يرسل النظام إنذارًا عند كل Exception. بعد أيام، يمتلئ الفريق بالتنبيهات ويبدأ تجاهلها. يسمى ذلك Alert Fatigue.

يجب أن يكون التنبيه قابلًا للتنفيذ، ومرتبطًا بأثر حقيقي، وله مستوى خطورة واضح، ويحتوي سياقًا، ويوجه إلى Runbook.

قد تسجل مشكلة صغيرة في Dashboard دون إيقاظ أحد. أما إذا كان المستخدمون لا يستطيعون الدفع، أو Queue الحرجة تتراكم بسرعة، أو معدل الأخطاء تجاوز الحد، أو قاعدة البيانات قاربت نفاد الاتصالات، أو فشل النسخ الاحتياطي — فقد تحتاج إلى تدخل فوري.

يركز Google SRE على أن أنظمة المراقبة يجب أن تميز بين ما يستحق مقاطعة الإنسان فورًا وما يمكن متابعته دون Page عاجل.

SLI وSLO وSLA

Service Level Indicator — SLI: المقياس الفعلي للخدمة، مثل نسبة الطلبات الناجحة، وزمن الاستجابة، ونسبة Jobs المكتملة خلال خمس دقائق.

Service Level Objective — SLO: الهدف الذي تريد تحقيقه، مثل «99.9% من طلبات API تنجح خلال الشهر» أو «95% من عمليات إنشاء التقرير تكتمل خلال دقيقتين».

Service Level Agreement — SLA: اتفاق رسمي أو تعاقدي مع العميل، وقد يتضمن تعويضات أو التزامات.

لا تخلط بينها. يمكن أن يكون لديك SLO داخلي أشد من SLA حتى تكتشف التراجع قبل خرق الالتزام الخارجي.

Error Budget

إذا كان SLO هو 99.9%، فأنت تقبل ضمنيًا نسبة صغيرة من الفشل خلال الفترة. هذه المساحة هي Error Budget.

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

يوضح Google SRE أن المطالبة بتحقيق 100% دائمًا غير واقعية وقد تؤدي إلى حلول مكلفة ومفرطة في التحفظ، ويقترح استخدام Error Budget مرتبط بالـSLO.

Site Reliability Engineering — SRE

SRE ليس اسمًا لأداة، ولا يعني فقط مراقبة الخوادم. هو أسلوب يجمع مبادئ هندسة البرمجيات مع تشغيل الأنظمة، ويهتم بأهداف الاعتمادية، والأتمتة، والمراقبة، وإدارة الحوادث، وتقليل العمل اليدوي المتكرر، واختبار التعافي، وقياس المخاطر، والتعلم من الأعطال.

قد لا يحتاج المشروع الصغير إلى فريق SRE مستقل، لكنه يستفيد من مبادئه. اسأل منذ البداية: ما أهم رحلة للمستخدم؟ وكيف نقيس نجاحها؟ وما مستوى التوفر المقبول؟ ومن يتلقى التنبيه؟ وما خطة الاستجابة؟ وكيف نتعلم من الحادث؟ وكيف نمنع تكراره دون إضافة تعقيد غير ضروري؟

٨. تجربة المستخدم عند الفشل

رسائل الخطأ للمستخدم ليست Logs تقنية

عندما يفشل النظام، يحتاج المستخدم إلى رسالة مفيدة:

تعذر إنشاء التقرير حاليًا. احتفظنا بطلبك وسنحاول معالجته مجددًا.

وليس NullReferenceException at ReportService.cs:183.

وفي الوقت نفسه، لا ينبغي أن يكون السجل التقني «حدث خطأ غير متوقع» لأنه لا يساعد الفريق.

قسّم التعامل إلى طبقتين:

رسالة المستخدم: واضحة، وآمنة، ولا تكشف البنية الداخلية، وتشرح ما حدث بالقدر المناسب، وتخبره بما يستطيع فعله.

تقرير المراقبة: نوع الخطأ، وStack Trace، والإصدار، والبيئة، وRequest ID، والاعتماد المتأثر، ومدة العملية، والبيانات غير الحساسة اللازمة للتحقيق.

Loading State جزء من الاعتمادية

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

لذلك حالات التحميل ليست مجرد تجميل. يجب أن توضح أن الطلب بدأ، وما إذا كان يمكن إلغاؤه، وهل يمكن مغادرة الصفحة، وهل العملية ما زالت جارية، وهل انتهت، وهل فشلت ويمكن إعادة المحاولة.

في العمليات الطويلة استخدم حالات مثل:

queued
processing
completed
failed
cancelled

ولا تجعل الواجهة تعتمد فقط على Spinner بلا نهاية.

Offline Handling والاتصالات الضعيفة

قد يفقد المستخدم الاتصال بعد إرسال طلب. لا تفترض أن عدم وصول الرد يعني عدم تنفيذ العملية.

في العمليات الحساسة: استخدم معرفًا للعملية، وتحقق من الحالة عند عودة الاتصال، ولا تنفذ الطلب من جديد بصورة عمياء، واعرض حالة غير مؤكدة عند الحاجة، واستخدم Idempotency.

أما العمليات المحلية الممكنة، فيمكن حفظها مؤقتًا، ومزامنتها لاحقًا، وإظهار حالة Pending، وحل التعارضات وفق قواعد واضحة.

لا تَعِد المستخدم بنجاح عملية لم يؤكدها الخادم.

Error Boundaries وعزل انهيار الواجهة

في تطبيق واجهة كبير، قد يتسبب خطأ في مكوّن واحد في سقوط الصفحة كاملة. يمكن استخدام Error Boundaries أو آليات مشابهة لعزل بعض الأخطاء وعرض بديل مثل «تعذر تحميل قسم الإحصاءات» بينما تبقى بقية الصفحة متاحة.

لكن لا تستخدمها لإخفاء الأخطاء بصمت. يجب أن تسجل الخطأ، وتعرض حالة بديلة، وتتيح إعادة المحاولة عند ملاءمتها، وتحافظ على البيانات التي أدخلها المستخدم قدر الإمكان.

الأداء البصري جزء من تجربة الاعتمادية

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

راجع الصور وأحجامها، وBundle Size، وMemory Leaks، والاشتراكات التي لا تُغلق، وAnimation Performance، والأداء على أجهزة ضعيفة، وScreen Readers، وResponsive Design، والاتصالات البطيئة.

الاعتمادية ليست فقط نسبة تشغيل الخادم، بل قدرة المستخدم على إكمال مهمته.

ماذا يجب أن يحدث عند تعطل مزود خارجي؟

لكل مزود خارجي، اكتب جدولًا مثل:

السؤالالقرار
ما المهلة؟3 ثوانٍ
هل نعيد المحاولة؟مرتان للأخطاء المؤقتة
هل العملية Idempotent؟نعم، بمفتاح العملية
هل يوجد Circuit Breaker؟يفتح بعد حد محدد
ما البديل؟Queue أو مزود احتياطي
ماذا يرى المستخدم؟حالة انتظار واضحة
ماذا نراقب؟الأخطاء والزمن والتكلفة
متى نتوقف؟بعد انتهاء الميزانية الزمنية

لا تترك كل مكتبة تختار إعداداتها الافتراضية دون فهم.

٩. أوامر عملية للمساعد البرمجي

مراجعة الاعتمادية والاستعداد للإنتاج

Act as a senior reliability engineer.

Review this system as a production service, not as a demo.

Do not modify code yet.

Map the full request and background-processing paths, including:

- load balancers,
- application instances,
- databases,
- caches,
- message queues,
- workers,
- object storage,
- scheduled jobs,
- and external APIs.

Identify:

1. Single points of failure.
2. Components that cannot run safely with multiple instances.
3. In-memory or local state that breaks horizontal scaling.
4. Missing health, readiness, and startup checks.
5. Missing graceful shutdown and connection draining.
6. Unbounded concurrency, queues, retries, or resource usage.
7. External calls without timeouts, retry limits, backoff, or circuit breakers.
8. Background jobs that are not idempotent.
9. Poison-message and DLQ handling gaps.
10. Deployment and rollback risks.
11. Missing logs, metrics, traces, alerts, and SLOs.
12. User journeys that fail completely when a non-critical dependency is unavailable.

For every finding, provide:

- exact component and code path,
- realistic failure scenario,
- user and data impact,
- current evidence,
- smallest safe improvement,
- and a test or failure simulation that proves the improvement.

Separate confirmed findings from assumptions.
Do not introduce Kubernetes, microservices, Redis, or new infrastructure
unless the current scale and failure requirements justify them.

مراجعة Queue وWorkers

Review this background-processing workflow for reliability.

Assume:

- messages can be delivered more than once,
- workers can crash at any point,
- processing can exceed the visibility timeout,
- external APIs can fail or rate-limit requests,
- and queue depth can grow faster than consumers can process it.

Verify:

1. Idempotency and duplicate-message protection.
2. Acknowledgement and deletion behavior.
3. Visibility timeout or lease renewal.
4. Retry classification and maximum attempt count.
5. Exponential backoff and jitter.
6. Poison-message handling.
7. Dead-letter queue configuration.
8. DLQ alerting and redrive procedure.
9. Queue size, age, TTL, and overflow limits.
10. Backpressure and admission control.
11. Worker concurrency and external API limits.
12. Graceful worker shutdown.
13. Heartbeats and detection of stuck jobs.
14. Safe replay after deployment or recovery.

Provide concurrent and crash-recovery tests.
Do not claim the workflow is reliable using sequential happy-path tests only.

مراجعة الخدمات الخارجية

Audit every external service dependency.

For each dependency, document:

- purpose,
- timeout,
- allowed retry conditions,
- maximum retries,
- exponential backoff and jitter,
- rate-limit handling,
- circuit-breaker behavior,
- fallback behavior,
- privacy constraints,
- cost limits,
- and user-visible failure state.

Test:

- timeout,
- connection refusal,
- invalid response,
- slow response,
- 429,
- 500,
- partial response,
- and duplicate request execution.

Do not retry permanent validation or authorization failures.
Do not retry side-effecting operations unless they are idempotent.

مراجعة النشر والتراجع

Review the deployment and rollback process as an executable recovery plan.

Inspect:

- application deployment,
- database migrations,
- queues and workers,
- scheduled jobs,
- caches,
- feature flags,
- object storage,
- and external side effects.

Determine whether the previous application version can run safely
after every migration in the release.

Produce:

1. Pre-deployment checks.
2. Deployment sequence.
3. Health and business validation checks.
4. Automatic abort thresholds.
5. Rollback decision criteria.
6. Exact rollback steps.
7. Database compatibility strategy.
8. Handling for messages created by the new version.
9. Backup and restore prerequisites.
10. A non-production rollback drill.

Do not call the plan complete until it has been executed and timed.

مراجعة المراقبة

Design production observability around user journeys.

For each critical journey, define:

- success condition,
- latency target,
- error condition,
- traffic metric,
- saturation risks,
- structured logs,
- trace boundaries,
- correlation identifiers,
- dashboard panels,
- and actionable alerts.

Include metrics for:

- API latency and errors,
- database connections and slow queries,
- cache hit rate,
- queue depth and oldest-message age,
- worker failures,
- DLQ messages,
- external API latency and rate limits,
- deployment version,
- and AI token usage and cost.

Do not create alerts for every exception.
Every paging alert must indicate user impact and link to a response procedure.

١٠. قائمة اختبار الاعتمادية — ٨٠ نقطة

قبل اعتبار المنتج جاهزًا، اطلب الإجابة عن هذه الأسئلة بالأدلة.

البنية والتوسع

  1. ما نقاط الفشل الواحدة؟
  2. ماذا يحدث عند توقف نسخة من التطبيق؟
  3. هل يمكن تشغيل نسختين دون تكرار Jobs أو فقد Sessions؟
  4. هل الملفات محفوظة في مكان مشترك؟
  5. هل التطبيق يحتفظ بحالة أساسية داخل الذاكرة؟
  6. هل يوجد Load Balancer عند الحاجة؟
  7. هل يرسل الحركة إلى النسخ السليمة فقط؟
  8. هل التوسع التلقائي يعتمد على مقياس مفيد؟
  9. هل قاعدة البيانات تتحمل عدد النسخ الأقصى؟
  10. هل توجد حدود واضحة للتكلفة والتوسع؟

فحوص الصحة

  1. هل Startup Check منفصل عن Readiness وLiveness؟
  2. هل Readiness يوقف الطلبات دون قتل النسخة بلا حاجة؟
  3. هل Liveness يكتشف التعليق الحقيقي؟
  4. هل فحص الصحة سريع؟
  5. هل يمكن أن يسقط الاعتماد الخارجي جميع النسخ؟
  6. هل اختُبر فشل كل فحص؟

الإيقاف والنشر

  1. هل تتوقف النسخة عن استقبال عمل جديد قبل إيقافها؟
  2. هل تكمل الطلبات الحالية؟
  3. هل يغلق Worker الرسالة بصورة صحيحة؟
  4. هل تُغلق اتصالات قاعدة البيانات؟
  5. هل يوجد Connection Draining؟
  6. هل الإصداران القديم والجديد متوافقان أثناء Rolling Deployment؟
  7. هل النشر يدعم Canary أو Blue/Green عند الحاجة؟
  8. ما المقاييس التي توقف النشر تلقائيًا؟
  9. هل اختُبر Rollback فعليًا؟
  10. هل يستطيع الإصدار السابق العمل بعد Migration؟

الطوابير والمهام

  1. هل الرسائل Idempotent؟
  2. ماذا يحدث إذا وصلت الرسالة مرتين؟
  3. ماذا يحدث إذا مات Worker في منتصف التنفيذ؟
  4. هل Visibility Timeout مناسب؟
  5. هل توجد Heartbeats للمهام الطويلة؟
  6. كيف تُكتشف المهام العالقة؟
  7. ما الحد الأعلى للمحاولات؟
  8. ما الأخطاء التي يعاد تنفيذها؟
  9. هل توجد DLQ؟
  10. هل يوجد تنبيه عند وصول رسالة إليها؟
  11. هل توجد طريقة آمنة لإعادة المعالجة؟
  12. ماذا يحدث عند امتلاء Queue؟
  13. هل يوجد Backpressure؟
  14. ما عمر أقدم رسالة مقبول؟

الخدمات الخارجية

  1. هل لكل طلب Timeout؟
  2. هل Retry محدود؟
  3. هل يستخدم Backoff وJitter؟
  4. هل العملية آمنة عند التكرار؟
  5. هل يوجد Circuit Breaker؟
  6. ما الذي يراه المستخدم عند توقف المزود؟
  7. هل توجد وظيفة بديلة؟
  8. هل توقف خدمة غير حرجة المنتج كاملًا؟
  9. هل تُراقب حدود المزود وتكاليفه؟
  10. هل اختُبر Fallback فعلًا؟

البيانات والتعافي

  1. ما RPO؟
  2. ما RTO؟
  3. متى أُنشئت آخر نسخة احتياطية؟
  4. هل استُعيدت بنجاح في اختبار؟
  5. كم استغرقت الاستعادة؟
  6. هل تشمل النسخة الأدوار والإعدادات اللازمة؟
  7. من يستطيع الوصول إليها؟
  8. هل هي مشفرة؟
  9. ماذا يحدث للبيانات بين وقت النسخة ووقت الفشل؟
  10. هل خطة التعافي مكتوبة وقابلة للتنفيذ؟

المراقبة

  1. هل توجد Logs منظمة؟
  2. هل تحتوي Correlation IDs؟
  3. هل تمنع تسجيل الأسرار؟
  4. هل توجد Metrics للطلبات والأخطاء والتشبع؟
  5. هل توجد Traces للرحلات المهمة؟
  6. هل نراقب P95 وP99 وليس المتوسط فقط؟
  7. هل نراقب Queue Depth وعمر الرسائل؟
  8. هل نراقب اتصالات قاعدة البيانات؟
  9. هل ترتبط الأخطاء بإصدار النشر؟
  10. هل كل تنبيه عاجل قابل للتنفيذ؟
  11. هل يوجد Dashboard للرحلات الحرجة؟
  12. هل حُدد SLO واضح؟

تجربة المستخدم

  1. هل توجد Loading States؟
  2. هل تمنع الضغط المتكرر؟
  3. هل توضح العمليات الطويلة؟
  4. هل يستطيع المستخدم معرفة حالة Job؟
  5. ماذا يحدث عند انقطاع الاتصال؟
  6. هل يمكن استعادة المدخلات؟
  7. هل يوجد Fallback للمكونات غير الحرجة؟
  8. هل يمنع خطأ مكوّن واحد سقوط الصفحة كلها؟

١١. لا تقبل عبارة: «النظام قابل للتوسع»

كما لم نقبل في الجزء الأول «تم تأمين الميزة»، ولم نقبل في الجزء الثاني «تم تحسين قاعدة البيانات» — لا تقبل في هذا الجزء «النظام قابل للتوسع وجاهز للإنتاج».

اطلب دليلًا.

تقرير ضعيف:

The system uses auto scaling, queues, monitoring, and automatic recovery.
It is production-ready.

تقرير أقوى:

Failure simulation:
- Terminated one of three API instances.
- Load balancer removed it from rotation after readiness failed.
- 0 failed requests were observed during the test.
- P95 latency increased from 210 ms to 320 ms for 40 seconds.

Queue recovery:
- Killed a worker while processing a job.
- Message became visible after the configured timeout.
- Second worker resumed it.
- Idempotency key prevented duplicate database writes.

Deployment rollback:
- Deployed revision 42 to 5% canary traffic.
- Error rate exceeded the 2% abort threshold.
- Traffic returned to revision 41 automatically.
- Database migration was backward-compatible.
- Rollback completed in 74 seconds.

Restore drill:
- Restored the latest backup into an isolated environment.
- Verified row counts and critical business invariants.
- RPO achieved: 5 minutes.
- RTO achieved: 38 minutes.

التقرير الثاني لا يقول فقط إن المكونات موجودة، بل يثبت كيف تصرفت عند الفشل.

١٢. لا تبنِ بنية شركة عالمية لتطبيق لم يبدأ

بعد قراءة كل هذه المفاهيم، قد يشعر مطور الفايب كودنق أنه يحتاج منذ اليوم الأول إلى:

هذا ليس الهدف.

قد يكون تطبيقك الصغير مستقرًا باستخدام:

المبدأ هو:

أضف التعقيد عندما يحل خطرًا أو حاجة مثبتة.

لا تستخدم Kubernetes فقط لأنك تعلمت اسمه.

ولا تضف Queue لعملية تستغرق 200 ms.

ولا تبنِ Leader Election إذا كان قيد فريد في قاعدة البيانات يحل المشكلة.

ولا تشغل عشر نسخ إذا كانت قاعدة البيانات لا تتحمل اتصالاتها.

ولا تدّعِ أنك تحتاج Multi-region قبل أن تعرف RTO وRPO المطلوبين.

💡
الهندسة الناضجة

ليست استخدام أكبر عدد من الأدوات، بل اختيار أبسط بنية تحقق مستوى الاعتمادية الذي يحتاج إليه المنتج.

١٣. الخلاصة: المنتج الحقيقي يعرف كيف يفشل

في النموذج التجريبي، نحتفل عندما تنجح العملية. أما في المنتج الحقيقي، فنسأل أيضًا:

قد يستطيع الذكاء الاصطناعي أن يكتب إعداد Auto Scaling، وQueue، وRetry، وCircuit Breaker، وHealth Check، وDashboard، وخطة Rollback.

لكن وجود هذه الأسماء في المشروع لا يثبت أن النظام موثوق.

⚠️
الاسم ليس دليلًا

قد تكون Health Check سطحية. وقد تكون Retry Loop تزيد العطل. وقد تكون Queue غير محدودة. وقد تكون DLQ بلا مراقبة. وقد يكون Fallback غير مختبر. وقد تعيد الخطة كود التطبيق بينما تترك قاعدة البيانات غير متوافقة. وقد توجد نسخة احتياطية لم يحاول أحد استعادتها.

لهذا يجب أن تسأل:

المنتج الموثوق ليس المنتج الذي يَعِد بألا يفشل، بل المنتج الذي يستطيع أن يقول:

أتوقع الفشل، وأعرف كيف أكتشفه، وأمنع انتشاره، وأتعافى منه بأقل أثر ممكن.

١٤. في الجزء الرابع

أنهينا حتى الآن:

لكن يبقى سؤال مهم:

ماذا عن الكود نفسه؟

قد يكون التطبيق آمنًا وسريعًا، لكنه يحتوي:

في الجزء الرابع سنتحدث عن:

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

📚
المصادر الأساسية

اعتمد هذا الجزء على وثائق Kubernetes الرسمية المتعلقة بالتوسع الأفقي، وفحوص Startup وReadiness وLiveness، ودورة حياة الـPods، والإيقاف المنظم، وعمليات النشر والتراجع وLeader Election؛ وعلى وثائق AWS الرسمية لموازنة الحمل، وAuto Scaling، وMessage Queues، وDLQ، وVisibility Timeout، وعمليات Retry وBackoff وJitter واستراتيجيات النشر؛ وعلى وثائق Microsoft الرسمية لأنماط Circuit Breaker وBulkhead؛ وعلى OpenTelemetry لشرح Logs وMetrics وTraces؛ وعلى كتب Google SRE الرسمية لمراقبة الأنظمة وSLOs وError Budgets؛ وعلى وثائق PostgreSQL للنسخ والاستعادة وPoint-in-Time Recovery.