أحمال عمل Kubernetes وإعدادها

التقارب والتلويث والتحكم في الجدولة

18 دقيقة الدرس 9 من 32

التقارب والتلويث والتحكم في الجدولة

المُجدوِل في Kubernetes هو أحد أكثر مكونات الأنظمة الموزعة تطورًا على الإطلاق. افتراضيًا يضع الـ Pods على أي عقدة تملك CPU وذاكرة كافية — وللكثير من الأعباء الوظيفية، هذا تمامًا ما تحتاجه. لكن كلاسترات الإنتاج على النطاق الواسع تتطلب دقة أكبر: أعباء GPU يجب أن تُنشر على عقد GPU، وPods الواجهة الأمامية ينبغي أن تتوزع عبر مناطق التوافر، ومهمة خط بيانات مرهقة للنظام لا ينبغي أن تشارك عقدة مع واجهة برمجية حساسة للكمون، ومجموعة عقد الـ spot-instance يجب ألا تقبل إلا الأعباء التي تتحمل الانقطاعات صراحةً. يغطي هذا الدرس العناصر الأربعة التي تمنحك تحكمًا دقيقًا ومتعمدًا في أماكن تشغيل الـ Pods.

تقارب العقد (Node Affinity): استهداف خصائص العقدة

تقارب العقد هو التطور عن nodeSelector الأقدم. بينما يدعم nodeSelector تطابق التسميات الحرفية فقط، يدعم تقارب العقد عوامل منطقية (In، NotIn، Exists، DoesNotExist، Gt، Lt) ويميز بين القواعد المطلوبة (قيود صارمة — لن تُجدول الـ Pod إذا لم تُستوفَ) وقواعد التفضيل (قيود ناعمة — يحاول المُجدول احترامها لكنه يضع الـ Pod على أي حال إذا تعذر).

# node-affinity.yaml — عبء GPU يجب أن ينشر على عقدة GPU، # ويُفضَّل نشره على عقد مُسماة zone=us-east-1a apiVersion: apps/v1 kind: Deployment metadata: name: model-inference namespace: ml spec: replicas: 4 selector: matchLabels: app: model-inference template: metadata: labels: app: model-inference spec: affinity: nodeAffinity: # قاعدة صارمة — تبقى Pod في حالة Pending إذا لم توجد عقدة مطابقة requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: accelerator operator: In values: [nvidia-a100, nvidia-h100] # قاعدة ناعمة — تفضيل المنطقة us-east-1a بوزن 80 من 100 preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: topology.kubernetes.io/zone operator: In values: [us-east-1a] - weight: 20 preference: matchExpressions: - key: node.kubernetes.io/instance-type operator: In values: [p4d.24xlarge] containers: - name: inference image: company/model-server:v3.2.1 resources: limits: nvidia.com/gpu: "1" # وضع تسمية على العقدة لتطابق القاعدة الصارمة: kubectl label node gpu-node-01 accelerator=nvidia-a100
IgnoredDuringExecution: كلا نوعي القواعد يحملان اللاحقة IgnoredDuringExecution، مما يعني أن تغييرات تسميات العقد بعد الجدولة لا تُؤدي إلى إخلاء الـ Pods قيد التشغيل. هناك نوع مستقبلي RequiredDuringExecution سيضيف هذه القدرة. في الوقت الحالي، إذا أزلت تسمية من عقدة، تستمر الـ Pods العاملة التي نُشرت استنادًا إلى تلك القاعدة في التشغيل دون أي اضطراب.

تقارب وتنافر الـ Pods: التجميع والفصل

يُجدوِل تقارب الـ Pod وحدةً بالقرب من Pods أخرى تطابق محدد تسميات (أو بعيدًا عنها)، يُقاس ذلك ضمن نطاق طوبولوجي — عادةً عقدة أو منطقة توافر أو رف. هكذا تُطبق نمطين إنتاجيين حيويين: التجميع (ضع ذاكرة التخزين المؤقت على نفس عقدة الـ API لتقليص الكمون) والفصل (انشر النسخ عبر مناطق التوافر حتى لا يقضي على الكل إذا فشلت منطقة واحدة).

