عمليات Kubernetes المتقدمة

التحجيم التلقائي للكتلة وKarpenter

18 دقيقة الدرس 6 من 30

التحجيم التلقائي للكتلة وKarpenter

التحجيم الأفقي للحاويات (HPA) يُوسّع أحمال العمل؛ أما التحجيم التلقائي للكتلة فيُوسّع البنية التحتية ذاتها. حين تمتلئ كل العقد ولا يستطيع Pod جديد الجدولة، تحتاج إلى ظهور طاقة استيعابية بسرعة وبتكلفة مناسبة وبالحجم الصحيح. إتقان هذا الأمر هو الفارق بين منصة تستوعب ارتفاعات حركة المرور بسلاسة وأخرى تُنبّهك الساعة الثالثة صباحاً لأن Pods ظلت في حالة Pending لعشرين دقيقة.

يتناول هذا الدرس جيلين من الفكرة ذاتها: Cluster Autoscaler (CA)، الأداة الكلاسيكية لمشروع Kubernetes، وKarpenter، البديل الحديث المنشأ من AWS (والمنضم الآن إلى CNCF) الذي يُعالج معظم القيود البنيوية في CA.

لماذا تنفد موارد العقد: مشكلة تعبئة الصناديق

تجدول Kubernetes الـ Pods على العقد باستخدام استراتيجية bin-packing: تضع أكبر عدد ممكن من الـ Pods في الطاقة المتاحة. كل Pod يُعلن عن requests (الحد الأدنى المضمون من CPU/الذاكرة) وlimits (السقف الصارم). يجمع المجدول الـ requests لكل عقدة ولن يضع Pod إذا كان سيتجاوز الطاقة الاستيعابية المُخصّصة للعقدة.

في الواقع العملي، لا تُستخدَم العقد بنسبة 100%: عفاريت النظام (kubelet وkube-proxy وعوامل السجلات وإضافات CNI) تستهلك 5–15% من طاقة كل عقدة قبل أن تبدأ أحمال العمل. عقدة بأربعة vCPUs تُتيح عادةً نحو 3.5 vCPU كطاقة قابلة للتخصيص. هذا الفارق يعني أنك دائماً تحتاج عقدًا أكثر مما تقترح الحسابات الخام.

Allocatable مقابل Capacity: نفّذ kubectl describe node <name> وانظر إلى كتلة Allocatable — هذا هو الرقم الذي يستخدمه المجدول. الفارق بين Capacity وAllocatable محجوز لنظام التشغيل ومكونات نظام Kubernetes.

Cluster Autoscaler (CA) — الأسلوب الكلاسيكي

يراقب CA الـ Pods في حالة Pending ويتحقق مما إذا كانت إضافة عقدة من أي Node Group مُهيّأ (ASGs في AWS، أو Managed Instance Groups في GKE، أو VMSS في Azure) ستجعل الـ Pod قابلاً للجدولة. إذا كان الجواب نعم، يرفع العداد المطلوب للمجموعة. كذلك يفحص العقد غير المُستخدمة، وبعد نافذة خمول قابلة للضبط (10 دقائق افتراضياً)، يُخلّيها ويُنهيها.

القيود الرئيسية لـ CA:

  • التفكير المرتبط بـ ASG: يعمل CA على أنواع instance محددة مسبقاً داخل كل ASG. يجب إنشاء ASG لكل عائلة instance — خلط أنواع instances يتطلب مجموعات متعددة وضبطاً دقيقاً للأولويات.
  • بطء التوسعة: يستطلع CA كل 10 ثوانٍ، ثم ينتظر مزوّد السحابة لتوفير عقدة (1–3 دقائق لـ EC2)، ثم تحتاج kubelet للتهيئة (~30–60 ثانية). المجموع: في الغالب 3–5 دقائق من حالة Pending إلى Pod يعمل.
  • محافظة مفرطة في التقليص: لن يُزيل CA عقدة إذا كان أي Pod غير مرتبط بـ controller عليها، أو إذا كان سيُخالف PodDisruptionBudget. هذا آمن لكنه يُبقي العقد الخاملة تعمل أطول من اللازم.
# تثبيت Cluster Autoscaler عبر Helm (مثال EKS) helm repo add autoscaler https://kubernetes.github.io/autoscaler helm repo update helm install cluster-autoscaler autoscaler/cluster-autoscaler \ --namespace kube-system \ --set autoDiscovery.clusterName=my-cluster \ --set awsRegion=us-east-1 \ --set rbac.serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::123456789:role/ClusterAutoscalerRole \ --set extraArgs.balance-similar-node-groups=true \ --set extraArgs.skip-nodes-with-system-pods=false \ --set extraArgs.scale-down-delay-after-add=5m \ --set extraArgs.scale-down-unneeded-time=10m # علّم مجموعات العقد المدعومة بـ ASG لاكتشافها من CA # (يُنجز على ASG في AWS Console أو عبر Terraform): # k8s.io/cluster-autoscaler/enabled = "true" # k8s.io/cluster-autoscaler/my-cluster = "owned"

