هذا المقال يتطلب مستوى: متوسط. الافتراض إنك بتشغّل خدمة HTTP طويلة العمر خلف Service في Kubernetes، والـ rollout بيستخدم استراتيجية RollingUpdate الافتراضية.
لو كل rolling update بيرجّعلك نسبة من الطلبات في صورة أخطاء 502، المشكلة مش في التطبيق ولا في السيرفر. المشكلة إن الـ Pod بيموت وهو لسه ماسك اتصالات شغّالة. هنا هتعرف تصلّحها في 3 إضافات صغيرة على الـ Deployment.
Graceful Shutdown: ليه الـ deploy بيقطع اتصالات المستخدمين وإزاي توقفه
المشكلة باختصار
وقت الـ rolling update، Kubernetes بيشيل الـ Pods القديمة ويطلع جديدة. لو الـ Pod القديم اتقفل فجأة وهو بينفّذ طلبات، الطلبات دي بتتقطع. النتيجة: أخطاء 502 و503 عند المستخدم من غير أي سبب واضح في الـ logs.
يعني إيه Graceful Shutdown؟
تخيّل كاشير في سوبر ماركت، والمدير قاله "اقفل حالًا". لو قام سايب زبون في نص عملية الدفع، الزبون بيطلع متضايق من غير فاتورة. الكاشير المحترم بيعمل حاجة أذكى: بيوقف استقبال زباين جداد، بس يخلّص اللي واقفين قدامه بالفعل، وبعدها يقفل. ده بالظبط الـ Graceful Shutdown.
علميًا: لما Kubernetes يقرر يوقف Pod، بيبعتله إشارة SIGTERM. دي مش قتل فوري، دي طلب "ابدأ تقفل بأدب". التطبيق المفروض يمسك الإشارة دي، يبطّل استقبال اتصالات جديدة، يكمّل الاتصالات الحالية، وبعدها يخرج. لو ماخرجش خلال مهلة اسمها terminationGracePeriodSeconds (الافتراضي 30 ثانية)، وقتها بس Kubernetes بيبعت SIGKILL اللي بيقتل العملية فورًا.
اللي بيحصل فعلاً: الـ race condition
فيه مصيدة أغلب الناس بتقع فيها. إزالة الـ Pod من قائمة الـ endpoints وإرسال SIGTERM بيحصلوا في نفس اللحظة تقريبًا وبشكل غير متزامن. يعني ممكن الـ Pod يبدأ يقفل وهو لسه مسجّل كوجهة صالحة، فيوصله طلب جديد على اتصال بيتقطع. النتيجة 502.
الحل: SIGTERM handler مع preStop
الحل جزئين. الأول: خلّي تطبيقك يمسك SIGTERM ويقفل بأدب. مثال بـ Node.js:
const server = app.listen(8080);
process.on('SIGTERM', function () {
// بطّل استقبال اتصالات جديدة، وكمّل الجارية
server.close(function () {
process.exit(0);
});
});
الجزء التاني: عالِج الـ race بإضافة preStop hook بينام شوية قبل SIGTERM، عشان تدّي وقت لإزالة الـ Pod من الـ endpoints تنتشر على كل الـ nodes. الإعداد في الـ Deployment:
spec:
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
الترتيب: أول ما الـ Pod يتعلّم للإنهاء، ينفّذ preStop (ينام 5 ثواني)، وبعدها يتبعت SIGTERM. الـ 5 ثواني دول بيسمحوا للـ Service إنه يوقف يوجّه طلبات جديدة للـ Pod ده قبل ما يبدأ يقفل فعلًا.
سيناريو بأرقام
لو عندك خدمة بتستقبل 50 ألف طلب في اليوم وبتعمل deploy مرتين يوميًا على 4 Pods: في قياس عملي، النسخة بدون preStop كانت بتسقّط حوالي 1.8% من الطلبات وقت التبديل. بعد إضافة preStop: sleep 5 ومعالج SIGTERM، النسبة نزلت لصفر تقريبًا. الأرقام دي تقديرية وبتختلف حسب مدة أطول request عندك ومعدل الترافيك، بس الاتجاه ثابت.
الـ trade-offs
- بتكسب: صفر طلبات مقطوعة وقت الـ deploy. بتخسر: كل rollout بيطول بمقدار مدة الـ sleep لكل دفعة Pods.
- قيمة
terminationGracePeriodSecondsكبيرة بتحمي الطلبات الطويلة، بس بتأخّر أي rollback وقت الحوادث. الافتراض إن أطول request عندك أقصر من المهلة دي. - الـ sleep الثابت حل بسيط لكنه تقريبي. البديل الأدق إنك تخلّي الـ readiness probe يفشل بمجرد استقبال SIGTERM، بس ده كود أكتر.
متى لا تستخدم هذه الطريقة
لو خدمتك batch job أو worker مالوش اتصالات HTTP حيّة، مفيش داعي للـ preStop. ولو كل طلباتك قصيرة جدًا (أقل من 100 مللي ثانية) والـ endpoint propagation عندك سريع، المكسب هيكون هامشي مقابل بطء في كل rollout. اقيس الأول قبل ما تضيف تعقيد.
الخطوة التالية
افتح الـ Deployment بتاع أهم خدمة عندك، ضيف terminationGracePeriodSeconds: 30 وpreStop: sleep 5 ومعالج SIGTERM في الكود. اعمل deploy وانت بتبعت ترافيك خفيف بأداة زي hey أو vegeta، وراقب عدّاد أخطاء 502. المفروض ينزل لصفر.
المصادر
- Kubernetes Docs — Pod Lifecycle (Termination of Pods)
- Kubernetes Docs — Container Lifecycle Hooks (PreStop)
- CNCF — Decoding the Pod Termination Lifecycle in Kubernetes
- Google Cloud — Kubernetes best practices: terminating with grace