# pod-affinity-antiaffinity.yaml # سيناريو: ذاكرة Redis يجب أن تعمل على نفس عقدة الـ API (تجميع)، # ونسخ الـ API يجب ألا تشترك في نفس المنطقة (تنافر لضمان التوفر العالي) apiVersion: apps/v1 kind: Deployment metadata: name: payments-api namespace: production spec: replicas: 3 selector: matchLabels: app: payments-api template: metadata: labels: app: payments-api spec: affinity: podAffinity: # مطلوب: الجدولة بالقرب من pod مُسمى app=payments-cache على نفس العقدة requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: payments-cache topologyKey: kubernetes.io/hostname # "نفس العقدة" podAntiAffinity: # مطلوب: لا نسختان من payments-api في نفس المنطقة requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: payments-api topologyKey: topology.kubernetes.io/zone # "نفس منطقة التوافر" # ناعم: يُفضَّل أيضًا عقد مختلفة ضمن نفس المنطقة preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: payments-api topologyKey: kubernetes.io/hostname containers: - name: api image: company/payments-api:v8.1.0
فخ إنتاجي — تنافر صارم للـ Pod مع عدد مناطق غير كافٍ: إذا ضبطت requiredDuringSchedulingIgnoredDuringExecution لتنافر الـ Pod بنطاق topology.kubernetes.io/zone وكان كلاسترك يحتوي على 3 مناطق لكنك تطلب 4 نسخ، ستبقى النسخة الرابعة في حالة Pending إلى الأبد — لا توجد منطقة رابعة لوضعها فيها. هذا سبب شائع لفشل التوسع الغامض. إما استخدم التنافر الناعم، أو اجعل عدد النسخ لا يتجاوز عدد المناطق المتاحة. تحقق دائمًا عبر kubectl get events --field-selector reason=FailedScheduling.
Pod anti-affinity spreading replicas across availability zones Zone A (us-east-1a) Node: worker-01 Pod: payments-api (replica 1) app=payments-api Node: worker-02 لا يوجد payments-api هنا (التنافر مُستوفى) Zone B (us-east-1b) Node: worker-03 Pod: payments-api (replica 2) app=payments-api Node: worker-04 لا يوجد payments-api هنا (التنافر مُستوفى) Zone C (us-east-1c) Node: worker-05 Pod: payments-api (replica 3) app=payments-api طلب النسخة الرابعة Pending — لا توجد منطقة 4! حدث FailedScheduling التنافر الصارم على مستوى المنطقة: 3 نسخ تنجح عبر 3 مناطق؛ النسخة الرابعة تبقى Pending (لا منطقة رابعة)
تنافر الـ Pod بمفتاح طوبولوجيا المنطقة: النسخ تتوزع عبر مناطق التوافر، لكن النسخة الرابعة تبقى معلقة عند وجود 3 مناطق فقط.

التلويث والتسامح (Taints & Tolerations): إبعاد الـ Pods عن العقد

يعمل التلويث في الاتجاه المعاكس للتقارب. التلويث (Taint) يُوضع على عقدة ويطرد جميع الـ Pods التي لا تحمل تسامحًا (Toleration) مطابقًا. هذه هي الطريقة التي تُنشئ بها مجمعات عقد مخصصة: مجمعات الـ spot-instance، ومجمعات GPU، وعقد الذاكرة العالية، وعقد حدود الامتثال التي ينبغي أن تعمل عليها أعباء بعينها فقط.

كل تلويث له ثلاثية key=value:effect. هناك ثلاثة تأثيرات:

  • NoSchedule: لن تُجدول الـ Pods الجديدة بدون تسامح مطابق على هذه العقدة. الـ Pods الموجودة لا تُخلى.
  • PreferNoSchedule: يحاول المُجدول تجنب وضع الـ Pods هنا لكنه سيفعل ذلك إذا لم يكن هناك خيار آخر. شكل ناعم من NoSchedule.
  • NoExecute: لا تُجدول الـ Pods الجديدة وتُخلى الـ Pods الموجودة بدون تسامح (بعد فترة سماح اختيارية عبر tolerationSeconds).
# --- إدارة التلوينات على العقد --- # إضافة تلويث: الأعباء المتسامحة مع الـ spot فقط تعمل هنا kubectl taint node spot-node-01 node-role=spot:NoSchedule # إضافة تلويث: عقد يجري تصريفها (Kubernetes يفعل ذلك تلقائيًا) kubectl taint node worker-02 node.kubernetes.io/unschedulable:NoSchedule # إزالة تلويث (لاحظ الشرطة في النهاية) kubectl taint node spot-node-01 node-role=spot:NoSchedule- # عرض التلوينات الموجودة kubectl describe node spot-node-01 | grep -A5 Taints # --- ملف Pod مع تسامحات --- apiVersion: apps/v1 kind: Deployment metadata: name: batch-processor namespace: data spec: replicas: 10 selector: matchLabels: app: batch-processor template: metadata: labels: app: batch-processor spec: # تحمل تلويث الـ spot — يمكن لهذه الـ Pod التشغيل على عقد spot tolerations: - key: node-role operator: Equal value: spot effect: NoSchedule # تحمل حالات عقدة غير جاهزة مؤقتة لمدة 60 ثانية # قبل الإخلاء (يُلغي الافتراضي البالغ 300 ثانية) - key: node.kubernetes.io/not-ready operator: Exists effect: NoExecute tolerationSeconds: 60 # مقترن مع تقارب العقدة لإلزام عقد spot (لا مجرد تحملها) affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node-role operator: In values: [spot] containers: - name: processor image: company/batch:v2.0.0
التسامحات تسمح، لا تجذب. التسامح وحده لا يضمن النشر على العقدة المُلوَّثة. يمكن للـ Pod أن تهبط أيضًا على أي عقدة غير مُلوَّثة. لإجبار النشر على مجمع تلويث بعينه، ادمج التسامح مع قاعدة nodeAffinity أو nodeSelector. هذا التوليف — تلويث + تسامح + تقارب — هو النمط الكانوني لمجمعات العقد المخصصة الذي تستخدمه كل كبرى موفري السحابة في عروض Kubernetes المُدارة (EKS, GKE, AKS).

