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

مشروع: استراتيجية القياس التلقائي

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

مشروع: استراتيجية القياس التلقائي

يجمع هذا الدرس الختامي كل مفهوم من مفاهيم البرنامج التعليمي في تصميم متماسك واحد. ستسير خلال العملية الشاملة لبناء استراتيجية قياس تلقائي لحمل عمل متقطع مدفوع بالأحداث — النوع من الأنظمة التي تعمل عند 4,000 طلب/ثانية معظم اليوم، ثم ترتفع إلى 40,000 طلب/ثانية في غضون دقيقتين عند إرسال بريد إلكتروني تسويقي. الهدف ليس تهيئة تجريبية؛ بل هو مخطط إنتاجي بأرقام حقيقية وأنماط فشل حقيقية وقرارات الحكم الأول التي تفصل بين نظام موثوق وآخر يُنبّهك في الساعة الثانية صباحًا.

حمل العمل: تشريح نظام متقطع

نظامنا المرجعي هو واجهة برمجية SaaS متعددة المستأجرين بالملف الشخصي التالي:

  • الخط الأساسي: 4,000 طلب/ثانية، متوسط زمن الاستجابة 45 مللي ثانية، P99 بـ 120 مللي ثانية. استخدام المعالج ~35 % على 20 عقدة m6i.2xlarge (8 vCPU لكل منها = 160 vCPU إجمالًا).
  • نمط الطفرة: رسائل البريد الإلكتروني للحملات التسويقية تصل في أحداث تقويمية محددة مسبقًا — عادةً الساعة 09:00 بالتوقيت المحلي لكل منطقة جغرافية من أصل 4 مناطق. معامل الطفرة 8–12x. المدة: 4–8 دقائق في الذروة، 20–30 دقيقة للعودة إلى الخط الأساسي.
  • الحساسية لزمن الاستجابة الطرفي: مستوى الخدمة P99 أقل من 500 مللي ثانية عند ذروة الطفرة. انتهاكه يؤدي إلى اعتمادات SLA للعملاء.
  • أنواع pods مختلطة: طبقة API عديمة الحالة، وطبقة عمال غير متزامنين (مستهلكو SQS)، ومجموعة Redis تُستخدم لمفاتيح الاتساق. تتوسع كل طبقة بشكل مختلف.
مبدأ التصميم: قبل كتابة أي ملف YAML، نمذج أسوأ الحالات على الورق. الطفرة 10x، تدوم 6 دقائق، ويجب أن تبقى ضمن مستوى الخدمة. هل يمكن لسلسلة القياس التلقائي (توسع pods عبر HPA → توفير عقدة بواسطة Karpenter → جاهزية الـ pod) أن تكتمل خلال أول 90 ثانية؟ إذا لا، فيجب أن تعتمد الاستراتيجية على التوسع الاستباقي بدلًا من التفاعلي.

الطبقة الأولى — HPA لطبقة API

طبقة API عديمة الحالة، مقيّدة بالمعالج، ومجزّأة أفقيًا. يستخدم HPA مقياسًا مركّبًا: معدل الطلبات المخصص لكل pod من Prometheus Adapter بالإضافة إلى المعالج كإجراء احترازي. يُضبط الاستهداف باستخدام 50 % من المعالج بشكل متحفظ لإبقاء هامش كافٍ لاستيعاب الطفرة قبل أن تصبح pods الجديدة جاهزة.

# hpa-api.yaml — HPA إنتاجي لطبقة API # قرارات رئيسية: # targetAverageValue 40 rps/pod (4000 rps أساسي ÷ 100 pod = 40؛ هامش الطفرة مدمج) # scaleUp: 100% في نافذة 30 ثانية — توسع عدواني؛ pods رخيصة # scaleDown: بطيء (stabilizeFor 300s) — تجنب الاهتزاز بعد استنزاف الطفرة apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: api-server namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: api-server minReplicas: 80 # الحد الأدنى = 4000 rps / 50 rps لكل pod؛ لا تنخفض أبدًا maxReplicas: 600 # السقف = 30000 rps / 50 rps لكل pod؛ يطابق حد المجموعة metrics: - type: Pods pods: metric: name: http_requests_per_second target: type: AverageValue averageValue: "50" - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 behavior: scaleUp: stabilizationWindowSeconds: 0 # تفاعل فوري policies: - type: Percent value: 100 # ضاعف عدد pods كل 30 ثانية إذا لزم periodSeconds: 30 - type: Pods value: 100 # أو أضف 100 pod دفعة واحدة، أيهما أكبر periodSeconds: 30 selectPolicy: Max scaleDown: stabilizationWindowSeconds: 300 # انتظر 5 دقائق بعد الطفرة قبل التقليص policies: - type: Percent value: 20 periodSeconds: 60

