DOCKER::ROOT الإقليم V · runc · containerd · shim · dockerd kernel live
REGION V المعمارية فوق البدائيات OCI runtime spec

المحرّك: لماذا أربعة شياطين لا واحد؟

بنيت كل شيءٍ بيدك المتعبة. Docker يفعل ذلك بأمرٍ واحد — لكنه خلف الكواليس يوزّعه على أربع طبقات. القاعدة الذهبية هنا: كل طبقةٍ وُجدت لتداوي ألماً حقيقياً. سنعيش الألم لنفهم الدواء.

النبذة

العزل الذي بنيته (I–IV) هو بالضبط ما يفعله runc — لا أكثر. لكن تشغيل كونتينر ليس فقط «اخلق العملية المعزولة»؛ هو دورة حياةٍ كاملة: من يحمّل الصورة؟ من يبقى حيّاً ليلتقط رمز الخروج إن مات الكونتينر بعد ساعة؟ من يعيد التشغيل عند ترقية الـ daemon دون قتل الكونتينرات؟ من يعرض API؟ كل سؤالٍ ولّد طبقة.

اللغز المستفزّ — شغّل كونتينراً بلا daemon، ثم اكتشف نواقصه

الجزء أ — runc وحده:

bash
$ mkdir -p ~/c/rootfs && cp -a merged/* ~/c/rootfs/ # rootfs من الإقليم III $ cd ~/c && runc spec # يولّد config.json $ sudo runc run mycontainer

افتح config.json وتأمّله. ستجد — بصيغة JSON معلنة — كل ما فعلته يدوياً: قسم namespaces (I)، resources.linux (II)، root.path+mounts (III)، capabilities+seccomp (IV).

مهمتك: اربط كل حقل في config.json بالأمر اليدوي المقابل. أثبت بنفسك أن runc = أيديك العارية + ملف إعدادٍ معياري.

الجزء ب — اكتشف لماذا runc وحده لا يكفي:

  1. شغّل runc run في طرفية، ثم اقتل عملية runc الأصلية. ماذا حدث للكونتينر؟ من سيلتقط رمز خروجه؟
  2. تخيّل أنك daemon يدير ٥٠٠ كونتينر، كلٌّ أبوه عملية runc حيّة. أردت ترقية الـ daemon. ماذا يحدث للكونتينرات إن كانت أبناءً مباشرين له؟
هذان السؤالان هما الألم

وُلدت الطبقات العليا لتداويه. فكّر فيهما قبل أن تقرأ الحلّ المعماري.

الدرس — تشريح الطبقات الأربع، كلٌّ بألمها

ليش المواصفة أولاً: OCI Runtime Spec

في 2015، خوفاً من أن تحتكر Docker تعريف «الكونتينر»، أُنشئت OCI بمواصفتين: Image Spec (III)، وRuntime Spec تعرّف «bundle»: مجلدٌ فيه rootfs/ وconfig.json. أي runtime يقبل الـ bundle = متوافق. runc هو التنفيذ المرجعي. الفائدة: ظهرت runtimes بمقايضاتٍ مختلفة:

runtimeالمقايضة
crunبلغة C، أخفّ وأسرع من runc (Go)
gVisor (runsc)kernel مستخدمٍ بين الكونتينر والـ kernel — sandbox أقوى، أداء أقل
Kataكل كونتينر في VM خفيفة — عزل hypervisor (لا kernel مشترك)، بثمن

كلهم يقبلون نفس config.json. علامة --runtime= تختار أيّهم — اشتقاق مباشر.

الطبقة ١: runc — المنفّذ اللحظي (وألمه)

يقرأ الـ bundle، ينفّذ كل ما في config.json (يخلق namespaces بـ clone، يكتب cgroups، يركّب الـ rootfs بـ pivot_root، يسقط القدرات، يحمّل seccomp)، ثم exec على الهدف. ثم ينتهي runc ويموت. هذا يخلق الألم: إن مات runc، من يبقى أباً للكونتينر؟ من يحصد رمز خروجه؟ ⇒ الـ shim.

الطبقة ٢: containerd-shim — الأب الخالد (الدواء)

عمليةٌ صغيرة تبقى حيّة طوال عمر الكونتينر كأبٍ له، بينما runc يموت. وظائفه — كلٌّ يداوي ألماً عشتَه:

  1. يتبنّى الكونتينر ويبقى أباه: يلتقط رمز الخروج متى مات ولو بعد أيام. (دواء ب-١.)
  2. يفصل الكونتينر عن الـ daemon: الكونتينر ابنٌ للـ shim لا dockerd. لذا تُرقّي Docker والكونتينرات تبقى حيّة — «live restore». (دواء ب-٢ — السبب الجوهري للـ shim.)
  3. يمسك stdio: يحتفظ بالمخرجات حتى لو لم يتّصل أحد، فلا تُفقد عند docker logs/attach.
  4. يبلّغ بالحالة: يخبر containerd بالموت عبر آليةٍ مستقلة عن شجرة عمليات الـ daemon.
bash اكتشف بعينك
$ ps -ef --forest | grep -A2 shim # أبو الكونتينر = containerd-shim، لا dockerd $ sudo systemctl restart docker # والكونتينر يبقى حيّاً ← هذا هو الـ shim

الطبقة ٣: containerd — مدير دورة الحياة والصور

يدير: تحميل الصور، فكّ الطبقات وتركيب الـ overlay (III)، قاعدة بيانات الكونتينرات، الـ snapshots، استدعاء الـ shim عند create/start/stop. daemon مستقلٌّ كامل (مشروع CNCF) يستعمله Kubernetes مباشرةً بلا Docker. هذا الفصل سمح لـ k8s أن يتخلّى عن Docker (2020، «dockershim removal») ويكلّم containerd رأساً.

الطبقة ٤: dockerd — الواجهة وتجربة المستخدم

لا يلمس namespaces ولا cgroups بنفسه؛ يفوّض كله لـ containerd. وظيفته الودودة: REST API عبر /var/run/docker.sock، بناء الصور (BuildKit، VII)، شبكات Docker (VI)، الـ volumes والـ plugins، Compose/Swarm.

FIG 1 الطبقات الأربع — التفويض للأسفل
docker CLI │ REST عبر /var/run/docker.sock ▼ dockerd ──── الصور عالية المستوى، الشبكات، volumes، البناء │ gRPC ▼ containerd ── تحميل الصور، snapshots، دورة الحياة، الحالة │ يستدعي ▼ shim ──────── أبٌ خالدٌ لكل كونتينر، يحصد الخروج، يمسك stdio │ exec ▼ runc ──────── namespaces+cgroups+mounts+caps+seccomp ثم يموت │ ▼ kernel ────── لا يعرف كلمة "كونتينر" — فقط عمليات خلف عدسات
الحدود بين الطبقات عقودٌ واضحة (OCI specs + gRPC) — لهذا k8s استبدل dockerd وأبقى containerd، وتُبدّل runc بـ gVisor، وتُرقّي dockerd دون قتل الكونتينرات.

تتبّع docker run من الأعلى للأسفل

عند docker run -d --memory=64m nginx:

  1. عميل docker → POST إلى dockerd عبر الـ socket.
  2. dockerd يطلب من containerd التأكد من صورة nginx (يسحبها إن لزم — VII).
  3. containerd يفكّ الطبقات ويجهّز snapshot الـ overlay (III) ⇒ rootfs.
  4. dockerd يبني config.json: namespaces، حدّ 64m (II)، القدرات المنزوعة (IV)، الشبكة (VI).
  5. containerd يطلق shim، والـ shim يستدعي runc مع الـ bundle.
  6. runc ينفّذ config.json (I–IV)، يولّد العملية، ثم يموت.
  7. الـ shim يبقى أباً؛ يلتقط المخرجات ورمز الخروج. dockerd يردّ بمعرّف الكونتينر.

اللغز — أعِد بناء سلسلة الاستدعاء، وأثبت الفصل

  1. runc يدوي كامل: أكمل جدول «حقل config.json ↔ بدائية I–IV» كاملاً. لا تترك حقلاً مجهولاً.
  2. أثبت أن dockerd ليس الأب: ارسم شجرة العمليات، حدّد الـ shim، أعد تشغيل docker وأثبت بقاء الكونتينر ورمز خروجه. وثّق من التقطه.
  3. تكلّم مع containerd مباشرة: بـ ctr شغّل كونتينراً بلا dockerd إطلاقاً (ctr image pull ثم ctr run). قارن ما يتطلّبه ctr (يدوي) بما يخفيه docker run (شبكة، volume، restart). الفرق هو تعريف dockerd الوظيفي.
المصادر

OCI Runtime Spec (opencontainers/runtime-spec): config.md وconfig-linux.md لمطابقة كل حقل ببدائيته. ولـ shim اقرأ README لـ containerd/containerd قسم architecture وruntime/v2/README.md.

الخلاصة — من اللبنات إلى المعمارية

أكملت القفزة من «البدائيات» إلى «كيف تُنظَّم في نظامٍ إنتاجي». OCI specs حوّلت الكونتينر من منتج Docker إلى عقدٍ معياري. runc = أيديك + config.json. الطبقات الأربع ليست تعقيداً عبثياً؛ كلٌّ تداوي ألماً: runc يموت ⇒ shim يخلد؛ ترقية ⇒ live restore؛ إدارة الصور ⇒ containerd؛ الواجهة والبناء ⇒ dockerd.

بذرة الغموض

في config.json رأيت قسم namespaces فيه network، وحين شغّلت كونتينرك بـ net namespace جديد كان معزولاً بلا أي اتصال (بذرة I). كيف يصل dockerd الكونتينرَ بالإنترنت وبعضِه ببعض؟ ما هو docker0 فعلاً؟ العزل كان أسهل جزء؛ الوصل هو الفن: كابلٌ افتراضي، مبدّلٌ افتراضي، وقاعدة NAT.