قيود انتشار الطوبولوجيا (Topology Spread Constraints): التوزيع المتساوي

topologySpreadConstraints هو أقوى عناصر الجدولة وأكثرها قابلية للتركيب، أُضيف في إصدارات Kubernetes الحديثة (GA في 1.19). يتيح لك تحديد أقصى انحراف مسموح به — الفرق في عدد الـ Pods بين نطاق الطوبولوجيا الأكثر حملًا والأقل. حيث يُعبِّر التنافر عن قاعدة ثنائية "لا معًا"، يُعبِّر انتشار الطوبولوجيا عن هدف توزيع مستمر.

# topology-spread.yaml # نشر 12 نسخة بأكبر قدر من التوازن عبر المناطق والعقد apiVersion: apps/v1 kind: Deployment metadata: name: frontend namespace: production spec: replicas: 12 selector: matchLabels: app: frontend template: metadata: labels: app: frontend spec: topologySpreadConstraints: # القاعدة 1: أقصى انحراف 1 pod عبر المناطق (صارم) - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule # صارم — يحجب إذا تعذر الاستيفاء labelSelector: matchLabels: app: frontend # القاعدة 2: أقصى انحراف 1 pod عبر العقد ضمن كل منطقة (ناعم) - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: ScheduleAnyway # ناعم — يُحذِّر لكن يمضي labelSelector: matchLabels: app: frontend containers: - name: web image: company/frontend:v5.3.0 # فحص قرارات الجدولة kubectl get pods -n production -o wide -l app=frontend | awk '{print $7}' | sort | uniq -c
قيم whenUnsatisfiable: DoNotSchedule (صارم — تبقى الـ Pod في Pending إذا كان انتشارها سيُخل بالانحراف) وScheduleAnyway (ناعم — يختار المُجدول العقدة التي تُقلص الانحراف إلى الحد الأدنى حتى لو خالفته). في الإنتاج، يجب أن يكون الانتشار على مستوى المنطقة صارمًا (DoNotSchedule) لضمان التوفر العالي، بينما يمكن أن يكون الانتشار على مستوى العقدة ناعمًا (ScheduleAnyway) لتجنب الـ Pending غير الضروري.

التجميع معًا: نمط مجمع عقد إنتاجي

الكلاسترات الحقيقية تُركِّب العناصر الأربعة. كلاستر نموذجي لشركة كبرى يحتوي على مجمعات عقد متعددة: مجمع عام (بدون تلوينات)، ومجمع GPU (مُلوَّث accelerator=true:NoSchedule)، ومجمع spot (مُلوَّث node-role=spot:NoSchedule)، ومجمع نظام (مُلوَّث CriticalAddonsOnly=true:NoSchedule للـ daemonsets الحيوية). كل عبء وظيفي يُعلن بالضبط المجمعات التي يتحملها، وتلك التي يطلبها عبر التقارب، وكيف يريد الانتشار. هذا الفصل يمنع تداخل الأعباء المزعجة، ويُحسِّن التكلفة (العمليات الدفعية على spot، والـ APIs على الـ on-demand)، ويضمن ألا يُخلى البنية التحتية الحيوية بواسطة أعباء المستخدمين.

# التحقق من قرارات الجدولة وتشخيص Pods في حالة Pending kubectl get events -n production --field-selector reason=FailedScheduling --sort-by='.lastTimestamp' # محاكاة الجدولة دون نشر فعلي للـ Pod (تشغيل تجريبي) kubectl apply -f deployment.yaml --dry-run=server # استخدام نقطة نهاية الشرح للمُجدول (ميزة alpha في الكلاسترات الحديثة) kubectl alpha events --for pod/my-pod-abc123 -n production # عرض تسميات العقد (تُستخدم لكتابة قواعد التقارب) kubectl get nodes --show-labels # عزل عقدة (يضيف تلويث Unschedulable، يمنع الـ Pods الجديدة) kubectl cordon worker-03 # تصريف عقدة للصيانة (يُخلي الـ Pods مع احترام PodDisruptionBudgets) kubectl drain worker-03 --ignore-daemonsets --delete-emptydir-data --grace-period=60 # إلغاء عزل العقدة بعد الصيانة kubectl uncordon worker-03
لا تُقيِّد كلاسترك بإفراط. كل قاعدة جدولة صارمة (requiredDuringScheduling، وتلوينات NoSchedule، وقيود طوبولوجيا DoNotSchedule) تُضيِّق مجموعة المواضع الصالحة. تراكم قواعد صارمة متعددة دون عقد كافية لاستيفائها جميعًا في آنٍ واحد يُبقي الـ Pods في حالة Pending إلى أجل غير مسمى — وغالبًا لا يُكتشف ذلك إلا في أثناء ارتفاع حاد في الحركة أو حدث فشل عقدة. فضِّل القواعد الناعمة قدر الإمكان، واختبر دائمًا مزيج قيودك بتوسيع مؤقت للـ Deployment في كلاستر تجريبي ومراقبة أي الـ Pods تصل إلى حالة Running.