مع minReplicas: 80، تمتلك المجموعة دائمًا 80 pod مجدولة مسبقًا. عند بدء الطفرة، يُطلق HPA في غضون فترة الاستطلاع الأولى (15 ثانية) ويمكنه مضاعفة عدد pods كل 30 ثانية. من 80 إلى 600 pod يستغرق نحو 4 خطوات توسع — حوالي دقيقتين — إذا كانت العقد دافئة بالفعل. هذا هو سبب أهمية طبقة المجموعة.

الطبقة الثانية — Karpenter للمجموعة

يجب على Karpenter توفير عقد جديدة أسرع مما تستنفد HPA العقد الموجودة. تُستخدم تهيئتان لـ NodePool: مجموعة أساسية من عقد on-demand من نوع m6i.2xlarge (دافئة دائمًا) ومجموعة طفرة من عقد spot من نوع m6i.4xlarge / c6i.4xlarge (تُوفَّر عند الطلب، spot للتوفير في التكلفة، نماذج كبيرة لتقليل زمن توفير العقدة).

# nodepool-burst.yaml — مجموعة طفرة Karpenter لطفرات حركة المرور # عقد Spot: ~70% توفير في التكلفة مقارنة بـ on-demand؛ مقبول لـ API عديمة الحالة # نماذج كبيرة: عقدة m6i.4xlarge واحدة توفر 16 vCPU — تشغيل ~50 pod لكل عقدة # بمعنى 10 عقد جديدة تستوعب 500 pod جديدة؛ Karpenter يمكنه توفير جميعها بالتوازي apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: burst-spot namespace: kube-system spec: disruption: consolidationPolicy: WhenEmpty # دمج العقد الفارغة تمامًا فقط بعد الطفرة consolidateAfter: 5m budgets: - nodes: "30%" # معدل تقليص آمن بعد استنزاف الطفرة limits: cpu: "1280" # 80 x m6i.4xlarge؛ سقف صارم memory: "5120Gi" weight: 10 # أولوية أقل من مجموعة on-demand الأساسية template: metadata: labels: pool: burst-spot spec: taints: - key: pool value: burst-spot effect: NoSchedule # فقط pods متحملة للطفرة تحط هنا requirements: - key: karpenter.sh/capacity-type operator: In values: ["spot"] - key: node.kubernetes.io/instance-type operator: In values: ["m6i.4xlarge", "m6i.8xlarge", "m7i.4xlarge", "c6i.4xlarge"] - key: topology.kubernetes.io/zone operator: In values: ["us-east-1a", "us-east-1b", "us-east-1c"] nodeClassRef: apiVersion: karpenter.k8s.aws/v1 kind: EC2NodeClass name: default kubelet: maxPods: 110

يجب أن يتحمل نشر API تلويث مجموعة الطفرة حتى يتمكن Karpenter من جدولة pods هناك. أضف tolerations إلى مواصفات الـ Deployment: key: pool, operator: Equal, value: burst-spot, effect: NoSchedule. بالإضافة إلى ذلك، استخدم topologySpreadConstraints للتوزيع عبر جميع مناطق التوفر الثلاث — إذا أصابت مقاطعة spot منطقة توفر واحدة خلال الطفرة، فإن pods المتبقية في منطقتي التوفر الأخريين تستوعب حركة المرور دون انتهاك مستوى الخدمة.

Two-Layer Autoscaling: HPA + Karpenter burst pool Traffic 10x burst HPA metric: rps/pod min 80 / max 600 scaleUp: 0s window Baseline NodePool on-demand m6i.2xlarge 20 nodes always warm 160 vCPU baseline Pre-scheduled pods ready Burst NodePool spot m6i/c6i.4xlarge 0 to 80 nodes on demand Karpenter provisions in parallel (~90s) warm pods new nodes Karpenter watches unschedulable pods; provisions fast parallel AZ launch SLO Target P99 < 500 ms at peak
طبقتا القياس التلقائي: HPA يُطلق فورًا على pods الدافئة في المجموعة الأساسية؛ Karpenter يُوفّر عقد spot في مجموعة الطفرة بالتوازي في غضون 90 ثانية.

