DOCKER::ROOT الإقليم II · cgroups · عزل الموارد kernel live
REGION II البدائية الثانية /sys/fs/cgroup

الحدود: كم تستطيع العملية أن تستهلك؟

namespaces عزلت الرؤية؛ cgroups تعزل الموارد. هاتان معاً ساقا الكونتينر. واحدةٌ بلا الأخرى عرجاء — وقد رأيت العرج للتو حين أسقطت fork bomb المضيفَ كله.

النبذة

cgroup اختصار «control group»: مجموعة عملياتٍ يطبّق عليها الـ kernel محاسبةً وحدوداً على موردٍ معيّن. كم ذاكرة؟ كم CPU؟ كم عملية؟ كم I/O؟ الفكرة بسيطة، لكن كيف عرضها الـ kernel — كنظام ملفات — قرارٌ عميق، ولماذا أعاد بناءها من الصفر في الجيل الثاني قصةٌ تعلّمك كيف تنضج الأنظمة.

اللغز المستفزّ — روّض الوحش الذي أسقط مضيفك

مهمتك (ممنوع Docker وممنوع systemd-run):

  1. اجعل عمليةً وكل أبنائها محدودةً بـ ٢٠ عملية، بحيث يرفض الـ kernel fork الحادي والعشرين ويبقى المضيف سليماً أمام fork bomb.
  2. حدّ نفس المجموعة بـ ٥٠ ميجابايت، شغّل ملتهِم ذاكرة، وراقب من يقتله وبأي إشارة وأين يُسجّل ذلك.
السؤال الذي يقودك

إن كانت cgroups نظام ملفات، فـ«إنشاء cgroup» = إنشاء مجلد، و«وضع عملية» = كتابة رقمها في ملف، و«تحديد حدّ» = كتابة رقمٍ في ملفٍ آخر. ما أسماء هذه الملفات؟ تصفّح /sys/fs/cgroup واقرأ. لا تبحث عن أمرٍ سحري؛ ابحث عن الملفات.

لحظة «آها» هنا أن cgroup ليس أمراً، بل بنية مجلداتٍ تكتب فيها — وهذا يغيّر كيف ترى كل أداةٍ بُنيت فوقها.

الدرس — لماذا نظام ملفات؟ ولماذا أُعيد بناؤه؟

ليش وُجدت cgroups

المشكلة عند Google: تشغيل آلاف المهام على آلةٍ واحدة دون أن تخنق مهمةٌ جشعةٌ البقية. الأدوات الموجودة (ulimit/setrlimit) كانت لكل عملية على حدة — لا شجرة عمليات تتكاثر. fork bomb تتجاوز ulimit لأن كل ابنٍ عمليةٌ «جديدة» بحدودها.

ليش — الفرق الجوهري

ulimit/rlimit: حدٌّ على عمليةٍ مفردة.  cgroup: حدٌّ على مجموعةٍ بأكملها، تُحاسَب وتُلجَم جماعياً، يرثه كل من يُولد داخلها. اشتُقّت من مشروعٍ اسمه «process containers» (2006) — لاحظ الكلمة.

ليش عُرِضت كنظام ملفات

كان الـ kernel أمام خيار: نظام نداءٍ جديد أم واجهة عبر نظام ملفاتٍ افتراضي؟ اختار الثاني — فلسفة يونكس «كل شيء ملف». الفائدة العملية ضخمة: لا أدوات خاصة. mkdir ينشئ cgroup، echo > file يضبط حدّاً، cat file يقرأ المحاسبة، والصلاحيات بصلاحيات الملفات المعتادة. هذه البساطة مكّنت آلاف الأدوات (Docker، systemd، k8s) من البناء فوقها بلا API خاص.

ليش v1 فشلت، وكيف أنقذتها v2

درسٌ هندسي ثمين. cgroup v1 ارتكب خطأً معمارياً: جعل لكل مورد (controller) شجرته الهرمية المستقلة. شجرة للذاكرة، أخرى للـ CPU، ثالثة للـ I/O...

لماذا كارثة؟ لأن الموارد مترابطة. عند استرجاع الذاكرة يحتاج الـ kernel كتابة صفحاتٍ متّسخة للقرص — أي محاسبة I/O. لكن إن كانت عمليتك في cgroup ذاكرة وI/O منفصلين، فلا يستطيع الـ kernel التنسيق بينهما. صار v1 «مجموعة هرميّاتٍ متخاصمة».

v2 («unified hierarchy») أصلح الجذر: شجرةٌ واحدة، كل عملية في عقدةٍ واحدة، وكل الـ controllers على نفس الشجرة. الآن يرى الـ kernel الصورة كاملةً في مكانٍ واحد فينسّق. نتيجةٌ تخصّك: على v2 يذهب --memory و--cpus إلى نفس مجلد cgroup؛ على v1 كانا في شجرتين مختلفتين. لهذا التوزيعات الحديثة افترضت v2.

قاعدة «no internal processes» في v2

