ليلة النشر التي كادت أن تكسر ظهري
أذكرها وكأنها البارحة. كانت ليلة خميس، وأنا في مكتبي الصغير أحاول نشر تحديث جديد لتطبيق ذكاء اصطناعي نعمل عليه. التطبيق كان عبارة عن خدمة تحليل نصوص، والموديل الجديد كان أذكى وأكبر حجمًا. على جهازي المحلي، كل شيء كان يعمل “زي الحلاوة”. الاختبارات ناجحة، الأداء ممتاز، والأمور تبدو مبشرة.
بكل ثقة، ضغطت على زر النشر على نظام Kubernetes الخاص بنا. فتحت لوحة المراقبة (Dashboard) لأرى الـ Pods الجديدة وهي تولد وتأخذ مكان القديمة. لكن شيئًا غريبًا كان يحدث. الـ Pod الجديد يظهر للحظات، ثم يختفي، ثم يعود، ثم يختفي مرة أخرى. نظرت إلى حالته، فرأيت تلك الكلمة المشؤومة التي يعرفها كل من تعامل مع Kubernetes: CrashLoopBackOff.
يا إلهي! التطبيق يدخل في حلقة لانهائية من الانهيار وإعادة التشغيل. Kubernetes، في محاولته لمساعدتي، كان يعيد تشغيل الحاوية (Container) كلما انهارت. لكنها كانت تنهار بسرعة لدرجة أني لم أكن ألحق حتى أن أقرأ سجلات الأخطاء (Logs) لأفهم ما الذي يجري. شعرت بالدم يغلي في عروقي. هل هي مشكلة في الكود؟ هل نسيت متغير بيئة (Environment Variable)؟ هل هناك تسريب في الذاكرة؟
أمضيت الساعات التالية في حالة من الذعر والترقب، أحاول “صيد” الخطأ قبل أن يموت الـ Pod مجددًا. كانت معركة خاسرة. كلما اعتقدت أنني وجدت طرف الخيط، كان الـ Pod يختفي من أمامي. في تلك الليلة، تعلمت درسًا قاسيًا ومهمًا: أحيانًا، المشكلة ليست في تطبيقك، بل في الطريقة التي تخبر بها Kubernetes أن تطبيقك “بخير”. وهنا يأتي دور أبطال قصتنا: مسابير الحياة والجاهزية.
ما هو جحيم الـ CrashLoopBackOff؟
قبل أن نغوص في الحل، دعونا نفهم المشكلة. عندما ترى حالة CrashLoopBackOff في Kubernetes، فهذا يعني أن Kubernetes يحاول أن يكون صديقك الوفي. هو يرى أن الحاوية الخاصة بك قد توقفت عن العمل (انهارت) بعد وقت قصير من بدئها. فماذا يفعل؟ يقول لنفسه: “لعلها كانت مشكلة عابرة، سأحاول تشغيلها مرة أخرى”.
لكن إذا استمرت الحاوية في الانهيار مباشرة بعد كل إعادة تشغيل، يدرك Kubernetes أن هناك مشكلة حقيقية. ولكي لا يستهلك موارد السيرفر في محاولات إعادة تشغيل فاشلة كل ثانية، يبدأ في زيادة الوقت بين كل محاولة وأخرى. هذا هو ما يسمى “BackOff” أو التراجع. فتجد نفسك في هذه الحلقة المفرغة: تشغيل -> انهيار -> انتظار -> تشغيل -> انهيار -> انتظار أطول…
المشكلة في حالتي لم تكن أن التطبيق ينهار بسبب خطأ برمجي. المشكلة كانت أن تطبيق الذكاء الاصطناعي الجديد كان يحتاج حوالي 45 ثانية لتحميل الموديل الضخم في الذاكرة وبدء الاستماع على الشبكة. Kubernetes، بافتراضاته الأولية، كان يرى أن التطبيق لم “يستجب” خلال ثوانٍ قليلة، فافترض أنه “ميت” وقام بقتله وإعادة تشغيله. وهكذا دواليك.
المنقذون: مسابير كوبرنيتيس (Kubernetes Probes)
مسابير كوبرنيتيس هي الطريقة التي يتواصل بها نظام الأوركسترا مع تطبيقاتك ليفهم حالتها الصحية. بدلًا من الافتراضات العمياء، يقوم Kubernetes بطرح أسئلة مباشرة على الحاوية، مثل “كيف حالك يا صاحبي؟ هل أنت على قيد الحياة؟ هل أنت جاهز لاستقبال الزوار؟”.
هناك ثلاثة أنواع رئيسية من المسابير، لكننا سنركز على أهم اثنين لحل مشكلتنا:
- مسبار الحياة (Liveness Probe): يسأل السؤال: “هل ما زلت تعمل أم أنك تجمدت؟”. إذا فشل هذا المسبار، فهذا يعني أن التطبيق في حالة سيئة (مثل deadlock) ويجب إعادة تشغيله.
- مسبار الجاهزية (Readiness Probe): يسأل السؤال: “هل أنت مستعد لاستقبال طلبات جديدة؟”. إذا فشل هذا المسبار، فهذا لا يعني أن التطبيق ميت، بل قد يكون مشغولًا بالبدء أو يقوم بعملية طويلة. في هذه الحالة، يتوقف Kubernetes عن إرسال الترافيك إليه، لكنه يبقيه يعمل.
1. مسبار الحياة (Liveness Probe): هل أنت على قيد الحياة؟
هذا المسبار هو شريان الحياة لتطبيقك. وظيفته هي التأكد من أن التطبيق لم يدخل في حالة تجمد (deadlock) حيث يكون البرنامج يعمل لكنه لا يستجيب لأي شيء. إذا فشل هذا المسبار لعدد معين من المرات، يقوم Kubernetes بقتل الحاوية وإعادة تشغيلها، على أمل أن تحل إعادة التشغيل المشكلة.
نصيحة أبو عمر: كن حذرًا جدًا مع مسبار الحياة. إذا كان المسبار نفسه ثقيلًا أو يعتمد على خدمات خارجية، قد يفشل بسبب بطء الشبكة وليس لأن تطبيقك ميت، مما يؤدي إلى إعادة تشغيل غير ضرورية. اجعل نقطة فحص الحياة (health check endpoint) بسيطة وسريعة قدر الإمكان.
يمكن تعريف المسبار بثلاث طرق: عبر طلب HTTP، أو فحص منفذ TCP، أو تنفيذ أمر داخل الحاوية. المثال الأكثر شيوعًا هو HTTP:
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: my-app-container
image: my-image
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /healthz # المسار الذي يجب أن يرد بـ 200 OK
port: 8080
initialDelaySeconds: 15 # انتظر 15 ثانية بعد بدء الحاوية قبل أول فحص
periodSeconds: 20 # افحص كل 20 ثانية
failureThreshold: 3 # اعتبره فاشلاً بعد 3 محاولات فاشلة متتالية
في هذا المثال، ينتظر Kubernetes لمدة 15 ثانية بعد بدء الحاوية، ثم يبدأ بإرسال طلب HTTP GET إلى /healthz على المنفذ 8080 كل 20 ثانية. إذا فشل الطلب 3 مرات متتالية، سيقوم بإعادة تشغيل الحاوية.
2. مسبار الجاهزية (Readiness Probe): هل أنت جاهز للعمل؟
هذا هو المسبار الذي كان سينقذني في تلك الليلة المشؤومة. وظيفته هي إخبار Kubernetes متى يكون تطبيقك مستعدًا بالفعل لخدمة المستخدمين. هذا مهم جدًا للتطبيقات التي تحتاج وقتًا للبدء، مثل تطبيقاتي التي تحمل موديلات تعلم الآلة، أو أي تطبيق يحتاج للاتصال بقاعدة بيانات، أو تهيئة ذاكرة تخزين مؤقت (cache).
عندما يفشل مسبار الجاهزية، لا يقوم Kubernetes بقتل الحاوية. بدلًا من ذلك، يقوم بإزالتها مؤقتًا من “قائمة الخدمة” (Service Endpoints). هذا يعني أنه لن يتم إرسال أي ترافيك جديد إليها حتى ينجح مسبار الجاهزية مرة أخرى.
نصيحة أبو عمر: مسبار الجاهزية هو سر عمليات النشر بدون انقطاع (Zero-Downtime Deployment). عندما تنشر نسخة جديدة، لن يرسل Kubernetes أي مستخدمين إلى الـ Pod الجديد إلا بعد أن يخبره مسبار الجاهزية “أنا جاهز يا كبير!”. هذا يضمن عدم مواجهة أي مستخدم لصفحة خطأ أثناء عملية التحديث.
هنا مثال على كيفية إضافته:
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: my-app-container
image: my-image
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /readyz # مسار مختلف للتأكد من الجاهزية
port: 8080
initialDelaySeconds: 5 # ابدأ الفحص مبكرًا
periodSeconds: 5 # افحص بشكل متكرر
livenessProbe:
# ... نفس مسبار الحياة من المثال السابق
# لكن مع initialDelaySeconds أطول بكثير
initialDelaySeconds: 60
لاحظ الفرق في initialDelaySeconds. بالنسبة لمسبار الجاهزية، نبدأ الفحص مبكرًا (5 ثوانٍ) وسيفشل بشكل طبيعي حتى يصبح التطبيق جاهزًا. أما مسبار الحياة، فنعطيه وقتًا طويلاً (60 ثانية) قبل أن نبدأ في القلق بشأن ما إذا كان التطبيق قد تجمد، لإعطائه فرصة كافية للبدء.
حل مشكلة تطبيق الذكاء الاصطناعي
بالعودة إلى قصتي، الحل كان في تطبيق هذين المسبارين بذكاء. إليك ما فعلته:
- أنشأت نقطتي فحص (Health Check Endpoints) في تطبيقي:
/healthz: نقطة بسيطة جدًا ترد بـ 200 OK بمجرد بدء الخادم. هذا ليخبر مسبار الحياة أن العملية لم تمت./readyz: نقطة أكثر ذكاءً. لا ترد بـ 200 OK إلا بعد أن يتم تحميل موديل الذكاء الاصطناعي بالكامل في الذاكرة.
- قمت بتحديث ملف تعريف النشر (Deployment YAML):
# ... (deployment spec)
spec:
containers:
- name: ai-model-server
image: my-ai-app:v2
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /readyz
port: 80
initialDelaySeconds: 10 # ابدأ الفحص بعد 10 ثوانٍ
periodSeconds: 5
failureThreshold: 12 # اسمح له بالفشل لمدة دقيقة (12 * 5 ثوانٍ)
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 90 # لا تبدأ فحص الحياة إلا بعد 90 ثانية!
periodSeconds: 30
بهذا الإعداد، حدث السحر. عند نشر الـ Pod الجديد:
- يبدأ الـ Pod.
- مسبار الجاهزية يبدأ بالفحص بعد 10 ثوانٍ ويفشل، وهذا طبيعي. Kubernetes يرى الـ Pod “غير جاهز” ولا يرسل له أي ترافيك.
- يستمر التطبيق في تحميل الموديل الضخم (يستغرق حوالي 45 ثانية).
- بعد حوالي 45 ثانية، يصبح التطبيق جاهزًا، ويبدأ مسار
/readyzبالرد بـ 200 OK. - ينجح مسبار الجاهزية، فيقوم Kubernetes بإضافة الـ Pod إلى الخدمة ويبدأ في إرسال الترافيك إليه.
- مسبار الحياة يبدأ عمله بعد 90 ثانية (وقت كافٍ جدًا)، ليضمن فقط أن التطبيق لن يتجمد لاحقًا.
انتهى كابوس CrashLoopBackOff. وأصبحت عمليات النشر سلسة وهادئة.
ملاحظة للمحترفين: مسبار البدء (Startup Probe)
في الإصدارات الحديثة من Kubernetes (1.18+)، تم تقديم نوع ثالث من المسابير يسمى Startup Probe. هذا المسبار مصمم خصيصًا لحل مشكلة التطبيقات بطيئة البدء بشكل أكثر أناقة. فكرته هي أنه يعطِّل مسبار الحياة والجاهزية حتى ينجح هو أولًا. هذا يمنعنا من الحاجة إلى استخدام initialDelaySeconds كبيرة جدًا في مسبار الحياة.
إذا كنت تستخدم إصدارًا حديثًا، فهذا هو الأسلوب الموصى به:
# ...
startupProbe:
httpGet:
path: /readyz # يمكن استخدام نفس مسار الجاهزية
port: 80
failureThreshold: 30
periodSeconds: 10
# بعد نجاح startupProbe، تبدأ المسابير التالية بالعمل
readinessProbe:
# ...
livenessProbe:
# ...
هنا، سيحاول Kubernetes فحص /readyz كل 10 ثوانٍ لمدة تصل إلى 300 ثانية (30 * 10). فقط بعد نجاح هذا الفحص، ستبدأ مسابير الحياة والجاهزية العادية في العمل. هذا هو الحل الأمثل والأكثر نظافة.
الخلاصة والنصيحة الأخيرة 💡
في عالم الحاويات والأوركسترا، التفاصيل الصغيرة هي التي تفرق بين تطبيق “بضل يوقع” وتطبيق صامد مثل شجرة الزيتون. لا تستهينوا بالمسابير أبدًا؛ فهي ليست مجرد إضافة اختيارية، بل هي عيونكم وآذانكم داخل الكلاستر، وهي أساس بناء أنظمة قوية ومستقرة.
تذكروا دائمًا هذا الملخص البسيط:
- Liveness Probe (مسبار الحياة): هل يجب أن أقتلك وأعيد تشغيلك؟ (للتعامل مع التجمد)
- Readiness Probe (مسبار الجاهزية): هل يجب أن أرسل لك مستخدمين الآن؟ (لإدارة الترافيك أثناء البدء والتحديثات)
- Startup Probe (مسبار البدء): هل انتهيت من عملية البدء الطويلة؟ (للتطبيقات بطيئة الإقلاع)
لا تنتظروا حتى تواجهوا كابوس CrashLoopBackOff في منتصف الليل لتتعلموا هذا الدرس. ابدأوا اليوم بإضافة مسابير صحيحة ومنطقية لجميع تطبيقاتكم على Kubernetes. هذا الاستثمار الصغير في ملفات الإعداد سيوفر عليكم ساعات لا تحصى من التوتر وتصحيح الأخطاء في المستقبل.
يلا، شدّوا حيلكم! 💪