الطبقة الثالثة — القياس القائم على قائمة الانتظار لطبقة العمال

تعالج طبقة العمال غير المتزامنة الوظائف المُدرجة في قائمة الانتظار من قِبَل API (تغيير حجم الصور، تسليم الـ webhooks، إرسال البريد الإلكتروني). خلال الطفرة، يرتفع عمق قائمة الانتظار قبل أن تلحق طاقة العمال. يُوسّع KEDA العمال مباشرةً من عمق قائمة SQS، وهو إشارة أكثر موثوقية بكثير من المعالج لمستهلكي قوائم الانتظار.

# keda-scaledobject-worker.yaml # توسيع العمال بناءً على عمق قائمة SQS، لا المعالج. # cooldownPeriod 120s: إبقاء العمال نشطين بعد الطفرة لاستنزاف الأعمال المتراكمة # minReplicaCount 5: لا تتوسع إلى الصفر؛ عمليات الإقلاع الباردة تضر الاستجابة للطفرة apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: name: job-worker namespace: production spec: scaleTargetRef: name: job-worker minReplicaCount: 5 maxReplicaCount: 400 cooldownPeriod: 120 pollingInterval: 10 advanced: restoreToOriginalReplicaCount: true horizontalPodAutoscalerConfig: behavior: scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 200 periodSeconds: 15 scaleDown: stabilizationWindowSeconds: 180 triggers: - type: aws-sqs-queue metadata: queueURL: https://sqs.us-east-1.amazonaws.com/123456789012/job-queue queueLength: "20" # الهدف: 20 رسالة قيد المعالجة لكل pod عامل awsRegion: us-east-1 scaleOnInFlight: "true"

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

أهم رؤية للطفرات المدفوعة بالتقويم: القياس التلقائي التفاعلي بطيء جدًا. سقوط البريد الإلكتروني الساعة 09:00 معروف قبل 48 ساعة. وسّع مسبقًا قبل 10 دقائق، قبل وصول حركة المرور، باستخدام CronTrigger من KEDA أو Kubernetes CronJob بسيط يُصحّح minReplicas مباشرةً.

#!/bin/bash # pre-scale.sh — تشغيله كـ Kubernetes CronJob عند 08:50 في كل منطقة زمنية مستهدفة # يرفع حدود minReplicas قبل الطفرة؛ مهمة ثانية تستعيدها في 10:30 set -euo pipefail BURST_MIN_API=300 # 15,000 rps مُوفّرة مسبقًا (300 pod x 50 rps) BURST_MIN_WORKER=100 RESTORE_MIN_API=80 RESTORE_MIN_WORKER=5 ACTION=${1:-"burst"} # "burst" أو "restore" if [[ "$ACTION" == "burst" ]]; then kubectl patch hpa api-server -n production \ --type=merge \ -p "{\"spec\":{\"minReplicas\":${BURST_MIN_API}}}" kubectl patch scaledobject job-worker -n production \ --type=merge \ -p "{\"spec\":{\"minReplicaCount\":${BURST_MIN_WORKER}}}" echo "تطبيق التوسع المسبق: api=${BURST_MIN_API}, worker=${BURST_MIN_WORKER}" else kubectl patch hpa api-server -n production \ --type=merge \ -p "{\"spec\":{\"minReplicas\":${RESTORE_MIN_API}}}" kubectl patch scaledobject job-worker -n production \ --type=merge \ -p "{\"spec\":{\"minReplicaCount\":${RESTORE_MIN_WORKER}}}" echo "تمت الاستعادة: api=${RESTORE_MIN_API}, worker=${RESTORE_MIN_WORKER}" fi
استخدم KEDA CronTrigger بديلًا: يدعم KEDA نوع مشغّل cron الذي يضبط desiredReplicas وفق جدول زمني دون الحاجة إلى نصوص خارجية. هذا أنظف لسير عمل GitOps لأن النية تعيش في مانيفيست ScaledObject، لا في CronJob منفصل.

تحديد الحمل كخط الدفاع الأخير