لا يمكن أن تحتوي عقدةٌ داخلية (لها أبناءٌ فعّالون) عملياتٍ مباشرة في آنٍ واحد — العمليات في الأوراق فقط. السبب: الغموض (على من تُطبَّق حصّة الأب؟). ستصطدم بهذا حين تبني هرمك، ورسالة -EBUSY ستكون مرشدك.

كيف تبدو فعلياً (v2)

/sys/fs/cgroup/ ├── cgroup.controllers # أي controllers متاحة هنا ├── cgroup.subtree_control # أي controllers مفعّلة للأبناء ← مفتاح ├── cgroup.procs # اكتب PID لتنقله هنا ├── memory.max # حدّ الذاكرة الصلب ├── memory.current # الاستهلاك الحالي ├── pids.max # حدّ عدد العمليات ├── cpu.max # "quota period" └── mygroup/ # ← cgroup أنشأته بـ mkdir └── ... نفس الملفات تظهر تلقائياً

ثلاث عملياتٍ تلخّص كل شيء:

bash
$ mkdir /sys/fs/cgroup/mygroup # إنشاء $ echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs # وضع عملية (والأبناء يرثون) $ echo 20 > /sys/fs/cgroup/mygroup/pids.max # ضبط حدّ

الدقّة التي ستوقعك: لتفعيل controller على أبنائك اكتبه في cgroup.subtree_control للأب. وبسبب قاعدة «no internal processes» قد تحتاج نقل عملياتك إلى ورقةٍ قبل أن يسمح لك بتفعيله. اكتشف الترتيب بنفسك؛ -EBUSY مرشدك.

من يقتل، وبأي إشارة

حين تتجاوز مجموعةٌ memory.max، يستدعي الـ kernel الـ OOM killer داخل تلك المجموعة، يقتل عمليةً منها (لا من المضيف) بـ SIGKILL (غير قابلة للاعتراض).

ليش — اشتقاق 137 لا حفظه

هذا يفسّر OOMKilled في docker inspect ورمز الخروج 137 = 128 + 9 (9 = SIGKILL). و128+n نفسه اصطلاح الـ shell. فهمت الرسالة من المبادئ، لا من جدول حفظ. تراقب الأحداث عبر memory.events (يَعُدّ oom_kill).

اللغز — ابنِ هرماً يحاكي ما يفعله Docker

  1. اكتشف أين يضع Docker كونتيراته. شغّل docker run --memory=64m --cpus=0.5 --pids-limit=20 ... ثم جِد cgroup الكونتينر تحت /sys/fs/cgroup (ابحث تحت system.slice أو docker/)، واقرأ memory.max وpids.max وcpu.max. تحقّق أنها تطابق ما كنت ستكتبه يدوياً. Docker لا يفعل سحراً — يكتب نفس الأرقام في نفس الملفات.
  2. افكك cpu.max. تحوي رقمين (quota period، مثلاً 50000 100000). استنتج: ماذا يعني --cpus=0.5 بدلالتهما؟ ولماذا يختلف هذا النموذج (حصة زمنية) جوهرياً عن --cpu-shares (وزن نسبي تنافسي)؟ متى تريد حدّاً صلباً ومتى وزناً؟
  3. محاكاة الضغط: memory.max=50M بلا swap (memory.swap.max=0)، شغّل ملتهِم ذاكرة، التقط حدث القتل من memory.events، واربطه برمز الخروج 137. تأمّل: العملية ماتت، والمضيف لم يهتزّ.
المصادر

man 7 cgroups، ووثيقة الـ kernel Documentation/admin-guide/cgroup-v2.rst. لفهم cpu.max اقرأ قسم «CPU»؛ لسلوك OOM اقرأ «Memory Interface Files» وتحديداً memory.events وmemory.oom.group.

الخلاصة — الساق الثانية اكتملت

namespaces
عزل الرؤية · I
cgroups
عزل الموارد · II

بهما معاً، عمليتك معزولةٌ في رؤيتها و ملجومةٌ في استهلاكها. fork bomb لم تعد تقتل المضيف. انتقلت من «وهم عزل» هشٍّ إلى عزلٍ له أسنان. وكل علامات الموارد في Docker (--memory, --cpus, --pids-limit...) صارت شفّافة: كلٌّ كتابةٌ في ملف cgroup.

بذرة الغموض

عمليتك معزولةٌ وملجومة، لكن مِمّ يتكوّن نظام ملفاتها؟ في كونتينرك اليدوي نسخت bash ومكتباته يدوياً — لا يتوسّع. كيف يملك Docker آلاف الصور دون أن ينسخ نظام ملفٍ كامل لكل كونتينر؟ عندك ٥٠ كونتينر من ubuntu نفسها: الساذج ينسخ ٥٠٠ميجا × ٥٠؛ الـ kernel عنده حيلةٌ أذكى — نظام ملفٍ واحد ظاهر، مبنيٌّ من طبقاتٍ شفّافة، أغلبها مشتركةٌ وللقراءة فقط.