المحرّك: لماذا أربعة شياطين لا واحد؟
بنيت كل شيءٍ بيدك المتعبة. 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 وحده لا يكفي:
- شغّل
runc runفي طرفية، ثم اقتل عملية runc الأصلية. ماذا حدث للكونتينر؟ من سيلتقط رمز خروجه؟ - تخيّل أنك 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 يموت. وظائفه — كلٌّ يداوي ألماً عشتَه:
- يتبنّى الكونتينر ويبقى أباه: يلتقط رمز الخروج متى مات ولو بعد أيام. (دواء ب-١.)
- يفصل الكونتينر عن الـ daemon: الكونتينر ابنٌ للـ shim لا dockerd. لذا تُرقّي Docker والكونتينرات تبقى حيّة — «live restore». (دواء ب-٢ — السبب الجوهري للـ shim.)
- يمسك stdio: يحتفظ بالمخرجات حتى لو لم يتّصل أحد، فلا تُفقد عند
docker logs/attach. - يبلّغ بالحالة: يخبر 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.
تتبّع docker run من الأعلى للأسفل
عند docker run -d --memory=64m nginx:
- عميل
docker→ POST إلى dockerd عبر الـ socket. - dockerd يطلب من containerd التأكد من صورة
nginx(يسحبها إن لزم — VII). - containerd يفكّ الطبقات ويجهّز snapshot الـ overlay (III) ⇒ rootfs.
- dockerd يبني
config.json: namespaces، حدّ 64m (II)، القدرات المنزوعة (IV)، الشبكة (VI). - containerd يطلق shim، والـ shim يستدعي runc مع الـ bundle.
- runc ينفّذ
config.json(I–IV)، يولّد العملية، ثم يموت. - الـ shim يبقى أباً؛ يلتقط المخرجات ورمز الخروج. dockerd يردّ بمعرّف الكونتينر.
اللغز — أعِد بناء سلسلة الاستدعاء، وأثبت الفصل
- runc يدوي كامل: أكمل جدول «حقل
config.json↔ بدائية I–IV» كاملاً. لا تترك حقلاً مجهولاً. - أثبت أن dockerd ليس الأب: ارسم شجرة العمليات، حدّد الـ shim، أعد تشغيل docker وأثبت بقاء الكونتينر ورمز خروجه. وثّق من التقطه.
- تكلّم مع 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.