التوسّع وموازنة الأحمال

الخدمات عديمة الحالة مقابل الخدمات ذات الحالة

18 دقيقة الدرس 2 من 10

الخدمات عديمة الحالة مقابل الخدمات ذات الحالة

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

ما معنى "الحالة"؟

الحالة (State) هي أي معلومة يجب أن تستمر بين الطلبات لتلبية الطلبات المستقبلية بشكل صحيح. من أبرز الأمثلة:

  • بيانات جلسة المستخدم المسجّل دخوله (هويته وصلاحياته)
  • محتويات سلة التسوق في منتصف عملية الدفع
  • بيانات ملف مُرفَّع جزئياً في الذاكرة المؤقتة
  • سجل رسائل اتصال WebSocket المفتوح

لا تحتفظ الخدمة عديمة الحالة (Stateless) بأيٍّ من هذه البيانات في ذاكرتها الداخلية. كل طلب وارد يحمل معه كل المعلومات التي يحتاجها الخادم للردّ عليه، أو يجلب الخادم هذه المعلومات من مخزن خارجي مشترك. بمجرد إرسال الاستجابة، يُنسي الخادم كل شيء عن ذلك الطلب.

أما الخدمة ذات الحالة (Stateful) فتحتفظ بالمعلومات بين الطلبات في ذاكرة العملية أو القرص المحلي. الطلبات اللاحقة من نفس العميل يجب أن تصل إلى نفس الخادم، وإلا تعطّلت الخدمة.

لماذا تتوسّع الخدمات عديمة الحالة أفقياً بسهولة؟

تخيّل خادم تطبيقات واحداً يعالج 1,000 طلب في الدقيقة. تضاعفت حركة المرور. مع خدمة عديمة الحالة، الحل ميكانيكي بحت: شغّل خادماً ثانياً متطابقاً، ضع موزّع الحمل أمامهما، ووجّه الطلبات لأيٍّ منهما. لا يعرف أيٌّ من الخادمَين ما يعالجه الآخر — فهما نسختان متبادلتان تماماً.

أما مع خدمة ذات حالة، فهذا الحل مسدود. إذا كان الخادم A يحتفظ بجلسة أليس في ذاكرته، وأرسل موزّع الحمل طلبها التالي إلى الخادم B، فلن يعرف B من هي أليس. ستُسجَّل خارجاً، أو أسوأ من ذلك، ستظهر لها بيانات تالفة.

قاعدة التوسع الأفقي: يمكنك توسيع طبقة ما أفقياً إذا وفقط إذا كان أي خادم فيها قادراً على معالجة أي طلب بالتساوي. التصميم عديم الحالة هو ما يجعل ذلك ممكناً.
Stateless vs Stateful: horizontal scaling comparison Stateless ✓ Scales Easily Client Load Balancer Server A Server B Shared State (Redis / DB) Any request → any server ✓ Stateful ✗ Sticky Problem Client Load Balancer Server A session in RAM Server B no session ✗ broken Request to wrong server = error Must use sticky sessions → hot spots Adding servers is risky ✗
الخوادم عديمة الحالة تشارك الحالة عبر مخزن خارجي فيتمكن أي خادم من معالجة أي طلب. الخوادم ذات الحالة تحبس الجلسات في ذاكرتها مما يُجبر على التوجيه اللاصق ويمنع التوسع الأفقي الحقيقي.

حلّ الجلسات اللاصقة — وسبب فشله

كثيراً ما يلجأ المهندسون الذين يديرون خدمات ذات حالة إلى الجلسات اللاصقة (Sticky Sessions) أو ما يُعرف بتثبيت الجلسة: يُعلّم موزّع الحمل كل عميل بكوكي ويوجّهه دائماً إلى نفس الخادم. يبدو هذا وكأنه توسع، لكنه ينطوي على مشكلات جسيمة:

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

كيف تُخرج الحالة إلى الخارج

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

١. مخزن جلسات مشترك (مثل Redis)

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

٢. المصادقة المبنية على الرمز المميز (مثل JWT)

لا تخزّن أي جلسة على الخادم أصلاً. أصدر للعميل رمزاً موقّعاً تشفيرياً (JWT) يحمل الهوية والصلاحيات مدمجة فيه. يتحقق الخادم من التوقيع في كل طلب — لا يحتاج إلى استعلام قاعدة بيانات ولا مخزن مشترك للمصادقة. عديم الحالة بطبيعة تصميمه.