حتى مع التوسع المسبق المثالي، يمكن أن تستنفد طفرة 20x غير متوقعة أو فشل متعدد مناطق التوفر السعةَ. يجب أن تتضمن الاستراتيجية تحديد الحمل لكي يتدهور النظام بشكل لطيف بدلًا من الانهيار. نفّذ طبقتين:

  • تحديد معدل Nginx عند بوابة الدخول: limit_req_zone مع بدل طفرة لكل مستأجر. المستأجرون في الخطط المجانية يُحدَّدون أولًا؛ الخطط المدفوعة تحصل على ميزانية طفرة أكبر.
  • قاطع دائرة على مستوى التطبيق: عندما يتجاوز عمق قائمة الانتظار الداخلية حدًا معينًا، ترد API بـ 429 Too Many Requests مع رأس Retry-After. هذا أرخص من السماح للطلبات بالانتظار داخل التطبيق وانتهاء مهلتها بخطأ 504.
فخ إنتاجي — القياس التلقائي + مقاطعة spot خلال الطفرة: يمكن استرداد عقد Spot بإشعار دقيقتين في أي وقت، بما في ذلك أثناء الطفرة. خفّف هذا الخطر بـ: (1) استخدام عائلات نماذج متعددة في NodePool من Karpenter حتى يجد AWS دائمًا سعة متاحة؛ (2) ضبط PodDisruptionBudget بـ maxUnavailable: 10% على نشر API حتى لا تُزيل مقاطعة spot أكثر من 10 % من pods في آنٍ واحد؛ (3) تشغيل حد أدنى من عقد on-demand في المجموعة الأساسية يمكنها وحدها تحمل 1x من الحمل — spot مخصص فقط لمعامل الطفرة.

التحقق: اختبار الحمل للاستراتيجية قبل الإنتاج

يجب التحقق من كل استراتيجية قياس تلقائي تحت حمل اصطناعي قبل حدث حقيقي. استخدم k6 لمحاكاة شكل الطفرة: ارتفاع إلى 10x خلال 60 ثانية، احتفاظ بالذروة لمدة 5 دقائق، استنزاف خلال 10 دقائق. راقب وقت استجابة HPA، زمن توفير Karpenter، P99 لزمن الاستجابة، ومعدل الخطأ في Grafana. إذا تجاوز P99 حاجز 400 مللي ثانية خلال مرحلة الارتفاع (قبل انضمام عقد جديدة)، ارفع حد التوسع المسبق أو خفّض استهداف استخدام HPA.

// burst-loadtest.js — سكريبت k6 لمحاكاة طفرة حركة مرور 10x // التشغيل: k6 run --out prometheus=remote_write_url burst-loadtest.js import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '2m', target: 400 }, // الخط الأساسي: 400 VU ~ 4000 rps { duration: '1m', target: 4000 }, // ارتفاع إلى طفرة 10x { duration: '5m', target: 4000 }, // إدامة الطفرة { duration: '2m', target: 400 }, // استنزاف العودة { duration: '3m', target: 400 }, // تأكيد الاستقرار عند الخط الأساسي ], thresholds: { http_req_duration: ['p(99)<500'], // مستوى الخدمة: P99 أقل من 500ms http_req_failed: ['rate<0.001'], // معدل الخطأ أقل من 0.1% }, }; export default function () { const res = http.get('https://api.example.com/v1/healthz', { headers: { 'X-Tenant-ID': `tenant-${Math.floor(Math.random() * 1000)}` }, }); check(res, { 'status 200': (r) => r.status === 200 }); sleep(0.1); }
ملخص الاستراتيجية — القرارات الأربعة: (1) اضبط minReplicas في HPA بشكل مرتفع بما يكفي لكي تستوعب السعة الدافئة أول 60 ثانية قبل أن تصبح عقد جديدة جاهزة. (2) استخدم عقد spot كبيرة في Karpenter لتقليل رحلات توفير العقدة. (3) وسّع مسبقًا للأحداث التقويمية المعروفة — القياس التلقائي التفاعلي وحده بطيء جدًا للطفرات المتوقعة. (4) أضف تحديد الحمل وPodDisruptionBudgets كشبكات أمان للمفاجآت. معًا، تتيح هذه الطبقات الأربع للنظام استيعاب طفرة 10x دون انتهاك مستوى الخدمة.