تخفيف الحمل والضغط العكسي
تخفيف الحمل والضغط العكسي
لكل نظام سقف لطاقته الاستيعابية. المسألة ليست إن كنت ستبلغه، بل ماذا يحدث حين تفعل. الأنظمة التي تفتقر إلى آليات متعمدة لتخفيف الحمل والضغط العكسي تستجيب للحمل الزائد بأسوأ صورة ممكنة: تقبل كل طلب، تستنزف الذاكرة والمعالج، تُضاعف الكمون على كل مستدعٍ، وفي النهاية تنهار — لتستغرق دقائق أو ساعات في الاسترداد. أما الأنظمة المبنية بوعي بالتخفيف فتتعطل بأناقة، وتتخلص أولًا من العمل الأقل تكلفة، وتحمي المسارات الأكثر أهمية، وتتعافى في ثوانٍ بمجرد انحسار الحمل. يتناول هذا الدرس الأنماط والأدوات والحكم الإنتاجي التي تُميّز النتيجتين.
لماذا الحمل الزائد بلا تخفيف يهزم نفسه بنفسه
حين تبلغ خدمة HTTP حالة التشبع، إضافة المزيد من الحمل لا يزيد الإنتاجية بصورة تناسبية — بل يُنهارها. تمتلئ مجمّعات الخيوط، ويرتفع ضغط جمع القمامة، وتتصاعد تنازعات الأقفال، ويتضخم الكمون عند النسبة 99. كل طلب جديد قيد التنفيذ يستهلك ذاكرةً ووقت خيوط كان يمكن توظيفه في خدمة الطلبات القائمة. الاستجابة الصحيحة هي رفض الطلبات الهامشية مبكرًا وبتكلفة منخفضة، مع الحفاظ على الطاقة للعمل المقبول. هذا هو تخفيف الحمل.
الضغط العكسي هو الآلية المتكاملة: بدلًا من إسقاط العمل صمتًا عند الحدود، تُرسل إشارةً للمنتجين المصدريين لإبطاء وتيرتهم. يعالج النمطان طوبولوجيتين مختلفتين — التخفيف جانب الحدود، والضغط العكسي داخل الأنابيب — والأنظمة الإنتاجية تحتاج كليهما.
حدود التزامن كآلية تخفيف أساسية
أكثر آليات تخفيف الحمل موثوقيةً هي الحد الصارم للتزامن — الحد الأقصى لعدد الطلبات المُعالَجة بالتوازي. حين يُبلَغ الحد، تُرفض الطلبات الجديدة بـ 503 Service Unavailable فورًا قبل أن يبدأ أي عمل. تطبّق مكتبة Netflix للحدود التزامنية (المدمجة في resilience4j) وحدود اتصال دائرة القطع في Envoy هذا النموذج. الإدراك المحوري: الكمون تحت الحمل تهيمن عليه عمق الطابور، وحد التزامن الضيق يُبقي الطابور عند الصفر أو قريبًا منه للطلبات المقبولة.
في Kubernetes نقطة التحكم الأساسية هي Envoy (عبر Istio أو كـ sidecar مستقل). يضبط المقتطف التالي حدود الاتصال والطلبات المعلقة لكل pod في خدمة مصدرية:
تحديد المعدل مقابل تحديد التزامن
تحديد المعدل (مثلاً 500 طلب في الثانية لكل مستأجر، مُطبَّق عبر دلو الرموز في Redis أو فلتر التحديد المحلي في Envoy) يحمي من العملاء المتفجرين وإساءة استخدام الحصص. تحديد التزامن يحمي من الحمل الزائد بصرف النظر عن مصدره. يحلّان مشكلتين مختلفتين. على نطاق Google وMeta، تُطبَّق حدود المعدل عند حافة/بوابة API لكل مستدعٍ مُصادَق عليه، بينما تُطبَّق حدود التزامن لكل نسخة خدمة مصغّرة. شغّل كليهما: حدود المعدل للخارج، وحدود التزامن للداخل.
AdaptiveBulkhead في resilience4j هذا النهج. بالنسبة للخدمات الحساسة للكمون، تتفوق الحدود التكيّفية على الثابتة بشكل ملحوظ أثناء الفشل الجزئي.
الضغط العكسي في الأنابيب غير المتزامنة
الضغط العكسي هو عقد المنتج-المستهلك الذي يقول: لا تنتج أسرع مما يستطيع المستهلك معالجته. في HTTP المتزامن يحدث هذا ضمنيًا — المستدعي يتجمد حتى تستجيب. في الأنابيب غير المتزامنة (Kafka، SQS، القنوات الداخلية) يجب هندسته صراحةً.
الضغط العكسي من جانب مستهلك Kafka يُتحكم فيه عبر max.poll.records وحلقة المعالجة: إذا عالج مستهلكك 100 سجل وأسند الإزاحات قبل جلب الدفعة التالية، فإن البروكر يُبطئ التسليم تلقائيًا ليتوافق مع معدل المعالجة. الخطأ الشائع هو المعالجة غير المتزامنة بدون قيد حيث تُفرز goroutines/خيوط لكل سجل — يتراكم التأخير بصمت حتى يصل المستهلك إلى نفاد الذاكرة.
في الأنابيب الداخلية بـ Go/Java، النموذج البدهي للضغط العكسي هو القناة المحدودة أو الطابور المانع. المنتج يتجمد (أو يُخفف) حين تكون القناة ممتلئة. في Go: ch := make(chan Job, 500) — القناة الممتلئة تُوقف المُرسِل، مُنشئةً ضغطًا للأعلى. لا تستخدم أبدًا طوابير غير محدودة في الأنابيب الحساسة للكمون.
التبطيء التدريجي: التدهور الجزئي بدلًا من الفشل الكامل
التبطيء التدريجي هو تدهور جزئي متعمد: تُبقي الخدمة تعمل لكن تُلقي بالميزات غير الأساسية لحماية الوظائف الجوهرية. تشمل استراتيجيات التبطيء الشائعة في الشركات الكبرى:
- أعلام الميزات مع بوابات الحمل: تعطيل لوحات التوصيات وعناصر الإثبات الاجتماعي أو استدعاءات التخصيص حين يتجاوز استخدام المعالج 80% — تُقدّم صفحة مجرّدة لكن وظيفية بدلًا من مهلة انتظار.
- الاحتياطيات الثابتة: إعادة نسخة مؤقتة أو ثابتة من المحتوى (ذاكرة تخزين مؤقت CDN قديمة، لقطة آخر حالة صالحة) حين يكون الأصل محملًا بشكل زائد.
- الطوابير ذات الأولوية: الفرز على مستوى SRE — إسقاط المهام الخلفية (استيعاب التحليلات، التدفئة المؤقتة) قبل تخفيف طلبات المستخدم الأمامية. طوابير Kafka أو SQS منفصلة لكل طبقة أولوية، مع مستهلكين متحيّزين نحو الطابور الأعلى أولوية.
- اتساق منخفض: تقديم القراءات مؤقتًا من نسخة طبق الأصل ببيانات قديمة قليلًا بدلًا من التجمد في الانتظار على الأساسي تحت ضغط الكتابة.
requests_shed_total مقسّم حسب السبب ونقطة النهاية، لا تستطيع التمييز بين "يعمل حسب التصميم" و"نزيف في الإيرادات". أضف الرصد قبل نشر منطق التخفيف.
تطبيق قرار التخفيف في حافة Nginx / HAProxy
للخدمات التي تعمل خلف Nginx، أبسط حماية من الحمل الزائد هي حد الاتصال وعمق طابور الطلبات. يوفر maxconn لكل خلفية في HAProxy مع timeout connect حماية مشابهة عند الطبقة 4. فيما يلي مثال Nginx يحد الاتصالات النشطة بـ 200 لكل upstream، ويُعيد 503 فورًا حين يكتمل طابور 50 طلبًا معلقًا، ويُرسل ترويسةً تتيح للعملاء إعادة المحاولة بعد تأخير مناسب:
مصير الطلب عند كل حد
قياس فاعلية التخفيف
ثلاثة مقاييس تُحدد ما إذا كانت استراتيجية التخفيف تعمل:
- معدل التخفيف (
requests_shed_total / requests_total): يجب أن يرتفع خلال أحداث الحمل الزائد ويعود إلى قرب الصفر بعدها. معدل تخفيف غير صفري في الأحوال الاعتيادية يعني أن طاقتك الطبيعية قريبة جدًا من الحد. - كمون p99 للطلبات المقبولة أثناء التخفيف: يجب أن يبقى ثابتًا. إذا ارتفع p99 أثناء التخفيف، فحد التزامن مرتفع جدًا — اخفضه.
- معدل استهلاك ميزانية الخطأ: أخطاء 503 من التخفيف تُحسب ضمن ميزانية خطأ SLO. اضبط عتبة التخفيف بحيث تبقى الأخطاء ضمن الميزانية خلال أسوأ ارتفاعات الحركة، لا أبعد منها.
قائمة التحقق الإنتاجية
- كل خدمة لديها حد تزامن أقصى مضبوط (Envoy أو Nginx أو middleware التطبيق) — ليس فقط مهلة انتظار.
- قرارات التخفيف تُصدر مقاييس
requests_shed_total{reason,endpoint}إلى Prometheus. - بوابات التبطيء التدريجي مُختبَرة في التدريج تحت حمل مصطنع (k6 أو Gatling) قبل الإنتاج.
- مستهلكو Kafka لديهم
max.poll.recordsمحدودة والإسناد يدوي — لا معالجة غير متزامنة غير محدودة أبدًا. - المستدعون المصدريون يتلقون دلالات إعادة المحاولة القابلة للقراءة آليًا: ترويسة
Retry-Afterعند 503، وتراجع أسي مع تشتيت في مكتبات العميل. - عتبات التخفيف تُراجَع في كل مراجعة طاقة (الدرس 9) — الحدود الثابتة تُصبح قديمة مع تطور أنماط الحركة.