المستوى المطلوب: محترف (Advanced). المقال موجّه لمهندسي DevOps ومطوّري الباك-إند اللي بيشغّلوا حاويات في الإنتاج. كل مفهوم صعب هتلاقيه متشروح الأول بمثال بسيط، وبعدها التفسير التقني الدقيق، فحتى لو Docker لسه جديد عليك تقدر تكمّل معانا.
لو خدمتك جوّه Docker فجأة بترمي fork: retry: Resource temporarily unavailable والـ CPU والرام لسه فاضيين، غالبًا المشكلة إن عمليات Zombie اتكدّست، لأن تطبيقك بيشتغل كـ PID 1 ومش بيحصدها. هنا هتعرف السبب على مستوى الكيرنل، وتصلّحه بسطر واحد، وتتأكد إن docker stop بيقفل خدمتك بهدوء بدل ما يقطعها في نص الطلب.
المشكلة باختصار
الحاوية مش نظام تشغيل كامل؛ هي عملية واحدة بتتفرّع منها عمليات تانية. أول عملية بتشتغل جوّه الحاوية بتاخد الرقم PID 1، وده رقم ليه معاملة خاصة جدًا في لينكس. المشكلة بتبدأ لمّا تطبيقك العادي (زي node أو python) يقع في خانة PID 1 من غير ما يكون مؤهّل للدور ده. النتيجة نوعين من الأعطال: عمليات Zombie بتتراكم لحد ما جدول العمليات يمتلي، وdocker stop بيفشل يوقّف خدمتك بهدوء.
يعني إيه عملية Zombie أصلاً؟
خلّينا نبدأ بمثال. تخيّل موظف خلّص عقده ومشي من الشركة فعلاً، بس ورقة إخلاء الطرف بتاعته لسه على مكتب المدير مستنية توقيع. الراجل مشي (العملية ماتت)، لكن ملفه لسه بياخد خانة في سجل الموظفين. لو المدير عمره ما وقّع على الأوراق دي، الملفات بتفضل متراكمة وبتاكل خانات السجل، لحد ما مفيش مكان لتعيين موظف جديد.
ده بالظبط اللي بيحصل تقنيًا. لمّا عملية ابن بتخلص وتموت في لينكس، مبتختفيش فورًا. بتفضل في حالة اسمها Zombie: العملية خلصت، بس حالة خروجها (exit status) لسه محتاجة الأب يقراها عن طريق نداء النظام wait(). النداء ده اسمه "الحصاد" (reaping). العملية الـ Zombie مش بتاخد ذاكرة ولا معالج، لكنها بتحتفظ بمدخلة في جدول العمليات وبرقم PID. ومتقدرش تقتلها بـ kill لأنها ميتة أصلاً؛ بتظهر في ps بعلامة Z وكلمة <defunct>.
عادي في أي نظام إن عمليات Zombie تظهر للحظات — الأب بيحصدها بسرعة فتختفي. المشكلة الحقيقية لمّا الأب ميعملش wait() أبدًا، فيتراكموا بلا سقف.
ليه PID 1 بالذات هو المتسبّب؟
PID 1 في لينكس هو الـ init: أول عملية بيشغّلها الكيرنل، وكل العمليات بتتفرّع منها. وله مسؤوليتين خاصتين مش موجودتين في أي عملية تانية:
- حصاد الأيتام. لو عملية مات أبوها، بيتم "تبنّيها" تلقائيًا بواسطة PID 1. يبقى PID 1 مسؤول إنه يعمل
wait()للعمليات دي لمّا تموت. لو تطبيقك هو PID 1 ومش بيعمل الحصاد، كل يتيم بيموت بيتحوّل لـ Zombie دائم. - معاملة خاصة مع الإشارات. الكيرنل بيحمي PID 1: أي إشارة (زي SIGTERM) ملهاش handler متركّب صراحةً في العملية، الكيرنل بيتجاهلها بدل ما يطبّق سلوكها الافتراضي. ده بيمنع إن حد يقتل الـ init بالغلط، بس كمان معناه إن تطبيقك كـ PID 1 هيتجاهل SIGTERM لو مش مركّب له handler.
وفيه فخ تاني كتير بيقع فيه: لو كتبت (الصيغة النصية / shell form)، Docker بيشغّل ، فالـ يبقى هو PID 1 مش الـ node. والـ shell مبيمرّرش الإشارات لابنه افتراضيًا، فتطبيقك عمره ما هيشوف SIGTERM أصلاً.