تخطيط السعة والتوسع التلقائي

تخفيف الحمل والضغط العكسي

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

تخفيف الحمل والضغط العكسي

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

لماذا الحمل الزائد بلا تخفيف يهزم نفسه بنفسه

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

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

حدود التزامن كآلية تخفيف أساسية

أكثر آليات تخفيف الحمل موثوقيةً هي الحد الصارم للتزامن — الحد الأقصى لعدد الطلبات المُعالَجة بالتوازي. حين يُبلَغ الحد، تُرفض الطلبات الجديدة بـ 503 Service Unavailable فورًا قبل أن يبدأ أي عمل. تطبّق مكتبة Netflix للحدود التزامنية (المدمجة في resilience4j) وحدود اتصال دائرة القطع في Envoy هذا النموذج. الإدراك المحوري: الكمون تحت الحمل تهيمن عليه عمق الطابور، وحد التزامن الضيق يُبقي الطابور عند الصفر أو قريبًا منه للطلبات المقبولة.

قانون Little في التطبيق: L = λ × W. إذا كان متوسط كمون خدمتك W يبلغ 50 ميلي ثانية وتريد تحديد الطلبات النشطة L بـ 200، فبإمكانك استيعاب إنتاجية λ تبلغ 4000 طلب في الثانية. عند 6000 طلب في الثانية يُطلق حد التزامن — تُخفف 33% من الطلبات لكن الـ 67% المقبولة تحصل على نفس الـ 50 ميلي ثانية، لا على خمس ثوانٍ من الانتظار.

في Kubernetes نقطة التحكم الأساسية هي Envoy (عبر Istio أو كـ sidecar مستقل). يضبط المقتطف التالي حدود الاتصال والطلبات المعلقة لكل pod في خدمة مصدرية:

# Istio DestinationRule: حدود التزامن لكل pod apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: payments-svc spec: host: payments-svc.payments.svc.cluster.local trafficPolicy: connectionPool: tcp: maxConnections: 200 http: http1MaxPendingRequests: 100 http2MaxRequests: 200 maxRequestsPerConnection: 50 outlierDetection: consecutive5xxErrors: 5 interval: 10s baseEjectionTime: 30s

تحديد المعدل مقابل تحديد التزامن

تحديد المعدل (مثلاً 500 طلب في الثانية لكل مستأجر، مُطبَّق عبر دلو الرموز في Redis أو فلتر التحديد المحلي في Envoy) يحمي من العملاء المتفجرين وإساءة استخدام الحصص. تحديد التزامن يحمي من الحمل الزائد بصرف النظر عن مصدره. يحلّان مشكلتين مختلفتين. على نطاق Google وMeta، تُطبَّق حدود المعدل عند حافة/بوابة API لكل مستدعٍ مُصادَق عليه، بينما تُطبَّق حدود التزامن لكل نسخة خدمة مصغّرة. شغّل كليهما: حدود المعدل للخارج، وحدود التزامن للداخل.

حدود التزامن التكيّفية: الحدود الثابتة تفشل في الخدمات ذات الكمون المتغير للمصادر العليا (قواعد البيانات، استدعاءات LLM). تقيس خوارزميات Gradient2 من Netflix والخوارزميات المستوحاة من TCP Vegas زمن رحلة الاستجابة، وتخفض حد التزامن ديناميكيًا حين يرتفع الكمون ثم ترفعه حين ينخفض. تطبّق AdaptiveBulkhead في resilience4j هذا النهج. بالنسبة للخدمات الحساسة للكمون، تتفوق الحدود التكيّفية على الثابتة بشكل ملحوظ أثناء الفشل الجزئي.

الضغط العكسي في الأنابيب غير المتزامنة

الضغط العكسي هو عقد المنتج-المستهلك الذي يقول: لا تنتج أسرع مما يستطيع المستهلك معالجته. في HTTP المتزامن يحدث هذا ضمنيًا — المستدعي يتجمد حتى تستجيب. في الأنابيب غير المتزامنة (Kafka، SQS، القنوات الداخلية) يجب هندسته صراحةً.

الضغط العكسي من جانب مستهلك Kafka يُتحكم فيه عبر max.poll.records وحلقة المعالجة: إذا عالج مستهلكك 100 سجل وأسند الإزاحات قبل جلب الدفعة التالية، فإن البروكر يُبطئ التسليم تلقائيًا ليتوافق مع معدل المعالجة. الخطأ الشائع هو المعالجة غير المتزامنة بدون قيد حيث تُفرز goroutines/خيوط لكل سجل — يتراكم التأخير بصمت حتى يصل المستهلك إلى نفاد الذاكرة.