Karpenter — نموذج مختلف جوهرياً

يتخلى Karpenter (مشروع CNCF في مرحلة الحضانة، بُني أصلاً من AWS) عن مفهوم Node Group كلياً. بدلاً من إدارة ASGs، يستدعي Karpenter واجهات برمجة السحابة مباشرةً لتشغيل نوع instance تحديداً يناسب حمل العمل المُعلّق. هذا يُزيل طبقة ASG الوسيطة ويجعل التوفير أسرع وأكثر كفاءة من حيث التكلفة.

يُقدّم Karpenter مورديْن مُخصّصيْن (CRDs):

  • NodePool — يحل محل Provisioner القديم (قبل الإصدار v0.32). يُعرّف عائلات instances المسموح بها، والمناطق، وأنواع الطاقة (on-demand أو spot)، والـ taints/labels، وميزانيات الإيقاع المتقطع.
  • EC2NodeClass (خاص بـ AWS) — يصف إعداد EC2 الأساسي: عائلة AMI، ومحددات الشبكة الفرعية، ومحددات مجموعة الأمان، وملف تعريف الـ instance، وبيانات المستخدم.
Karpenter provisioning flow vs Cluster Autoscaler Cluster Autoscaler Pending Pod scheduler unschedulable CA polls every 10 s Scale ASG +1 fixed instance type New Node Joins ~3-5 min total Karpenter Pending Pod scheduler unschedulable Karpenter watches event-driven, no poll Direct EC2 API call best-fit instance picked New Node Joins ~60-90 s total CA: ~3-5 دقائق (عبر ASG) مقابل Karpenter: ~60-90 ثانية (API مباشر، instance مثالي الحجم)
أسلوب Karpenter القائم على الأحداث والاستدعاء المباشر لـ EC2 يُوفّر الطاقة بسرعة أكبر بـ 2–4 أضعاف مقارنةً بـ Cluster Autoscaler الكلاسيكي.
# تثبيت Karpenter على EKS (الإصدار v1.x، باستخدام Helm) export KARPENTER_VERSION=1.0.6 export CLUSTER_NAME=my-cluster export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) export AWS_REGION=us-east-1 helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \ --version "${KARPENTER_VERSION}" \ --namespace kube-system \ --set "settings.clusterName=${CLUSTER_NAME}" \ --set "settings.interruptionQueue=${CLUSTER_NAME}" \ --set controller.resources.requests.cpu=1 \ --set controller.resources.requests.memory=1Gi \ --set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=\ arn:aws:iam::${AWS_ACCOUNT_ID}:role/KarpenterControllerRole --- # NodePool — يُعرّف الطاقة المسموح بها apiVersion: karpenter.sh/v1 kind: NodePool metadata: name: default spec: template: metadata: labels: billing-team: platform spec: nodeClassRef: group: karpenter.k8s.aws kind: EC2NodeClass name: default requirements: - key: karpenter.sh/capacity-type operator: In values: ["spot", "on-demand"] - key: kubernetes.io/arch operator: In values: ["amd64", "arm64"] - key: karpenter.k8s.aws/instance-category operator: In values: ["c", "m", "r"] - key: karpenter.k8s.aws/instance-generation operator: Gt values: ["4"] expireAfter: 720h # إعادة تدوير العقد بعد 30 يوماً limits: cpu: "1000" memory: 4000Gi disruption: consolidationPolicy: WhenEmptyOrUnderutilized consolidateAfter: 1m --- # EC2NodeClass — الإعداد الخاص بـ AWS apiVersion: karpenter.k8s.aws/v1 kind: EC2NodeClass metadata: name: default spec: amiFamily: AL2023 subnetSelectorTerms: - tags: karpenter.sh/discovery: my-cluster securityGroupSelectorTerms: - tags: karpenter.sh/discovery: my-cluster instanceProfile: KarpenterNodeInstanceProfile blockDeviceMappings: - deviceName: /dev/xvda ebs: volumeSize: 50Gi volumeType: gp3 encrypted: true

طاقة Spot: توفير 60–90% على الحوسبة

تُقدّم Spot Instances (AWS) / Preemptible VMs (GCP) / Spot VMs (Azure) خصومات كبيرة — أرخص عادةً بنسبة 60–90% من on-demand — مقابل إشعار إنهاء لمدة دقيقتين كحد أقصى. عند استخدامها صحيحاً، تُحدث spot فارقاً كبيراً لأحمال العمل الدُفعية والخدمات عديمة الحالة وحتى بعض الأحمال ذات الحالة مع معالجة مناسبة للإيقاع المتقطع.

