الجذر: مِمّ يتكوّن عالم العملية؟
عزلنا الرؤية والموارد. الآن: ما نظام الملفات الذي تراه العملية حين تنظر إلى
/؟ في كونتينرك اليدوي نسخت bash بيدك — حلٌّ لا يتوسّع. هنا
الحلّ الذي يتوسّع، ولماذا اتخذ شكل طبقات.
النبذة
الـ image ليس «ملفاً» ولا قرصاً افتراضياً. إنه كومةٌ من طبقات، كل طبقةٍ مجموعة فروقاتٍ على نظام ملفات: «أضِف هذه، عدّل تلك، احذف هذه». حين تشغّل كونتينراً، يكدّس الـ kernel الطبقات شفّافةً عبر union filesystem فتبدو نظام ملفٍ واحد. الطبقات كلها للقراءة فقط ومشتركة؛ ويُضاف فوقها طبقةٌ رقيقةٌ واحدة قابلة للكتابة خاصة بكل كونتينر. كل هذا وُلد من سؤالٍ اقتصاديٍّ واحد: كيف لا ننسخ؟
اللغز المستفزّ — نظام ملفٍ من العدم، بلا نسخ
عندك مجلدان يمثّلان «طبقتين»: base/ فيه app.txt=v1 وconfig.txt=default، وfeature/ فارغ. أنشئ merged/ بحيث:
- ترى فيها
app.txtوconfig.txtمنbase/(دون نسخهما). - الكتابة في
merged/app.txt=v2لا تغيّرbase/app.txt— يبقىv1. - إنشاء
merged/new.txtيظهر في «مكان التعديلات» لا فيbase/. - حذف
merged/config.txtيختفي منmerged/لكن يبقى فيbase/. (أصعب نقطة: كيف «تحذف» ملفاً من طبقةٍ للقراءة فقط دون تعديلها؟)
إن كانت الطبقة السفلى للقراءة فقط، فكيف يُسجَّل «هذا محذوف»؟ لا بدّ من علامةٍ في طبقةٍ أعلى تقول للـ union «تجاهل ما تحتك بهذا الاسم». ما شكلها؟ ابدأ من man mount وابحث عن نوع fs يقبل lowerdir, upperdir, workdir.
لحظة «آها»: «الحذف» ليس حذفاً، بل قناع (whiteout) — وهذا يفسّر لاحقاً لماذا حذف ملفٍ كبير في Dockerfile لا يصغّر الصورة.
الدرس — OverlayFS، وكيف صار image format
ليش الطبقات؟ الدافع الاقتصادي
بلا طبقات: كل صورة نسخةٌ كاملة، وكل كونتينر نسخة. عشرة كونتينرات = ١٠ جيجا من بتاتٍ مكرّرة، ومع كل تعديل سطر تعيد رفع جيجابايت. لا يتوسّع.
محتوى مُعنوَن بالمحتوى (content-addressable) + طبقاتٌ للقراءة فقط مشتركة.
الطبقة تُعرَّف بـ digest (هاش SHA-256 لمحتواها). طبقتان متطابقتان =
تُخزَّنان مرة واحدة. هذا يعطي: تكرار صفري على القرص، نقلٌ تفاضلي عند pull، وكاش بناء.
الطبقات تتراكم ولا تُطرَح. إن أضفت ملف ١ جيجا في طبقة ثم «حذفته» أعلى، فالطبقة السفلى ما زالت تحمله — الحذف مجرد قناع فوقها. لهذا «نظّف في نفس الـ RUN»: الحذف في RUN لاحق يضيف طبقةً بقناع، ولا يحذف البايتات.
ليش OverlayFS تحديداً (وما سبقها)
جرّب Docker عدّة «storage drivers»، كلٌّ محاولةٌ لنفس المشكلة:
| driver | الفكرة / المشكلة |
|---|---|
AUFS | الأصلي، لم يدخل الـ kernel الرسمي قط (خارج الشجرة) فكان عبئاً. |
devicemapper | على مستوى الكتل لا الملفات؛ معقّد وبطيء كثيراً. |
btrfs/zfs | أنظمة ملفاتٍ كاملة بلقطاتها؛ قوية لكن تفرض نظام ملفٍ معيّناً. |
overlay2 | الحاضر: في الـ kernel، على مستوى الملفات، بلا متطلبات. انتصر لأنه الأبسط الرسمي. |
الدرس المتكرّر: ما يدخل الـ kernel الرسمي وبأبسط نموذج يفوز على الأقوى الخارجي.
كيف تعمل OverlayFS — النموذج الدقيق
bash$ mount -t overlay overlay \ -o lowerdir=base,upperdir=feature,workdir=work \ merged
قواعد الدمج (استنتجها لا تحفظها):
- قراءة: من الأعلى للأسفل، أول طبقةٍ فيها الاسم تفوز.
- كتابة على ملفٍ سفلي — copy-up: يُنسخ أولاً إلى
upperdirثم يُعدَّل؛ السفلي يبقى. (سرّ بطء أول كتابةٍ على ملفٍ كبير.) - حذف ملفٍ سفلي — whiteout: ملف جهازٍ خاص (char device بـ 0/0) في
upperdirبنفس الاسم، يفسّره الـ kernel كـ«محجوب». لهذا الحذف لا يحرّر مساحة الأسفل. - حذف مجلد: «opaque dir» يقول «تجاهل كل ما تحت هذا الاسم سفلياً».
افحص بنفسك بعد اللغز: بعد حذف config.txt، نفّذ ls -l feature/ وابحث عن ملفٍ بنوعٍ غريب — هذا الـ whiteout. رؤيته بعينك تثبّت المفهوم.
من OverlayFS إلى OCI image
الوصلة الكبرى: طبقة الكونتينر = lowerdir. الصورة كومة طبقات overlay محزومةٌ للنقل. مواصفة OCI Image Spec تعرّف:
- كل طبقة = أرشيف
tar(اختيارياً مضغوط) يحوي الفروقات. الحذف يُمثَّل بملفات.wh.<name>يترجمها الـ runtime إلى whiteout عند الاستخراج. - digest لكل طبقة =
sha256:...= العنوان والهوية والمفتاح. - config (JSON) =
Env,Cmd,Entrypoint, وrootfs.diff_ids(قائمة هاشات الطبقات بترتيبها)، وhistory. - manifest (JSON) = يربط config بقائمة الطبقات؛ يُعنوَن هو نفسه بـ digest.
image@sha256:...يعنون الـ manifest. - manifest list / index = (multi-arch) يشير إلى manifests، واحدٌ لكل معمارية.
أين يخزّنها Docker؟ تحت /var/lib/docker/overlay2/ (الطبقات جاهزةً للتركيب) وimage/ (الـ manifests والـ configs). استكشفه في اللغز.
اللغز — فكّك صورةً حقيقية وأعِد تركيبها
- التشريح:
docker pull alpineثمdocker save alpine -o alpine.tar. فُكّ وتجوّل:index.json→ manifest → config + الطبقات. افتح طبقةً (هي tar). ارسم بيدك الشجرة index→manifest→config+layers مع الـ digest المختصر بجانب كل عقدة. لا تكمل قبل أن ترى السلسلة كاملة بعينيك. - إعادة التركيب: ركّب طبقات alpine المفكوكة بـ
mount -t overlay، والناتجmerged/rootfs صالح. ثم — وصلٌ بالإقليم I — استخدمه جذراً لكونتينرك اليدوي عبرpivot_root. صنعت كونتينر Alpine حقيقياً من طبقاتٍ حقيقية، بلا Docker. ذروة الإقليم. - اللاتوسّع المعكوس: أنشئ ملف ١٠٠ميجا في طبقة، قِس الحجم، «احذفه» أعلى، أعد القياس. أثبت أن الحجم لم ينقص. اربطه بقاعدة الـ Dockerfile.
مواصفة OCI Image Spec (opencontainers/image-spec): اقرأ manifest.md لبنية الـ manifest، config.md لـ rootfs.diff_ids، layer.md لتمثيل الـ whiteouts (.wh. و.wh..wh..opq).
الخلاصة — الأرجل الثلاث، والصورة تتّضح
من هذه الثلاث يُشتقّ مفهوم الكونتينر بالكامل. كل ما بقي (المعمارية، الشبكة، البناء، التوزيع) تنظيمٌ وأتمتة لها، لا مفاهيم جديدة من الجذر. هذه لحظة الصورة الكبرى: النقاط المتفرّقة بدأت تلتحم.
content-addressing (شجرة Merkle) سيعود بقوة في التوزيع (VII): كيف يتشارك الـ registry الطبقات عبر الشبكة بلا تكرار = نفس فكرة الـ digest. لكن أولاً: namespaces عزلت الرؤية، cgroups الموارد، overlay الملفات... فماذا يُسمح للعملية أن تفعل حين تكون «root» بداخلها؟ كلمة «root» ليست مفتاحاً واحداً، بل حزمةٌ من ~٤٠ مفتاحاً منفصلاً — Docker يرمي معظمها قبل أن يسلّمك الكونتينر.