Karpenter لا يقوم بزيادة عدد العقد: الأسباب والحلول (2026)
١٦ أغسطس ٢٠٢٦

عندما لا يقوم Karpenter بزيادة عدد العقد (nodes)، تحقق من أربعة احتمالات بالترتيب: أن المجدول Kubernetes لم يحدد أبداً أن الـ pod الخاص بك غير قابل للجدولة، أو أن NodePool أو EC2NodeClass ليس في حالة Ready، أو أنه تم تجاوز حد NodePool، أو أنه لا يوجد نوع instance يلبي متطلبات الـ pod و NodePool معاً.
ملخص
Karpenter ليس متحكماً عاماً لـ "إضافة السعة". توثيقه دقيق بشأن المحفز: "يركز Karpenter على جدولة الـ pods التي حدد المجدول Kubernetes أنها غير قابلة للجدولة."1 كل ما يتبع ذلك يسير وفق سلسلة قصيرة قابلة للفحص — NodePool في حالة Ready، و EC2NodeClass يشير إليه ويكون أيضاً في حالة Ready، وحدود لم يتم الوصول إليها، ونوع instance واحد على الأقل يتوافق عرضه مع تقاطع متطلبات الـ pod و NodePool. أي خلل في أي حلقة سيجعل الـ pods تبقى في حالة Pending دون إضافة عقد جديدة، وأحياناً دون وجود أي شيء في الـ pod يشير إلى السبب. يرشدك هذا الدليل عبر هذه السلسلة بالترتيب الذي يتم حله بشكل أسرع، باستخدام السلوك الموثق لإصدار Karpenter v1.14.2 ملاحظة حول النطاق قبل البدء: karpenter.sh هو موقع التوثيق الخاص بمزود AWS، لذا فإن EC2NodeClass، وسلوك EC2 Fleet، وتفاصيل VPC CNI أدناه مخصصة لـ AWS فقط. أما NodePools و NodeClaims ونموذج الجدولة فهي مشتركة بين جميع مزودي Karpenter.
ما ستتعلمه
- كيف تقرر بسرعة ما إذا كانت مشكلتك تتعلق بـ Karpenter أو
kube-scheduler— ولماذا يكتب كلاهما أحداثFailedSchedulingعلى نفس الـ pod - كيف تفرق بين "Karpenter بطيء" و "Karpenter متوقف"، ولماذا يُتوقع حدوث توقف لبضع ثوانٍ
- كيفية التحقق من جاهزية NodePool و EC2NodeClass، ولماذا يؤدي عدم جاهزية NodeClass إلى تعطيل كل NodePool يشير إليه
- ماذا يحدث عند تجاوز
spec.limits، وكيفية رؤية الاستخدام الحالي - ماذا يعني فعلياً أن
no instance type met the scheduling requirements or had a required offering - لماذا يمكن أن يكون الـ pod "غير متوافق" مع NodePool يبدو متوافقاً معه
- لماذا تتسبب قيود توزيع التوبولوجيا (topology spread constraints) في حالة جمود (deadlock) حتى عندما يمتلك العنقود مناطق (zones) فائضة
- لماذا يقوم Karpenter أحياناً بتشغيل عقد تظل فارغة ويتم حذفها في حلقة مفرغة
- لماذا يمكن لعقدة تم تشغيلها أن تترك الـ pod الخاص بك في حالة Pending أو عالقاً في
ContainerCreating - لماذا لا يؤدي امتلاء ResourceQuota إلى أي زيادة في السعة على الإطلاق
- ما هي المقاييس (metrics) وإعدادات السجلات (logs) التي يجب اللجوء إليها أولاً
هل المشكلة من Karpenter، أم من المجدول Kubernetes؟
ابدأ من هنا، لأن بقاء الـ pod في حالة Pending ليس دليلاً بحد ذاته على أن Karpenter ارتكب خطأً ما. يتفاعل Karpenter مع الـ pods التي أعلن kube-scheduler مسبقاً أنها غير قابلة للجدولة1 — من الناحية العملية، الـ pods التي تحمل حالة PodScheduled مع سبب Unschedulable. إذا لم يتم تحديد الـ pod الخاص بك بهذه الطريقة، فلن يجد Karpenter شيئاً لـ React تجاهه. (من المفيد معرفة ذلك إذا كنت تشغل مجدولاً ثانياً: المجدول الذي يبلغ عن سبب مختلف لن يحفز Karpenter على الإطلاق.)
kubectl describe pod <pod> -n <namespace> | sed -n '/Events/,$p'
اقرأ هذه المخرجات بعناية، لأن هناك متحكمين مختلفين يكتبان أحداث FailedScheduling على نفس الـ pod، ومن السهل قراءة أحدهما والتوقف. يقوم kube-scheduler بكتابة الرسالة المألوفة 0/N nodes are available… التي تشرح سبب رفض كل عقدة موجودة. أما Karpenter فيكتب حدث FailedScheduling الخاص به ليشرح سبب رفضه بناء عقدة جديدة، وذلك في شكل Failed to schedule pod, <reason> — حيث يكون السبب عبارة عن سلسلة نصية مثل nodepool requirements filtered out all available instance types، أو no instance type met the scheduling requirements or had a required offering، أو incompatible requirements، أو did not tolerate taint، أو node limits have been exhausted for nodepool، أو all available instance types exceed limits for nodepool. يضيف العديد من هذه الرسائل تفاصيل مهيكلة بين قوسين — مثل الـ taint الذي لم يتم تحمله، أو الـ NodePool الذي نفدت مساحته — لذا ابحث عن العبارة وليس السطر بالكامل.
الرسالة الثانية هي التي يجب البحث عنها، وهي تظهر بالفعل على الشاشة من الأمر السابق. إذا كانت موجودة، فقد أعطتك الإجابة ويمكنك تخطي معظم ما يلي. الأقسام أدناه مخصصة للحالات التي تكون فيها الرسالة غائبة، أو مقتضبة، أو إذا كنت تريد فهم معناها. (هذه السلاسل النصية داخلية للمتحكم وقد تم إعادة صياغتها عبر الإصدارات، لذا طابق شكل الرسالة بدلاً من النص الدقيق — القائمة أعلاه هي صياغة الإصدار v1.14.)
| ما تراه | الجهة المسؤولة | الخطوة التالية |
|---|---|---|
الـ pod لم يتم إنشاؤه على الإطلاق، أو في حالة Pending لسبب لا يتعلق بالجدولة | Kubernetes admission | تحقق من kubectl get events -n <ns> و kubectl describe replicaset — ثم راجع قسم ResourceQuota |
Failed to schedule pod, … من karpenter | Karpenter، وقد قام بالفعل بتشخيص المشكلة | اقرأ السبب، ثم انتقل إلى القسم المطابق أدناه |
FailedScheduling من default-scheduler فقط، ولا يوجد حدث من Karpenter | Karpenter لم يتخذ أي إجراء — أو لم يتخذ إجراءً بعد؛ راجع قسم التجميع (batching) قبل افتراض الفشل | استمر في تتبع هذه القائمة من الأعلى |
الـ Pod في حالة ContainerCreating على عقدة جديدة | Node bootstrap / CNI | انتقل إلى قسم node-launched-but-still-Pending |
تعمل رسالة المجدول (scheduler) كمواصفات أيضاً: فهي تسرد سبب رفض كل عقدة موجودة — سواء كان taint غير مقبول، أو CPU غير كافٍ، أو selector غير مطابق — ومهمة Karpenter هي بناء عقدة لا تؤدي إلى هذه الرفوضات.
تنبيه واحد بشأن الاعتماد على الأحداث بشكل عام: يتم حذف أحداث Kubernetes تلقائياً بعد مرور --event-ttl الخاص بخادم API، والذي يكون افتراضياً ساعة واحدة، لذا فإن غياب حدث FailedScheduling ليس دليلاً على عدم وجود فشل — خاصة من جانب kube-scheduler. حالة PodScheduled الخاصة بالـ pod هي الإشارة الدائمة، وهي ما يعتمد عليه Karpenter فعلياً. كما يكتب Karpenter أحداثاً على الـ NodePool بالإضافة إلى الـ pod — حيث يظهر تحذير NoCompatibleInstanceTypes على كائن NodePool وليس على الـ pod — لذا تحقق من كليهما:
kubectl describe nodepool <name> | sed -n '/Events/,$p'
هل Karpenter بطيء بدلاً من أن يكون متوقفاً؟
من الجدير باستبعاد هذا الاحتمال مبكراً، لأنه لا يمكن تمييزه عن الفشل إذا كنت تراقب الطرفية (terminal). يقوم Karpenter بتجميع الـ pods المعلقة عمداً قبل توفير الموارد: يقوم BATCH_IDLE_DURATION (الافتراضي 1s) بتمديد نافذة التجميع في كل مرة يصل فيها pod معلق جديد، ويحدد BATCH_MAX_DURATION (الافتراضي 10s) الحد الأقصى للمدة التي يمكن أن يستمر فيها هذا التمديد.3 وصول الـ pods المعلقة بشكل مستمر سيبقي النافذة مفتوحة حتى الحد الأقصى.
لذا، فإن عدم حدوث عملية توسيع (scale-up) بعد خمس ثوانٍ لا يعد دليلاً على أي شيء بعد. انتظر حتى يتجاوز الوقت سقف الدفعة (batch ceiling)، بالإضافة إلى وقت تشغيل EC2 ووقت تسجيل العقدة (node-registration)، قبل أن تستنتج أن Karpenter لا يقوم بتوسيع العقد على الإطلاق.
هل الـ NodePool الخاص بك جاهز فعلاً؟
إن الـ NodePool الذي ليس في حالة جاهزية (Ready) يكون غير مرئي لعملية الجدولة (scheduling)، ولن تذكر أحداث الـ pod نفسها أنه السبب — عليك أن تذهب وتبحث بنفسك. الوثائق واضحة تماماً: "إذا لم يكن الـ NodePool جاهزاً، فلن يتم النظر فيه للجدولة."4
kubectl get nodepool -o wide
kubectl describe nodepool <name> | sed -n '/Conditions/,$p'
يقوم الأمر kubectl get nodepool -o wide بطباعة الوزن، وإجمالي الموارد، و — وهو أمر مفيد للقسم التالي — الـ NodeClass الذي يشير إليه كل pool، لذا اقرأ الأسماء الحقيقية من هناك بدلاً من افتراض أن كل شيء يسمى default.
تحمل الـ NodePools أربع حالات من الشروط:4
| الشرط | المعنى |
|---|---|
NodeClassReady | الـ NodeClass الأساسي جاهز |
ValidationSucceeded | نجح التحقق من صحة NodePool CRD |
NodeRegistrationHealthy | يوضح ما إذا كان هناك خطأ في التكوين يمنع العقد التي تم تشغيلها من التسجيل بنجاح، ويتطلب تحقيقاً يدوياً |
Ready | "شرط المستوى الأعلى الذي يشير إلى ما إذا كان الـ nodePool جاهزاً. لن يكون هذا الشرط صحيحاً حتى تكون جميع الشروط الأخرى في nodePool صحيحة" |
يحتاج الصف الأخير إلى تنبيه، لأن الوثائق تتناقض مع نفسها بشأنه. يقول الجدول أن Ready "لن يكون صحيحاً حتى تكون جميع الشروط الأخرى في nodePool صحيحة،" ولكن الفقرة الموجودة مباشرة أسفل ذلك الجدول تنص على أن NodeRegistrationHealthy هو "معلوماتي ولا يؤثر على شرط Ready في المستوى الأعلى."4 العبارة الثانية هي العبارة الفعالة: يمكن لـ NodePool أن يبلغ عن Ready=True بينما يخبرك NodeRegistrationHealthy=False بشكل منفصل أن خطأ في التكوين يمنع العقد التي تم تشغيلها من التسجيل. اقرأ كلا الشرطين بدلاً من الثقة في Ready وحده.
وهناك الحالة المتدهورة، والتي يسهل الوقوع فيها في عنقود (cluster) جديد أو بعد ترقية Helm فاشلة: "لن يقوم Karpenter بأي شيء إذا لم يكن هناك NodePool واحد على الأقل تم تكوينه."4 نتيجة فارغة للأمر kubectl get nodepool هي تفسير كامل للمشكلة.
هل الـ EC2NodeClass الخاص بك جاهز؟
تحقق من هذا حتى عندما يبدو الـ NodePool جيداً، لأن الـ NodeClass غير الجاهز يعطل الـ NodePools المرتبطة به دون المساس بتكوين الـ NodePool نفسه. وفقاً لوثائق NodeClass: "إذا لم يكن الـ NodeClass جاهزاً، فإن الـ NodePools التي تشير إليه من خلال nodeClassRef الخاصة بها لن يتم النظر فيها للجدولة."5
kubectl get ec2nodeclass
kubectl describe ec2nodeclass <name> | sed -n '/Conditions/,$p'
مجموعة الشروط تفصيلية، وهذا ما يجعلها مفيدة — فكل شرط يحدد خطوة اكتشاف أو تحقق يمكن أن تفشل بشكل مستقل:5
| الحالة | ما تغطيه |
|---|---|
SubnetsReady | تم اكتشاف الشبكات الفرعية (Subnets) |
SecurityGroupsReady | تم اكتشاف مجموعات الأمان (Security Groups) |
InstanceProfileReady | تم اكتشاف ملف تعريف المثيل (Instance Profile) |
AMIsReady | تم اكتشاف صور AMIs |
ValidationSucceeded | نجح التحقق من EC2NodeClass |
PlacementGroupReady | تم اكتشاف مجموعات التوزيع (placement groups) المشار إليها |
CapacityReservationsReady | تم اكتشاف حجوزات السعة المشار إليها — تظهر فقط عند تفعيل ميزة حجز السعة |
Ready | المستوى الأعلى؛ تكون false إذا كانت أي من الحالات أعلاه false |
ليس عليك تخمين أي تبعية تعطلت: عندما تكون Ready هي false، فإن "Message في الحالة تشير إلى التبعية التي لم يتم حلها."5 حالة false في SubnetsReady أو AMIsReady تشير مباشرة إلى شروط الاختيار التي لم تعد علاماتها (tags) تطابق أي شيء — على سبيل المثال، شبكة فرعية تم تغيير علاماتها أثناء عملية نقل VPC.
إذا كنت قد غيرت للتو أذونات IAM ولا يزال NodeClass يبلغ عن تحقق قديم، فإن Karpenter يقوم بتخزين نتائج التحقق مؤقتاً؛ الطريقة الموثقة لفرض التحديث هي إضافة أي تعليق توضيحي (annotation) إلى EC2NodeClass.2
هل وصلت إلى حدود NodePool؟
تعتبر spec.limits نقطة توقف نهائية. على عكس بوابات الجاهزية المذكورة أعلاه، فإن هذه البوابة تعلن عن نفسها على الـ pod — حيث أن رسائل node limits have been exhausted for nodepool و all available instance types exceed limits for nodepool من خطوة الفرز تأتي من هنا، مع تسمية NodePool المتسبب في المشكلة في التفاصيل الهيكلية. إذا ظهرت لك إحدى هذه الرسائل، فأنت تعرف السبب بالفعل وتحتاج فقط إلى الأرقام. السلوك الموثق هو: "إذا تم تجاوز الحد، يتم منع توفير العقد حتى يتم إنهاء بعض العقد."4
kubectl get nodepool -o custom-columns=\
NAME:.metadata.name,LIMITS:.spec.limits,USED:.status.resources,NODES:.status.nodes
الأمر المختصر المذكور في الوثائق لهذا هو kubectl get nodepool -o=jsonpath='{.items[0].status}'،4 وهو أمر جيد لعنقود (cluster) يحتوي على مجمع واحد، ولكنه يقرأ NodePool عشوائياً واحداً ولا يطبع أي حدود للمقارنة بها — وهذا ليس ما تريده في عنقود يحتوي على عدة مجمعات. في كلتا الحالتين، أنت تقارن status.resources — وهي وحدة المعالجة المركزية (CPU) والذاكرة والتخزين المؤقت التي قام المجمع بتوفيرها فعلياً — بالإضافة إلى status.nodes مقابل spec.limits. هناك ملاحظتان هامتان:
limits.nodesمنفصلة عن حدود الموارد و"تقيد الحد الأقصى لعدد العقد أثناء عمليات التوسع أو استبدال الانحراف (drift replacement)."4 يمكن أن يكون المجمع أقل بكثير من حد الـ CPU ومع ذلك يكون مقيداً بعدد العقد.- فرض الحدود ليس فورياً (transactional). تنص الوثائق بوضوح على أن "التحقق من الحدود يتسم بالاتساق النهائي (eventually consistent)، مما قد يؤدي إلى تجاوز الحد أثناء عمليات التوسع السريعة."4 إذا كنت تحاول معرفة سبب تجاوز المجمع حده بقليل، فهذا سلوك متوقع وليس خطأً برمجياً.
إذا لم يتم تعيين spec.limits، فلا يوجد "حد افتراضي لتخصيص الموارد"، ويصبح سقفك هو الحصص (quotas) الخاصة بمزود السحابة لديك بدلاً من ذلك.4 وهذا ينقل نفس العرض إلى وحدة تحكم مختلفة — لذا من الجدير التحقق من حصص vCPU في EC2 قبل استنتاج أن Karpenter هو المخطئ.
بالنسبة لمستخدمي Prometheus، فإن karpenter_nodepools_limit و karpenter_nodepools_usage يكشفان عن جانبي هذه المقارنة، مصنفين حسب اسم nodepool ونوع المورد.6
ماذا يعني "no instance type met the scheduling requirements or had a required offering"؟
هذا السطر في السجلات يعني أن Karpenter قام بمحاكاة عملية التشغيل ولم يجد شيئاً لتشغيله. تقسم الوثائق هذا الأمر إلى فشلين متميزين يظهران في رسالة واحدة.2
النصف الأول — عدم وجود نوع مثيل (instance type) يلبي متطلبات الجدولة — هو مشكلة في الحجم أو التصفية. قد يكون للـ pod طلبات موارد تستلزم حداً أدنى لحجم المثيل، وإذا كان الـ NodePool مقيداً بعائلة وحجم مثيل معينين، فقد لا يتناسب أي شيء. ومن المهم ملاحظة أن "طلبات الموارد من الـ daemonsets تؤخذ في الاعتبار عند تحديد ما إذا كان نوع المثيل متوافقاً مع الـ pod".2 هذه هي التفصيلة التي تفسد الحسابات اليدوية: الـ pod الذي تتناسب طلباته مع نوع مثيل مسموح به نظرياً قد يتم رفضه، لأن الـ DaemonSets التي ستحط على تلك العقدة يتم طرح مواردها أولاً.
النصف الثاني — أو كان لديه عرض مطلوب (required offering) — يتعلق بالتوافر بدلاً من الحجم. يحدد قسم الأسئلة الشائعة في Karpenter "العرض" (offering) بأنه "مزيج من المنطقة ونوع السعة" لنوع مثيل معين.1 إذا كان الـ pod مثبتاً في منطقة توافر (availability zone) واحدة، فيجب أن يكون نوع المثيل موجوداً هناك. تعطي الوثائق المثال النموذجي: pod من نوع StatefulSet مع وحدة تخزين EBS متصلة تغيرت الشبكة الفرعية (subnet) الخاصة بها، مما قد يؤدي إلى وجوده في منطقة توافر مختلفة عن وحدة التخزين التي يحتاجها، مما ينتج عنه خطأ في العرض المطلوب (required-offering error).2
هناك سلوكان للسعة يشكلان ما تراه لاحقاً. يعطي Karpenter الأولوية للسعة reserved، ثم spot، ثم on-demand، مع التراجع إلى الخيار التالي "عادةً في غضون أجزاء من الثانية" عندما يكون النوع ذو الأولوية الأعلى غير متاح.4 وعندما يبلغ الـ Fleet عن سعة غير كافية، فإن "Karpenter يقوم بتخزين هذه النتيجة عبر جميع محاولات توفير سعة EC2 لنوع المثيل والمنطقة هذه لمدة 3 دقائق القادمة".4 هذه النافذة الزمنية (ثلاث دقائق) من المهم معرفتها قبل البدء في تغيير الإعدادات: إذا نجحت إعادة المحاولة بعد بضع دقائق، فقد يكون ما انتهى ببساطة هو حالة عدم توافر مخزنة مؤقتاً وليس أي شيء قمت بإصلاحه.
راقب karpenter_cloudprovider_instance_launch_failures_total، والذي يفصل إخفاقات CreateFleet حسب منطقة التوافر، ومعرف المنطقة، ونوع السعة، وسبب فشل التشغيل.6
هناك متطلب أساسي على مستوى حساب AWS ينطبق على الحسابات الجديدة ويظهر تماماً كمشكلة في السعة: ما لم يكن الحساب قد تم تفعيله بالفعل لـ EC2 Spot، فإن الدور المرتبط بالخدمة (service-linked role) الخاص بـ spot لن يكون موجوداً، وسيفشل كل تشغيل لـ spot مع AuthFailure.ServiceLinkedRoleCreationNotPermitted. الحل هو أمر واحد، aws iam create-service-linked-role --aws-service-name spot.amazonaws.com.2 في NodePool مقتصر على spot، لا يوجد تراجع (fallback) إلى on-demand لإخفاء هذه المشكلة.
لماذا يعتبر الـ pod الخاص بي "غير متوافق" مع NodePool يبدو متوافقاً؟
التوافق هو نقطة تقاطع، وليس مجرد تشابه. يتم اختيار العقد (Nodes) باستخدام متطلبات كل من NodePool والـ pod، وتوضح وثائق NodePool النتيجة بصراحة: "إذا لم يكن هناك تداخل، فلن يتم تشغيل العقد."4 أو كما ورد في نفس الصفحة، يجب أن تكون متطلبات الـ pod ضمن متطلبات NodePool. هناك ثلاثة أشياء تضيق هذا التقاطع — ثم فشل رابع يرتدي نفس المظهر دون أن يكون مشكلة تقاطع على الإطلاق.
الـ Taints. "إذا واجه Karpenter تلوثاً (taint) في NodePool لا يتحمله الـ Pod، فلن يستخدم Karpenter هذا الـ NodePool لتوفير الـ pod."4 يتم استبعاد هذا الـ pool بهدوء من الاعتبار لهذا الـ pod؛ وإذا لم يتمكن أي pool آخر من استيعابه أيضاً، فإن حدث Failed to schedule pod الخاص بالـ pod ينتهي بـ did not tolerate taint مع ذكر الـ taint المسبب للمشكلة في التفاصيل الهيكلية.
للتأكيد والإصلاح، اقرأ الـ taints الخاصة بالـ pool ثم قرر أي جانب يجب تغييره:
kubectl get nodepool <name> -o jsonpath='{.spec.template.spec.taints}'
إما إضافة إدخال tolerations مطابق إلى مواصفات الـ pod، أو تخفيف spec.template.spec.taints في NodePool. يعتمد الخيار الصحيح على القصد: الـ taints في NodePool موجودة لحجز تلك السعة لأحمال عمل معينة — مثال الوثائق نفسها يضع taint لـ pool الخاص بـ GPU بحيث "لكي يعمل pod على عقدة معرفة في هذا الـ NodePool، يجب أن يتحمل nvidia.com/gpu في مواصفات الـ pod الخاصة به."4 إذا كان الحجز متعمداً، فاجعل الـ pod يتحمله. إذا كان الـ taint زائداً عن الحاجة، فقم بإزالته من الـ pool — مع تذكر أن taints هي إحدى حقول spec.template التي تغذي الـ drift hash، لذا فإن تعديلها يؤدي إلى إعادة تشغيل العقد الموجودة في ذلك الـ pool.
المتطلبات التي تستبعد nodeSelector الخاص بالـ pod. تعطي الوثائق مثالاً مباشراً: إذا طلب pod نوع مثيل (instance type) عبر nodeSelector وكان هذا النوع غير موجود في متطلبات نوع المثيل الخاصة بـ NodePool، فإن "Karpenter لن ينشئ عقدة أو يجدول الـ pod."4
minValues تحت السياسة الافتراضية. إذا حدد أحد المتطلبات minValues ولم يتمكن Karpenter من تلبية هذا الحد الأدنى من المرونة، فإن السلوك يعتمد على --min-values-policy (أو MIN_VALUES_POLICY): تحت خيار Strict، تفشل حلقة الجدولة لـ NodePool هذا، مما يؤدي إلى الرجوع إلى NodePool آخر أو فشل الـ pod تماماً؛ أما تحت خيار BestEffort، فإنه يقوم بتخفيف minValues حتى يمكن تلبيتها.4 الفخ هنا هو أن Strict هو الخيار الافتراضي،3 لذا فإن هذا ينطبق عليك سواء قمت بتكوينه أم لا — حيث أن minValues صارم للغاية قد يكون وسيلة صامتة لاستبعاد الـ pool الوحيد المتاح لديك. لاحظ النتيجة المترتبة على ذلك: karpenter_nodeclaims_created_total يحمل تسمية (label) لـ "ما إذا كان قد تم تخفيف القيم الدنيا لهذا الـ nodeclaim"،6 ولكن تحت خيار Strict لا يحدث التخفيف أبداً، لذا فإن هذه التسمية تعطي إشارة فقط بمجرد تحولك إلى BestEffort.
كتالوج أنواع مثيلات (instance-type) فارغ. هذه المشكلة ليست مشكلة pod مقابل NodePool على الإطلاق: يمكن لمتطلبات NodePool نفسها أن تستبعد كل نوع مثيل يقدمه المزود، وذلك عن طريق تكديس قيود مثل instance-family و instance-generation و instance-category و capacity-type حتى لا يوجد شيء يلبيها جميعاً في وقت واحد. يقوم Karpenter بالإبلاغ عن ذلك لكل NodePool، كحدث تحذيري NoCompatibleInstanceTypes تكون رسالته "NodePool requirements filtered out all compatible available instance types" — أو، عندما يكون minValues هو السبب، تظهر نفس الرسالة مع إضافة "due to minValues incompatibility". ومن المهم ملاحظة أن هذا الحدث مرتبط بكائن NodePool، وليس بالـ pod، لذا فإن الكائن الذي يرتبط به هو ما يحدد أي pool عاد فارغاً.
هذا الارتباط هو السبب في سهولة إغفال هذا السبب. لا يلتقط الـ pod سبباً مطابقاً لـ nodepool requirements filtered out all available instance types إلا عندما يتم تصفية كل NodePool حتى لا يتبقى شيء. في العنقود (cluster) الذي يحتوي على عدة pools، يمكن أن يفرغ pool واحد تماماً بينما لا يزال الآخر يعمل، ولن تقول أحداث الـ pod أي شيء عن ذلك على الإطلاق — الأثر الوحيد هو التحذير الموجود على NodePool. تحقق من kubectl describe nodepool لكل pool تتوقع أن يخدم عبء العمل، وليس فقط الـ pod.
عندما يتطابق عدة pools، تحرص الوثائق على قول "لا يوجد ضمان للترتيب"،1 بينما تذكر أيضاً أن Karpenter يستخدم NodePool صاحب أعلى weight إذا تطابق أكثر من واحد.4 بقراءتهما معاً: weight هو الفيصل الموثق عندما تختلف الأوزان، وحينما لا تختلف، ترفض الوثائق تقديم أي وعود بشأن أي pool سيفوز. يقوم التنفيذ بترتيب الـ pools ذات الأوزان المتساوية بشكل حتمي، ولكن هذا ليس عقداً موثقاً — وهذا هو السبب في أن الوثائق توصي بجعل NodePools حصرية متبادلاً بدلاً من الاعتماد على ترتيب الاختيار على الإطلاق.4 لتثبيت عبء عمل بشكل متعمد، استخدم node selector karpenter.sh/nodepool: my-nodepool.1
هناك أمر واحد ليس عدم توافق في وقت الجدولة، رغم أنه يبدو كذلك: وهو الحد الأقصى "بحد 100 على العدد الإجمالي للمتطلبات في كل من NodePool و NodeClaim"، والتي يتم احتساب spec.template.metadata.labels ضمنها أيضاً.4 يتم فرض هذا السقف بواسطة مخطط CRD، لذا ستواجهه كعملية kubectl apply مرفوضة أو فشل في إنشاء NodeClaim — وليس كـ NodePool يعمل ويرفض الـ pod الخاص بك بهدوء.
لماذا لا يقوم Karpenter بالتوسع من أجل قيود توزيع التوبولوجيا (topology spread constraint) الخاصة بي؟
لأن الـ pods لا ترث متطلبات الـ NodePools التي يمكن أن تخدمها. يستمد Karpenter النطاقات المؤهلة للـ pod من متطلبات الـ pod نفسه، وليس من الـ pool الذي سيخدمه فعلياً.2 بقراءة المثال الموثق بدقة، فإن الكون (universe) الذي تُسحب منه هذه النطاقات يشمل كل NodePool في العنقود (cluster) — بالإضافة إلى المناطق (zones) الممثلة بالفعل في العقد الحالية — بدلاً من الاكتفاء بالـ pool المطابق فقط.
يظهر هذا المأزق (deadlock) كلما كان ذلك الكون أوسع من مجموعة المناطق التي يمكن للـ pool الذي يخدم الـ pod الوصول إليها فعلياً. ينتج المثال الموثق هذا المأزق باستخدام اثنين من الـ NodePools: واحد مرن default مع topology.Kubernetes.io/zone: Exists، قادر على التشغيل في جميع مناطق التوفر الثلاث، وإلى جانبه np-zonal-constraint، المقيد بمنطقتين. إن Deployment يحتوي على nodeSelector لا يحققه سوى np-zonal-constraint، ومع topologySpreadConstraint منطقي ولكن بدون nodeAffinity منطقي، يرى جميع المناطق الثلاث كنطاقات مؤهلة — لأن الـ pool الافتراضي default وضع المنطقة الثالثة في الكون — بينما الـ pool الوحيد الذي يمكنه خدمته يغطي منطقتين فقط.2
تكون النتيجة ذات شكل مميز: يتم تشغيل أول نسختين (replicas) بشكل جيد، أما الثالثة فلا يتم توفيرها أبداً، لأن Karpenter "لا يمكنه توفير سعة في النطاق الثالث".2 الحل هو جعل رؤية الـ pod متطابقة مع رؤية الـ NodePool الذي يخدمه عن طريق إضافة nodeAffinity منطقي مطابق إلى مواصفات الـ pod، أو توسيع الـ NodePool.2
هذا الأمر يستحق الاستيعاب لأن القراءة السطحية — "الـ NodePool الخاص بي يفتقد منطقة" — ليست هي سبب الفشل بحد ذاتها. فالـ pool المقيد في عنقود لا يوجد فيه أي شيء آخر في تلك المنطقة الثالثة لا يساهم بنطاق ثالث، وبالتالي لا يحدث مأزق. يجب أن يكون هناك شيء آخر يضع تلك المنطقة في الكون. عندما تواجه هذا، ابحث عن السبب: NodePool آخر يصل إلى المنطقة التي لا يستطيع الـ pool الخاص بك الوصول إليها، أو عقد موجودة بالفعل هناك — مثل managed node group على سبيل المثال — بما أن المناطق الممثلة في العقد الحالية تُحتسب أيضاً.
هناك شكوى ذات صلة ولكنها مختلفة وهي قيود التوزيع التي يتم توفيرها بشكل صحيح ولكن يتم توزيعها بشكل خاطئ. هذا سلوك خاص بـ kube-scheduler: إذا قام Karpenter بتشغيل عقد يمكن لكل منها استيعاب أكثر من العدد المطلوب من الـ pods وأصبحت جاهزة (Ready) في أوقات مختلفة، فقد يقوم المجدول بملء العقدة الأولى الجاهزة بشكل زائد. الحل المفضل الموثق هو حقل minDomains في topologySpreadConstraints، "المفعل افتراضياً بدءاً من Kubernetes 1.27".1
لماذا يستمر Karpenter في تشغيل عقد تظل فارغة؟
هذه هي حلقة تلوث التشغيل (startup-taint loop)، وهي تكلف أموالاً حقيقية طالما استمرت في العمل. يقوم شيء ما — سواء كان DaemonSet، أو سكربت userData، أو وكيل شبكات — بتطبيق تلوث (taint) بعد تجهيز العقدة (node). يرى Karpenter بعد ذلك أن الـ pod المعلق لا يمكن جدولته على العقدة التي أطلقها للتو، فيقوم بتجهيز عقدة أخرى.1
عادةً ما يتم مسح التلوث ويتم استرداد العقدة الإضافية عن طريق الدمج (consolidation). ولكن إذا لم يتم مسحه بسرعة كافية، تصف الوثائق النتيجة المرضية مباشرة: "حلقة لا نهائية من تجهيز العقد ودمجها دون أن يتم جدولة الـ pod المعلق أبداً."1 وتضع وثائق NodePool نفس التحذير من الاتجاه الآخر — "الفشل في توفير startupTaints دقيقة يمكن أن يؤدي إلى قيام Karpenter بتجهيز عقد جديدة باستمرار."4
أولاً، ابحث عن التلوث الذي يتم تطبيقه فعلياً، بدلاً من التخمين:
kubectl get node <looping-node> -o jsonpath='{.spec.taints}'
ثم قم بالتصريح عنه في startupTaints في NodePool التي تسبب الحلقة. هذه هي الخطوة التي يجب تنفيذها بشكل صحيح: إضافة NodePool جديدة ومنفصلة مع startupTaints الصحيحة لن يساعد، لأن الـ pool الأصلية لا تزال موجودة ولا تزال تطابق الـ pod، لذا يمكن أن يستمر اختيارها والاستمرار في الحلقة. قم بتعديل الـ pool المسببة للمشكلة.
هناك تفصيلان يحددان ما إذا كان هذا سينجح. يتم مطابقة تلوثات التشغيل بناءً على المفتاح (key) والتأثير (effect)، لذا يجب أن يتطابق التأثير الذي تصرح به مع التأثير المطبق فعلياً — التصريح بـ NoExecute مقابل تلوث تم تطبيقه كـ NoSchedule لن يتطابق، وستبقى في الحلقة. (وثائق Karpenter نفسها مثال حي على هذا الارتباك: صفحة NodePools تستخدم NoExecute لتلوث Cilium بينما تستخدم الأسئلة الشائعة FAQ NoSchedule لنفس التلوث.41) كما أن startupTaints هي إحدى حقول spec.template التي تغذي هاش الانحراف (drift hash) الخاص بـ Karpenter، لذا فإن تعديلها في NodePool نشطة يؤدي إلى انحراف كل NodeClaim تملكه تلك الـ pool — توقع استبدالاً تدريجياً لعقدها كثمن للإصلاح. (لا تتصرف كل الحقول بهذه الطريقة: يتم التعامل مع تغيير requirements كحالة خاصة ولا يؤدي بمفرده إلى انحراف العقد التي تظل قيمها الحالية متوافقة.7)
إليك NodePool كاملة وقابلة للتطبيق — لاحظ أن nodeClassRef و requirements مطلوبان في CRD، لذا فإن الجزء الموضح في الوثائق سيتم رفضه إذا تم استخدامه بمفرده:4
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
startupTaints:
- key: node.cilium.io/agent-not-ready
value: "true"
effect: NoExecute
يطبق Karpenter تلوثات التشغيل على العقد التي يجهزها، ولكنه لا يتطلب من الـ pods تحملها (tolerate) — فهو يعاملها كشيء مؤقت ويتوقع من نظام آخر إزالتها.4 وهذا الاستنتاج مهم للقسم التالي: لا تعتبر العقدة مهيأة حتى يتم إزالة كل تلوث تشغيل مصرح به في .spec.template.spec.startupTaints من .spec.taints الخاصة بالعقدة.2
قام Karpenter بإطلاق عقدة — لماذا لا يزال الـ pod الخاص بي في حالة Pending؟
في هذه المرحلة، يكون الـ autoscaler قد أدى مهمته وانتقلت المشكلة إلى مرحلة الـ node bootstrap أو الشبكات. لا تحاول استنتاج ذلك من الصفر — فحالات الحالة (status conditions) الخاصة بـ NodeClaim تحدد الخطوة المعطلة، مع دمج الـ taint أو المورد المسبب للمشكلة في الرسالة:
kubectl get nodeclaim
kubectl get nodeclaim <name> -o jsonpath='{.status.conditions}'
هناك حالتان مهمتان هنا وتفشلان لأسباب مختلفة. Registered تغطي الفشل المبكر: حيث تم تشغيل الـ instance ولكنها لم تنضم إلى الـ cluster على الإطلاق. توضح وثائق استكشاف الأخطاء وإصلاحها ثلاثة أسباب قد تؤدي إلى فشل انضمام الـ node — الصلاحيات، ومجموعات الأمان (security groups)، والشبكات — وتغطي التشخيص تحت بند Node NotReady، بما في ذلك كيفية سحب سجلات kubelet عبر SSM.2 أما Initialized فتغطي ما يحدث بعد الانضمام.
بالنسبة لعملية الـ initialization تحديداً، تذكر الوثائق ثلاثة عوامل: أن تكون حالة Ready الخاصة بالـ node هي True، وأن تكون جميع الموارد المتوقعة قد سجلت كمية غير صفرية في .status.allocatable، وأن تكون الـ startup taints الخاصة بـ NodePool قد تمت إزالتها.2 هناك موردان يشيع فشلهما في التسجيل: nvidia.com/gpu، عندما يتم تشغيل instance مزودة بـ GPU ولكن الـ device-plugin DaemonSet الذي يعلن عن المورد يكون غائباً، و vpc.amazonaws.com/pod-eni، عندما يتوقعه Karpenter ولكن ENABLE_POD_ENI تكون false في VPC CNI.2 اقرأ الحالة بدلاً من مراجعة القائمة يدوياً — فحقل reason يخبرك أي منها تسبب في المشكلة.
إذا انتقل الـ pod إلى حالة ContainerCreating بدلاً من Pending، فافحص تخصيص الـ IP أولاً — حيث يظهر خطأ aws-cni كـ failed to assign an IP address to container.2 هناك سببان موثقان:
maxPodsيتجاوز كثافة الـ pods المدعومة للـ instance. إذا كانmaxPodsالذي قمت بتكوينه في إعدادات kubelet الخاصة بـ EC2NodeClass أكبر من عناوين IP التي يدعمها نوع الـ instance، فلن يتمكن CNI من تخصيص عنوان. الحلول الموثقة هي تفعيل prefix delegation، أو تقليلmaxPods، أو إزالةmaxPodsتماماً للعودة إلى إعدادات Karpenter و EKS AMI الافتراضية، أو تعيينRESERVED_ENIS=1عند استخدام Security Groups for Pods.2- نفاد عناوين IP في الـ Subnet. يقوم EC2 بتشغيل الـ instance بنجاح لأن الـ subnet لديها مساحة لـ ENI، ولكن لا توجد عناوين IP متبقية للـ pods. الحلول الموثقة: استخدام topology spread على
topology.Kubernetes.io/zone، أو استخدام subnet CIDR أكبر، أو custom networking، أو cluster يدعم IPv6.2
هناك حالة أخرى تبدو وكأنها تعليق (hang) ولكنها ليست كذلك: مع Security Groups for Pods، يمكن للـ pods التي تطلب vpc.amazonaws.com/pod-eni أن تظل في حالة ContainerCreating "لمدة تصل إلى 30 دقيقة قبل الانتقال إلى حالة Running،" وهو تفاعل مع amazon-vpc-resource-controller. الحل الموثق هو استخدام تسمية (label) vpc.amazonaws.com/has-trunk-attached: "false" على الـ NodePool، مع قصر متطلبات نوع الـ instance على الأنواع التي تدعم ENI trunking.2
تضيف أحجام التخزين المستمرة (Persistent volumes) فشلاً خاصاً بها في عملية التوسيع (scale-up). لا يدعم Karpenter إضافات التخزين المدمجة (in-tree storage plugins)، لذا فإن StorageClass التي يتم توفيرها بواسطة شيء مثل Kubernetes.io/aws-ebs تجعل Karpenter غير قادر على اكتشاف حدود ربط أحجام التخزين — حيث يسجل أن "عمليات التوسيع قد تفشل لأن Karpenter لن يكتشف حدود المشغل" وقد يعتقد أن العقدة (node) لديها مساحة غير متوفرة فعلياً.2 والحل هو الانتقال إلى مشغل CSI. وبشكل منفصل، هناك سباق Kubernetes معروف بين المجدول (scheduler) وتسجيل CSINode يمكن أن يجعل المجدول يفترض أن العقدة تدعم عمليات ربط أحجام تخزين أكثر مما تدعم في الواقع؛ ويدعم كل من aws-ebs-csi-driver و aws-efs-csi-driver ميزة "startup taint" للقضاء على هذه المشكلة، ويتم تكوينها من خلال startupTaints في NodePool.2
لماذا لا يقوم Karpenter بالتوسيع عندما تكون ResourceQuota ممتلئة؟
بسبب عدم وجود pod في حالة Pending. عندما يتم استنفاد ResourceQuota الخاصة بـ namespace، يرفض Kubernetes طلب الإنشاء: "إذا كان إنشاء أو تحديث مورد ينتهك قيد الحصة (quota constraint)، فإن لوحة التحكم ترفض هذا الطلب برمز حالة HTTP 403 Forbidden."8 لا يتم إنشاء كائن pod أبداً، لذا لا يقوم kube-scheduler أبداً بتمييز أي شيء على أنه غير قابل للجدولة، وKarpenter — الذي يعمل فقط على الـ pods غير القابلة للجدولة1 — ليس لديه ما يلاحظه.
العرض المميز لهذه الحالة هو: أن عدد النسخ (replica count) في Deployment الخاص بك لا يرتفع أبداً، ولأن الرفض يقع على المتحكم الذي يقوم بالإنشاء، يظهر الفشل كحدث FailedCreate على ReplicaSet بدلاً من أي pod.
من الجدير بذكر هذه النقطة بوضوح بدلاً من التعامل معها كخطأ يجب إصلاحه: هذا السلوك صحيح. حصة الـ namespace هي سقف إداري، وأي موسع تلقائي (autoscaler) يقوم بالتوفير بما يتجاوز هذا السقف سيكون بمثابة إبطال لهذا التحكم. هناك طلب مفتوح منذ فترة طويلة ضد مزود AWS يطلب من Karpenter التوسيع في هذه الحالة، ولا يزال مفتوحاً ومقدماً ضد نسخة ما قبل v1، ولكن "السلوك المتوقع" المذكور فيه يؤول إلى طلب من الموسع التلقائي التوفير بما يتجاوز الحصة.9 تعامل مع مساحة الحصة المتاحة كمتطلب أساسي للتوسيع التلقائي، وليس كشيء يمكن للتوسيع التلقائي حله.
لماذا لا يقوم Karpenter بالتوسيع من أجل DaemonSet جديد — أو من أجل نفسه؟
هذان رفضان موثقان، وكلاهما متعمد — لا يعتبر أي منهما خطأً يجب إيجاد حل بديل له بقدر ما هو سلوك يجب التصميم بناءً عليه.
DaemonSets. "لن يقوم Karpenter بتوسيع سعة إضافية من أجل DaemonSet إضافي من تلقاء نفسه"، لأن الـ pod الوحيد الذي سيستقر على العقدة الجديدة هو pod الـ DaemonSet نفسه — مما يعني استهلاك السعة دون فائدة. يتم احتساب DaemonSets كأعباء إضافية (overhead) عند تحديد حجم التوسيع لـ pods أعباء العمل، ولكن لا يتم اعتبارها أبداً سبباً للتوسيع. الحل البديل الموثق هو إعطاء pods الـ DaemonSet أولوية عالية باستخدام preemptionPolicy: PreemptLowerPriority، بحيث تقوم بإزاحة الـ pods ذات الأولوية المنخفضة على العقد الموجودة وتدفع تلك الـ pods إلى حالة Pending التي تؤدي بالفعل إلى تحفيز Karpenter.1
Karpenter نفسه. "Karpenter لن يقوم بتوفير سعة لتشغيل نفسه"، وهو ما يظهر في سطر السجلات (log line) حول karpenter.sh/nodepool DoesNotExist requirement. منذ الإصدار 0.16.0، أصبح عدد النسخ المتماثلة (replica count) الافتراضي هو 2، لذا فإن العنقود (cluster) الذي يحتوي فقط على سعة كافية غير تابعة لـ Karpenter لتشغيل pod واحد للمتحكم يترك الـ pod الثاني في حالة Pending بشكل دائم. الحلول الموثقة هي تقليل عدد النسخ المتماثلة إلى 1، أو التأكد من وجود سعة كافية لا يديرها Karpenter لتشغيل كلا الـ pods — في AWS، يتم ذلك عن طريق رفع معايير minimum و desired في مجموعة القياس التلقائي (autoscaling group) الخاصة بمجموعة العقد (node group).2
حالة ثالثة تستحق المعرفة قبل الإبلاغ عن خطأ (bug): العقد الإضافية التي تظهر أثناء عملية النشر (rollout) وتختفي بعد فترة وجيزة قد تكون ببساطة بسبب maxSurge. تقوم عملية الـ Consolidation بتجميع العقد بشكل مكثف، لذا مع قيمة maxSurge افتراضية بنسبة 25%، قد لا تجد الـ surge pods مكاناً للعمل؛ فيقوم Karpenter بتشغيل عقدة لها ويقوم بإزالتها بمجرد عدم الحاجة إليها.1
لماذا اختار Karpenter عقدة تبين أنها صغيرة جداً؟
لأن Karpenter يقوم بنمذجة الذاكرة القابلة للتخصيص قبل وجود العقدة، وهذا النموذج يحتوي على عامل تصحيح (fudge factor) متعمد. بالنسبة لزوج جديد من AMI ونوع مثيل (instance-type)، فإنه يقلل ذاكرة المثيل بنسبة VM_MEMORY_OVERHEAD_PERCENT، وقيمتها الافتراضية 7.5% — "تم ضبطها لتطابق الواقع بدقة لغالبية أنواع المثيلات دون المبالغة في التقدير".2 بعد التشغيل الأول لهذا الزوج، يقوم Karpenter بتخزين السعة الملحوظة مؤقتاً (cache) ويستخدم الرقم الحقيقي للعقد اللاحقة.
الجزء المفيد هنا هو اتجاه الخطأ: Karpenter "عادة ما يقلل من تقدير الذاكرة المتاحة على العقدة لنوع مثيل معين".2 التقليل من التقدير هو أمر آمن. أما تقليل القيمة لتضييق النطاق فهو الاتجاه الخطير — فالقيمة التي تسبب المبالغة في التقدير "يمكن أن تؤدي إلى قيام Karpenter بتشغيل عقد صغيرة جداً بالنسبة لحمل العمل الخاص بك".2
لاكتشاف ذلك بدلاً من استنتاجه، راقب حالة ConsistentStateFound في NodeClaim، والتي تتحول إلى False مع السبب ConsistencyCheckFailed:2
kubectl get nodeclaim $NODECLAIM_NAME \
-o jsonpath='{.status.conditions[?(@.type=="ConsistentStateFound")]}'
تقدم صفحة استكشاف الأخطاء وإصلاحها operator_status_condition_count{type="ConsistentStateFound",kind="NodeClaim",status="False"} كالمقياس (metric) الذي يجب مراقبته لهذا الغرض.2 لاحظ التضارب مع مرجع المقاييس: هذه العائلة غير المسبوقة (un-prefixed) مُعلمة بأنها مهجورة (DEPRECATED)، والبديل المكافئ لها في مرحلة BETA لكل نوع هو operator_nodeclaim_status_condition_count.6 قم بضبط التنبيهات على الاسم المهجور إذا كان هذا ما يصدره نظامك، ولكن توقع الانتقال إلى الاسم الجديد.
السبب المباشر هو عدم تحديد مواصفات الـ pods بدقة بدلاً من سوء نمذجة الـ nodes. يقوم Karpenter بعمل bin-packing بناءً على requests الموارد، لذا فإن الـ pods ذات الطلبات المنخفضة جداً أو المفقودة يتم حشرها بكثافة عالية، "مما يؤدي إلى تعرض الـ pods لعملية CPU throttling أو إنهاؤها بواسطة OOM killer." الحل الموثق للتخفيف من ذلك هو استخدام LimitRanges لكل namespace لفرض الحد الأدنى من أحجام الطلبات.2 هذا الأمر ليس خاصاً بـ Karpenter — حيث يقوم kube-scheduler بنفس الشيء مع الطلبات غير الدقيقة — لكن Karpenter يضخم المشكلة من خلال تحديد حجم الـ node بناءً على نفس الرقم الخاطئ. إذا كنت لا تزال تقرر ما يجب أن تكون عليه تلك الطلبات، فإن ميزة pod resizing في مكانها تمنحك طريقة لـ تغيير طلبات CPU والذاكرة دون إعادة تشغيل الـ pod أثناء عملية القياس.
ما هي المقاييس والسجلات التي يجب أن أتحقق منها أولاً؟
اقرأ سجلات المتحكم (controller) العادية أولاً. يتم بالفعل تسجيل إخفاقات الجدولة واستبعادات NodePool بمستوى info و error، لذا لا تحتاج إلى تغيير أي إعدادات لرؤيتها:
kubectl logs -n "${KARPENTER_NAMESPACE:-kube-system}" \
-l app.kubernetes.io/name=karpenter -c controller --tail=200
سجلات التصحيح (Debug logging) — عبر متغير البيئة LOG_LEVEL، أو --set logLevel=debug عند التثبيت2 — مفيدة حقاً، ولكن تعامل معها كخطوة ثانية وليس كرد فعل تلقائي. تغييرها يتطلب إعادة تشغيل الـ deployment، ولا يقوم Karpenter بتوفير أي شيء حتى يتم إعادة مزامنة ذاكرة التخزين المؤقت لحالة العنقود (cluster-state cache) بعد إعادة التشغيل. إعادة تشغيل الـ autoscaler في عنقود يعاني بالفعل من فشل في التوسع ليس هو المكان الذي تود أن تبدأ منه.
يوفر Karpenter مقاييس Prometheus عند karpenter.kube-system.svc.cluster.local:8080/metrics، ويمكن تهيئتها عبر METRICS_PORT.6 هناك ستة مقاييس تحمل معظم الإشارات اللازمة لتحقيقات التوسع (scale-up):6
| المقياس (Metric) | ما الذي يخبرك به | الاستقرار (Stability) |
|---|---|---|
karpenter_scheduler_unschedulable_pods_count | عدد الـ Pods غير القابلة للجدولة | ALPHA |
karpenter_scheduler_queue_depth | عدد الـ pods التي تنتظر الجدولة حالياً | BETA |
karpenter_scheduler_pending_pods_by_effective_zone_count | الـ pods المعلقة حسب قيود المنطقة الفعالة — يبلغ عن اسم المنطقة، أو flexible، أو none في حال عدم وجود تقاطع صالح | ALPHA |
karpenter_nodepools_limit | الحدود المهيأة على الـ nodepool، حسب نوع المورد | ALPHA |
karpenter_nodepools_usage | الموارد التي تم توفيرها فعلياً للـ nodepool | ALPHA |
karpenter_cluster_state_synced | 1 إذا كانت حالة العنقود في Karpenter تطابق خادم API، و 0 خلاف ذلك | STABLE |
لا تتجاهل الصف الأخير. مزامنة حالة العنقود (Cluster-state sync) هي بوابة وليست مجرد معلومة عابرة: طالما أنها تقرأ 0، فإن Karpenter لا يتخذ أي قرارات توفير على الإطلاق، لذا فإن العنقود الذي أعاد تشغيل المتحكم للتو أو الذي يعاني من اضطراب شديد قد يبدو مثل أي فشل آخر في هذه القائمة.
القيمة none في karpenter_scheduler_pending_pods_by_effective_zone_count هي التي يجب إطلاق تنبيه بشأنها: فهي تعني أن التقاطع بين إشارات المنطقة على مستوى الـ pod، وتوبولوجيا وحدة تخزين PVC، وقيود التوبولوجيا فارغ.6 هذا يمثل تناقضاً منطقياً في المناطق داخل مواصفات الـ pod نفسه، ويحول فئة من التخمينات — "هل هذه مشكلة منطقة؟" — إلى رقم يمكنك تمثيله بيانياً.
ملاحظتان حول الاستقرار حتى لا تبني لوحة تحكم على رمال متحركة. معظم مقاييس المجدول (scheduler metrics) المفيدة مصنفة كـ ALPHA بدلاً من STABLE، ومرجع المقاييس ينشر مستوى الاستقرار لكل مقياس تحديداً لكي تتمكن من معرفة أي منها آمن للاعتماد عليه.6 كما أن عائلة operator_status_condition_* غير المسبوقة بعلامة مصنفة كـ DEPRECATED، بينما المتغيرات الخاصة بكل نوع — operator_nodepool_status_condition_count، و operator_nodeclaim_status_condition_count، و operator_ec2nodeclass_status_condition_count — هي BETA.6
كيف يختلف هذا عن Cluster Autoscaler؟
الاختلاف المهم لعمليات تصحيح الأخطاء (debugging) هو مكان وجود القيد. بدلاً من توسيع مجموعة حددتها مسبقاً، يقوم Karpenter باشتقاق المثيل (instance) من الـ pods المعلقة — وهذا هو السبب في أن إخفاقاته تظهر كمشاكل في تقاطع المتطلبات بدلاً من مجموعة عقد (node group) وصلت إلى حدها الأقصى. يقوم Karpenter بتعبئة الدفعة المعلقة في أصغر نوع مثيل مناسب، ثم يضيف 59 نوعاً أكبر ويمرر جميع الخيارات الـ 60 إلى EC2 Fleet، والتي تختار باستخدام استراتيجية تخصيص Price Capacity Optimized.1 إن الـ NodePool الذي يسمح فقط بعدد قليل من أنواع المثيلات يعيق هذه الآلية، لذا فإن خطأ "العرض المطلوب" (required-offering error) يستحق المراجعة مقابل متطلباتك الخاصة قبل أن تستنتج أن السعة لم تكن متوفرة.
يتبع ذلك اختلافان تشغيليان. Karpenter "ليس مرتبطاً بإصدار Kubernetes محدد، كما هو الحال مع Cluster Autoscaler"، لذا يمكنك ترقيته وفقاً لجدوله الزمني الخاص1 — رغم أن مصفوفة التوافق لا تزال تضع حداً أدنى: إصدار Kubernetes 1.36 يتطلب Karpenter 1.13 أو أحدث، و 1.35 يتطلب 1.9 أو أحدث، و 1.34 يتطلب 1.6 أو أحدث.10 وهذه ليست خيارات إما/أو: "يمكن لـ Karpenter العمل جنباً إلى جنب مع Cluster Autoscaler"، كما أن الـ NodePools "مصممة للعمل جنباً إلى جنب مع حلول إدارة السعة الثابتة مثل EKS Managed Node Groups و EC2 Auto Scaling Groups".1 إذا كنت في منتصف عملية انتقال، فتأكد من أي متحكم (controller) يمتلك الـ pods التي تقوم بتصحيح أخطائها قبل افتراض أن Karpenter هو من يتجاهلها.
الخلاصة
تصبح مشكلة عدم قيام Karpenter بزيادة سعة العقد قابلة للحل بمجرد التوقف عن النظر إلى الـ autoscaler والبدء في النظر إلى البوابات التي تسبقه. اتبع التسلسل التالي: ابدأ بقراءة أحداث الـ pod للبحث عن رسالة Karpenter الخاصة بـ Failed to schedule pod، والتي غالباً ما تذكر السبب صراحةً وتسمح لك بتجاوز الباقي. إذا كانت غائبة أو غير واضحة، افحص البوابات بالترتيب — kubectl get nodepool و kubectl get ec2nodeclass للتأكد من الجاهزية، ثم status مقابل spec.limits، ثم سجلات المتحكم (controller logs) للبحث عن رسالة تقاطع المتطلبات (requirement-intersection). كل خطوة من هذه الخطوات إما أن تفتح البوابة أو تحدد السبب، وهذا هو السبب في أن العمل بالترتيب أفضل من التخمين.
هناك عادتان تمنعان تكرار هذه المشاكل. قم بتعريف كل taint يوضع على العقدة بعد التشغيل كـ startupTaint، لأن البديل هو حلقة توفير (provisioning loop) تكلف أموالاً حقيقية. وحافظ على متطلبات NodePool واسعة بقدر ما يتحمله عبء العمل الخاص بك فعلياً — توصي الوثائق بترك متطلبات نوع المثيل (instance-type) غير محددة لأن ذلك "يزيد من الخيارات المتاحة"،4 وتشير بشكل منفصل إلى أن أفضل دفاع ضد نفاد سعة spot هو السماح بأكبر عدد ممكن من أنواع المثيلات المختلفة.1
بالنسبة لجانب النشر من نفس المشكلة، يغطي دليلنا حول النشر بدون توقف (zero-downtime deployments) على Kubernetes إعدادات الجاهزية والتعطيل التي تحدد ما إذا كانت الـ pods ستنتقل فعلياً إلى السعة التي يوفرها Karpenter، كما أن شرح جعل helm upgrade --install idempotent يستحق القراءة قبل ترقية chart الخاص بـ Karpenter القادمة، حيث أن الإصدار المطبق جزئياً يمكن أن يترك NodePools غير جاهزة. إذا كنت تقوم بتصحيح خطأ في كائن Kubernetes آخر عالق في حالة pending، فإن نفس منهجية قراءة الحالات تنطبق في مقالنا حول شهادة cert-manager عالقة في حالة Pending.
Footnotes
-
Karpenter، "الأسئلة الشائعة"، توثيقات karpenter.sh (v1.14)، آخر تعديل في 12 أغسطس 2026. https://karpenter.sh/docs/faq/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18
Karpenter، "Troubleshooting،" توثيق karpenter.sh (الإصدار v1.14)، آخر تعديل في 12 أغسطس 2026. https://karpenter.sh/docs/troubleshooting/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26 ↩27 ↩28 ↩29 ↩30 ↩31
Karpenter، "Settings," توثيق karpenter.sh (v1.14) — متغيرات البيئة وأعلام CLI، بما في ذلك MIN_VALUES_POLICY (الافتراضي Strict)، و BATCH_IDLE_DURATION (الافتراضي 1s) و BATCH_MAX_DURATION (الافتراضي 10s). https://karpenter.sh/docs/reference/settings/ ↩ ↩2 ↩3
Karpenter, "NodePools," توثيق karpenter.sh (v1.14)، آخر تعديل في 12 أغسطس 2026. https://karpenter.sh/docs/concepts/nodepools/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26 ↩27
Karpenter, "NodeClasses," توثيق karpenter.sh (v1.14)، آخر تعديل 12 أغسطس 2026. https://karpenter.sh/docs/concepts/nodeclasses/ ↩ ↩2 ↩3 ↩4
Karpenter, "Metrics," توثيق karpenter.sh (v1.14)، آخر تعديل 12 أغسطس 2026. https://karpenter.sh/docs/reference/metrics/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
Karpenter, "Disruption," توثيق karpenter.sh (v1.14)، آخر تعديل 12 أغسطس 2026 — انظر "Special Cases on Drift"، والتي تدرج spec.template.spec.requirements كحقل حيث لا يؤدي تغيير CRD بالضرورة إلى حدوث drift في NodeClaim تظل قيمته الحالية متوافقة. https://karpenter.sh/docs/concepts/disruption/ ↩
Kubernetes، "Resource Quotas"، توثيق Kubernetes.io. https://Kubernetes.io/docs/concepts/policy/resource-quotas/ ↩ ↩2
citiatish, "Karpenter not scaling nodes when ResourceQuota in namespace is fully utilized," مشكلة رقم 6737، aws/karpenter-provider-aws، تم فتحها في 14 أغسطس 2024؛ مفتوحة ومصنفة كـ bug / triage/needs-investigation اعتباراً من 16 أغسطس 2026. https://GitHub.com/aws/karpenter-provider-aws/issues/6737 ↩ ↩2
Karpenter، "Compatibility،" توثيق karpenter.sh (v1.14)، آخر تعديل في 12 أغسطس 2026. https://karpenter.sh/docs/upgrading/compatibility/ ↩