قواعد الإنتاج لأحمال العمل الآمنة مع spot:

  1. حدّد دائماً PodDisruptionBudget (PDB) حتى لا يستطيع Karpenter/CA إخلاء عدد كبير جداً من النسخ في وقت واحد.
  2. وزّع الـ Pods عبر عائلات instances متعددة ومناطق توافر متعددة — مجمّعات spot هي لكل نوع instance لكل منطقة توافر؛ التنويع يُقلل بشكل كبير من خطر الإنهاء المتزامن.
  3. تعامل مع SIGTERM بأناقة. يجب أن ينهي تطبيقك الطلبات الجارية ضمن terminationGracePeriodSeconds (يُوصى بـ 60–120 ثانية).
  4. لا تُشغّل المكونات الحرجة الفردية (etcd، CA نفسه، admission webhooks) على spot أبداً.

يتعامل Karpenter مع إنهاء spot عبر قائمة انتظار SQS للإيقاع المتقطع: حين يُرسل EC2 إشعار إنهاء spot، يتلقاه Karpenter من SQS، يُغلق العقدة فوراً، ويبدأ جدولة طاقة بديلة — كل هذا قبل انتهاء نافذة الدقيقتين. هذا يُعطي MTTI أفضل بكثير مقارنةً بأسلوب node-problem-detector الافتراضي.

صيغة تنويع Spot: استخدم على الأقل 5–8 أنواع instances لكل NodePool. يُنتج karpenter.k8s.aws/instance-category: [c, m, r] مجتمعاً مع instance-generation >= 4 عادةً 30–50 نوع instance مؤهل لكل منطقة — أوسع مجمّع spot ممكن، مما يُعظّم توافر الطاقة.

الدمج والتكثيف: القوة الخارقة لـ Karpenter

الدمج هو قدرة Karpenter على إعادة ضبط حجم أسطول العقد باستمرار. حين تكون العقد غير مُستغلة بالكامل، يُحاكي Karpenter ما إذا كانت جميع الـ Pods تستطيع الاستيعاب في عقد أقل (أو أصغر)، ثم يُنفّذ عملية تعبئة الصناديق: يُشغّل عقدة بديلة أرخص، يُخلّي العقد غير الكفؤة، ويترك الـ Pods لإعادة الجدولة. يحدث هذا بشكل مستقل، ضمن قيود PDB الخاصة بك، دون أي تدخل يدوي.

اضبط consolidationPolicy: WhenEmptyOrUnderutilized للدمج الكامل. استخدم WhenEmpty فقط للمستويات الإنتاجية الحساسة حيث تريد تجنب أي إيقاع متقطع طوعي للـ Pods العاملة.

لا تضبط consolidateAfter: 0s في الإنتاج. الدمج العدواني يُطلق دوراناً مستمراً للعقد، مما يُعطّل الـ Pods دون ضرورة وقد يُسبب إخفاقات متسلسلة إذا كانت أحمال عملك لها أوقات بدء بطيئة. قيمة من 1m إلى 5m تُعطي المجدول وقتاً للاستقرار بعد حدث توسعة قبل انطلاق جولة دمج أخرى.

مراقبة المُوسّع التلقائي أثناء العمل

# متابعة سجلات متحكم Karpenter في الوقت الفعلي kubectl logs -n kube-system -l app.kubernetes.io/name=karpenter -f --container controller # عرض الـ Pods المُعلّقة وسبب عدم قابليتها للجدولة kubectl get pods --all-namespaces --field-selector=status.phase=Pending kubectl describe pod <pending-pod> | grep -A 10 Events # سرد العقد المُدارة بواسطة Karpenter وNodePool الخاصة بها kubectl get nodes -l karpenter.sh/nodepool --show-labels # فحص NodeClaim (كائن طلب العقدة الداخلي في Karpenter) kubectl get nodeclaims kubectl describe nodeclaim <name> # التكلفة: عرض annotations العقدة لنوع الـ instance ونوع الطاقة kubectl get node <name> -o json | jq \ \'{instance: .metadata.labels["node.kubernetes.io/instance-type"], capacity: .metadata.labels["karpenter.sh/capacity-type"], zone: .metadata.labels["topology.kubernetes.io/zone"]}\'

اجمع Karpenter مع Kubernetes Metrics Server وHPA للحصول على مكدّس تحجيم تلقائي متكامل: يُوسّع HPA الـ Pods استجابةً لمقاييس CPU/الذاكرة/المخصصة، مما يُسبب Pods مُعلّقة حين تمتلئ العقد، ويُلاحظها Karpenter ويحلها بتشغيل طاقة جديدة. لا يتعارض المتحكمان أبداً — كل منهما يعمل على موارد مختلفة (Pods مقابل Nodes).

CA أم Karpenter؟ على EKS، Karpenter هو المسار الموصى به الآن للكتل الجديدة. يظل CA صالحاً على GKE (حيث GKE Autopilot هو المعادل لـ Karpenter) وAKS. إذا كنت تُهاجر كتلة مُدارة بـ CA إلى Karpenter، شغّلهما معاً بعلامات Node Group غير متداخلة خلال الانتقال، ثم أخلِّ ASGs المُدارة بـ CA وأزلها أخيراً.