# Kafka consumer: ضغط عكسي محدود عبر max.poll.records + commit يدوي spring.kafka.consumer.max-poll-records=50 spring.kafka.consumer.enable-auto-commit=false spring.kafka.listener.ack-mode=MANUAL_IMMEDIATE spring.kafka.consumer.properties.fetch.max.bytes=5242880 spring.kafka.consumer.properties.max.partition.fetch.bytes=1048576

في الأنابيب الداخلية بـ 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 طلبًا معلقًا، ويُرسل ترويسةً تتيح للعملاء إعادة المحاولة بعد تأخير مناسب:

# nginx.conf — تخفيف تزامن upstream upstream api_backend { server 10.0.1.10:8080; server 10.0.1.11:8080; keepalive 64; } limit_conn_zone $binary_remote_addr zone=per_ip:10m; limit_conn_zone $server_name zone=per_server:10m; server { listen 443 ssl http2; location /api/ { limit_conn per_ip 20; limit_conn per_server 200; limit_conn_status 503; limit_conn_log_level warn; add_header Retry-After 5 always; proxy_pass http://api_backend; proxy_read_timeout 10s; proxy_send_timeout 5s; } }

مصير الطلب عند كل حد

Load Shedding & Backpressure Flow Clients 10k RPS Edge Rate Limit Shed #1 429 / 503 Envoy Sidecar Concurrency Limit Shed #2 DestinationRule App Logic Priority Queue Brownout Gate Feature Flags Downstream DB / Queue Backpressure إشارة الضغط العكسي 429 تخفيف (حد المعدل) 503 تخفيف (التزامن) 200 + ميزات مخفّضة مفتاح الرموز: تدفق الطلبات تخفيف / ضغط عكسي التخفيف عند الحافة أولًا (الأرخص). ثم عند Envoy لكل خدمة. التبطيء التدريجي يحافظ على 200 OK مع تقليص الميزات.
مصير الطلب عند كل حد: تخفيف بتحديد المعدل عند الحافة، تخفيف بتحديد التزامن عند Sidecar، وتبطيء تدريجي في طبقة التطبيق — مع عودة الضغط العكسي من المصدر السفلي.

قياس فاعلية التخفيف

ثلاثة مقاييس تُحدد ما إذا كانت استراتيجية التخفيف تعمل:

  • معدل التخفيف (requests_shed_total / requests_total): يجب أن يرتفع خلال أحداث الحمل الزائد ويعود إلى قرب الصفر بعدها. معدل تخفيف غير صفري في الأحوال الاعتيادية يعني أن طاقتك الطبيعية قريبة جدًا من الحد.
  • كمون p99 للطلبات المقبولة أثناء التخفيف: يجب أن يبقى ثابتًا. إذا ارتفع p99 أثناء التخفيف، فحد التزامن مرتفع جدًا — اخفضه.
  • معدل استهلاك ميزانية الخطأ: أخطاء 503 من التخفيف تُحسب ضمن ميزانية خطأ SLO. اضبط عتبة التخفيف بحيث تبقى الأخطاء ضمن الميزانية خلال أسوأ ارتفاعات الحركة، لا أبعد منها.
مسار النضج لتخفيف الحمل على نطاق واسع: ابدأ بحد تزامن ثابت عند الحافة. أضف تحديد المعدل لكل عميل بعد ذلك. ثم أجرِ الرصد وضبط التزامن التكيّفي. أخيرًا، أضف بوابات التبطيء التدريجي حول الميزات غير الجوهرية مع تكامل أعلام الميزات. كل طبقة تحمي أكثر دون أن تستلزم الطبقة التالية — ابدأ بما تستطيع شحنه هذا الأسبوع.

قائمة التحقق الإنتاجية

  1. كل خدمة لديها حد تزامن أقصى مضبوط (Envoy أو Nginx أو middleware التطبيق) — ليس فقط مهلة انتظار.
  2. قرارات التخفيف تُصدر مقاييس requests_shed_total{reason,endpoint} إلى Prometheus.
  3. بوابات التبطيء التدريجي مُختبَرة في التدريج تحت حمل مصطنع (k6 أو Gatling) قبل الإنتاج.
  4. مستهلكو Kafka لديهم max.poll.records محدودة والإسناد يدوي — لا معالجة غير متزامنة غير محدودة أبدًا.
  5. المستدعون المصدريون يتلقون دلالات إعادة المحاولة القابلة للقراءة آليًا: ترويسة Retry-After عند 503، وتراجع أسي مع تشتيت في مكتبات العميل.
  6. عتبات التخفيف تُراجَع في كل مراجعة طاقة (الدرس 9) — الحدود الثابتة تُصبح قديمة مع تطور أنماط الحركة.