مقايضة JWT: لا يمكن إلغاء صلاحية الرمز قبل انتهاء مدته. للرموز قصيرة العمر (5–15 دقيقة) مقرونة بآلية رمز تحديث، هذا مقبول عادةً. للجلسات الطويلة التي يجب إلغاؤها فوراً (مثل بعد تغيير كلمة المرور)، مخزن الجلسات المدعوم بـ Redis أكثر أماناً.

٣. التفويض للعميل

بعض الحالات — تفضيلات واجهة المستخدم، عناصر سلة التسوق — يمكن أن تعيش في المتصفح (localStorage، كوكيز، حالة العميل). يصبح الخادم دالة نقية: مدخلات تدخل، مخرجات تخرج، لا ذاكرة بين الاستدعاءات. هذا يناسب حالات العرض الخاصة بالمستخدم وغير الحساسة.

مخطط: إخراج حالة الجلسة إلى Redis

Stateless app tier with shared Redis session store Client Load Balancer App Server 1 App Server 2 App Server 3 Redis Session Store ~1ms read/write Database session_id cookie All servers are identical replicas Add / remove freely → true horizontal scale
ثلاثة خوادم تطبيقات متطابقة تتشارك مخزن جلسات Redis واحد. يستطيع أي خادم معالجة أي طلب لأن بيانات الجلسة تعيش في Redis لا في ذاكرة الخادم.

متى تكون الحالة مقصودة؟

ليست كل الخدمات ذات الحالة أخطاء في التصميم. بعض المكونات ذات حالة بطبيعتها وهي مصمَّمة على هذا الأساس:

  • قواعد البيانات والكاشات — دورها الأصيل هو تخزين الحالة. تتوسع عبر النسخ المتماثل والتجزئة وأنماط القائد-التابع (تُغطَّى في دروس لاحقة).
  • طوابير الرسائل — Kafka وRabbitMQ. تُبقي الرسائل عبر الوسطاء بالتكرار والتقسيم.
  • خوادم WebSocket / البث — الاتصال الحي ذو حالة بطبعه. يُعالَج بتوجيه معرفات الاتصال عبر حافلة نشر/اشتراك (Redis Pub/Sub، Kafka) حتى تتمكن خوادم متعددة من البث لنفس العميل.

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

قائمة عملية: جعل خدمة ما عديمة الحالة

  1. دقّق كل متغير يحتفظ به خادمك بين الطلبات: الجلسات، الكاشات في الذاكرة، أقفال الملفات، عدّادات الاتصالات — كلها حالة.
  2. انقل بيانات الجلسة إلى Redis (أو مخزن جلسات مدعوم بقاعدة بيانات).
  3. استبدل استعلامات هوية المستخدم من الجلسة بالتحقق من JWT، أو احتفظ برموز جلسة رفيعة تُشير إلى مدخلات Redis.
  4. انقل أي كاش في الذاكرة خاص بكل خادم إلى كاش مشترك (Redis، Memcached).
  5. تأكد من أن الملفات المُرفَّعة تذهب إلى تخزين الكائنات (S3، GCS) — لا إلى نظام ملفات الخادم المحلي.
  6. أزل أي مهام خلفية أو مؤقتات تعمل داخل عملية الويب — انقلها إلى عامل طوابير مخصص.
منهجية التطبيق الاثني عشر عاملاً (12factor.net) تُقنّن هذا المبدأ في العامل السادس: العمليات عديمة الحالة ولا تتشارك شيئاً. قراءة العوامل الاثني عشر الأصلية ساعة منتجة لأي مهندس يبني خدمات قابلة للتوسع.

الخلاصة الرئيسية

  • الخدمة عديمة الحالة يمكن توسيعها أفقياً بإضافة نسخ متطابقة — دون أي تنسيق مطلوب.
  • الخدمة ذات الحالة مُقيَّدة: الطلبات يجب أن تصل إلى الخادم الحامل لحالتها، مما يخلق اختناقات وهشاشة.
  • الحل هو إخراج الحالة: Redis للجلسات، JWT للهوية، تخزين الكائنات للملفات، الطوابير للعمل غير المتزامن.
  • البنية التحتية ذات الحالة (قواعد البيانات، الطوابير) مقبولة — فلديها آليات توسع مدمجة. منطق التطبيق ذو الحالة هو المشكلة التي يجب إزالتها.