Docker والحاويات

الحاويات مقابل الأجهزة الافتراضية

18 دقيقة الدرس 1 من 30

الحاويات مقابل الأجهزة الافتراضية

قبل كتابة ملف Dockerfile واحد، تحتاج إلى نموذج ذهني دقيق لما تعنيه الحاوية فعلاً على مستوى النواة. المهندسون الذين يتخطون هذه الخطوة يتعاملون مع Docker كصندوق أسود ويصطدمون بأعطال غامضة في الإنتاج — من ثغرات تصعيد الامتيازات الأمنية إلى العمليات التي تتجاوز حدود مواردها المتوقعة. هذا الدرس يبني النموذج من المبادئ الأولى.

نموذج الجهاز الافتراضي

يحقق الجهاز الافتراضي العزل عن طريق محاكاة مكدس أجهزة كامل. يجلس المشرف الافتراضي (VMware ESXi، KVM، Hyper-V، AWS Nitro) بين العتاد الفيزيائي ونظام تشغيل أو أكثر. تحصل كل نسخة على شريحة من المعالج والذاكرة والقرص تبدو — من منظور النسخة — كأجهزة مخصصة. تُشغّل النسخة نواتها الخاصة، وتدير صفحات ذاكرتها الخاصة، وتشغّل نظام init الخاص بها (systemd، OpenRC، إلخ).

هذا النموذج قوي: خطأ في نواة النسخة لا يستطيع إتلاف نواة المضيف لأن النواتين لا تتشاركان الذاكرة أبداً. سطح الهجوم بين الأجهزة الافتراضية متعددة المستأجرين هو المشرف الافتراضي، وهو قاعدة كود صغيرة ومُراجَعة بعمق. تعتمد مزودو السحاب متعددو المستأجرين (AWS، GCP، Azure) على هذا الضمان لتشغيل عملاء متنافسين على نفس الجهاز الفيزيائي.

لكن النموذج ثقيل أيضاً. تشغيل جهاز افتراضي يستلزم تحميل نواة كاملة (ثوانٍ إلى دقائق)، وتخصيص ذاكرة لعبء OS (غالباً 200 ميغابايت إلى 1 غيغابايت من RAM فقط لنظام التشغيل الضيف قبل أن يبدأ تطبيقك)، والحفاظ على صورة قرص كاملة. بدء تشغيل 50 جهازاً افتراضياً استجابةً لارتفاع مفاجئ في الحركة يُقاس بالدقائق لا بالثواني.

نموذج الحاوية: بدائيان من نواة النظام

الحاويات ليست مفهوماً جديداً اخترعه Docker. إنها ميزة من نواة Linux قام Docker بتغليفها في تجربة مطور قابلة للاستخدام عام 2013. يؤدي نظامان فرعيان من نواة النظام العمل الحقيقي: مساحات الأسماء (namespaces) ومجموعات التحكم (cgroups).

مساحات الأسماء: ما يمكنك رؤيته

مساحة الأسماء في Linux هي غلاف حول مورد نظام عام يجعل العملية الداخلية تعتقد أن لديها نسخة معزولة خاصة بها من ذلك المورد. تحتفظ النواة بمساحات أسماء منفصلة لـ:

  • pid — مساحة أسماء معرّف العملية. PID 1 داخل الحاوية هو مجرد عملية عادية على المضيف (مثلاً PID 3841)، لكن الحاوية تراه PID 1. لا تستطيع الحاوية رؤية عمليات المضيف أو الإشارة إليها أو إلى عمليات حاويات أخرى.
  • net — مساحة أسماء الشبكة. تحصل كل حاوية على واجهة loopback خاصة وجدول توجيه خاص وقواعد iptables خاصة ومجموعة مقابس خاصة. يمكن لحاويتين ربط المنفذ 8080 دون تعارض لأنهما تعيشان في مساحتَي أسماء شبكة مختلفتين.
  • mnt — مساحة أسماء التحميل. عرض نظام الملفات للحاوية معزول. ترى الحاوية نظام ملفات جذر (طبقات الصورة) مختلفاً عن / الخاص بالمضيف. يمكنك تحميل أدلة المضيف في هذه المساحة، وهو أساس وحدات تخزين Docker.
  • uts — مساحة أسماء UTS. يمكن للحاوية أن تمتلك اسم مضيف واسم نطاق خاصَّين بها، مستقلَّين عن المضيف.
  • ipc — مساحة أسماء الاتصال بين العمليات. مقاطع الذاكرة المشتركة والإشارات معزولة لكل مساحة أسماء، مما يمنع الاتصال بين الحاويات.
  • user — مساحة أسماء المستخدم. تُعيّن معرفات المستخدم داخل الحاوية إلى معرفات مختلفة على المضيف. جذر الحاوية (UID 0) يمكن أن يُعيَّن إلى UID غير متميز (مثلاً UID 100000) على المضيف — أساس الحاويات عديمة الجذر.
  • cgroup (Linux 4.6+) — يُخفي تسلسل cgroup للمضيف عن الحاوية، بحيث ترى فقط حدود مواردها الخاصة كحدود المستوى الأعلى.
الفكرة الأساسية: مساحات الأسماء تجيب على سؤال "ما الذي تستطيع هذه العملية رؤيته؟" العملية المحتواة هي لا تزال عملية Linux عادية. إنها لا تعمل في نواة مختلفة — بل في نفس نواة كل عملية أخرى على المضيف، لكن عرضها للنظام محدد لمساحة أسمائها.

يمكنك فحص عضوية مساحة الأسماء لأي عملية مباشرة من المضيف:

# سرد جميع مساحات الأسماء التي تنتمي إليها الصدفة الحالية ls -la /proc/$$/ns/ # المخرج (مختصر) — كل رابط رمزي يُسمّي نوع مساحة الأسماء ورقم inode: # lrwxrwxrwx 1 root root 0 Jun 11 08:00 cgroup -> 'cgroup:[4026531835]' # lrwxrwxrwx 1 root root 0 Jun 11 08:00 net -> 'net:[4026531993]' # lrwxrwxrwx 1 root root 0 Jun 11 08:00 pid -> 'pid:[4026531836]' # شغّل حاوية وقارن أرقام inode مساحة الأسماء الخاصة بها مع صدفة المضيف: docker run --rm -d --name demo nginx:alpine CPID=$(docker inspect demo --format '{{.State.Pid}}') echo "Host shell net ns: $(readlink /proc/$$/ns/net)" echo "Container net ns: $(readlink /proc/$CPID/ns/net)" # ستكون أرقام inode مختلفة — مما يؤكد عزل الشبكة. # يمكنك أيضاً الدخول إلى مساحة أسماء حاوية من المضيف (قوي للتشخيص): nsenter --target $CPID --net ip addr # ينفّذ \'ip addr\' داخل مساحة أسماء شبكة الحاوية بدون صدفة.

مجموعات التحكم: ما يمكنك استهلاكه

مساحات الأسماء تتحكم في الرؤية. مجموعات التحكم (cgroups) تتحكم في الاستهلاك. مجموعة التحكم هي آلية نواة لتجميع العمليات وفرض حدود على الموارد التي تستهلكها مجتمعةً. يُترجم Docker كل خيار --memory و--cpus و--pids-limit إلى إدخالات cgroup في /sys/fs/cgroup/.

تختلف نسختا cgroup اختلافاً كبيراً:

  • cgroups v1 (القديمة) — لكل متحكم موارد (cpu، memory، blkio، pids، …) تسلسله الهرمي المستقل الخاص تحت /sys/fs/cgroup/<controller>/. يمكن أن تكون العملية في مواقع مختلفة في تسلسلات هرمية مختلفة في آنٍ واحد — معقد وعرضة للخطأ.
  • cgroups v2 (الموحّدة، الافتراضية منذ نواة 5.8 / Ubuntu 22.04 / RHEL 9) — تسلسل هرمي موحّد واحد. جميع المتحكمات تحت /sys/fs/cgroup/. نموذج تفويض أبسط، دعم أفضل للحاويات عديمة الجذر، معلومات ضغط المهلة (PSI) للذاكرة والمعالج، وسلوك أفضل لقاتل OOM. استخدم v2 دائماً على الأنظمة الجديدة.
# التحقق من إصدار cgroup الذي يعمل عليه المضيف: stat -fc %T /sys/fs/cgroup/ # cgroup2fs = v2 (موحّدة) tmpfs = v1 (قديمة) # تشغيل حاوية بحدود موارد صريحة: docker run --rm -d \ --name limited-app \ --memory="256m" \ --memory-swap="256m" \ # تعطيل المبادلة (swap = حد الذاكرة، لا swap إضافي) --cpus="0.5" \ # 50% من نواة معالج واحدة --pids-limit=100 \ # حد أقصى 100 عملية/خيط داخل الحاوية nginx:alpine # العثور على مجموعة cgroup التي وُضعت فيها الحاوية (cgroups v2): CPID=$(docker inspect limited-app --format '{{.State.Pid}}') cat /proc/$CPID/cgroup # 0::/system.slice/docker-<container-id>.scope # فحص حد الذاكرة الفعلي الذي تفرضه النواة: cat /sys/fs/cgroup/system.slice/docker-$(docker inspect limited-app --format '{{.Id}}').scope/memory.max # 268435456 (= 256 * 1024 * 1024 بايت — بالضبط ما طلبناه)
مصيدة إنتاجية: ضبط --memory دون --memory-swap يسمح للحاوية باستخدام swap إضافي يساوي حد الذاكرة (إجمالي swap = ضعف الذاكرة). على مضيف به استخدام مبادلة مكثّف، يسبب هذا ارتفاعات حادة في زمن الاستجابة. اضبط الخيارَين دائماً، أو اضبط --memory-swap مساوياً لـ --memory لتعطيل المبادلة كلياً للأحمال الحساسة للكمون.

الفرق المعماري — مرئياً

يُظهر الرسم البياني أدناه بالضبط ما يُشارَك وما يُعزَل في كل نموذج. هذا هو الرسم الذي يجب استيعابه:

VM stack vs Container stack side by side Virtual Machines Physical Hardware (CPU / RAM / Disk) Hypervisor (KVM / ESXi / Nitro) VM 1 Guest OS + Kernel Libs / Runtime App A VM 2 Guest OS + Kernel Libs / Runtime App B Each VM: full OS + kernel + libs (~500 MB+, seconds to boot) Containers Physical Hardware (CPU / RAM / Disk) Host OS Kernel (SHARED) namespaces + cgroups isolate each container Container Runtime (containerd + runc) Container 1 Libs / Runtime App A Container 2 Libs / Runtime App B Each container: libs only (~5-100 MB, milliseconds to start) No guest kernel — kernel calls go directly to host
يسار: كل جهاز افتراضي يحمل نظام تشغيل ضيف ونواة كاملة، معزولة بواسطة المشرف الافتراضي. يمين: تتشارك الحاويات نواة المضيف — مساحات الأسماء تُوفّر العزل، ومجموعات التحكم تُفرض حدود الموارد. المقايضة الجوهرية هي عمق حدود الأمان مقابل سرعة البدء والكثافة.

لماذا انتصرت الحاويات (تشغيلياً)

يوفر نموذج الحاوية ثلاثة مزايا عملية قادت اعتماده على نطاق واسع:

  • وقت البدء: تبدأ عملية الحاوية في 50-300 ملليثانية لأنه لا توجد نواة يجب إقلاعها. يحتاج الجهاز الافتراضي 5-60 ثانية حتى مع صورة مُحسَّنة. على نطاق Kubernetes — حيث يُنشأ ويُتلَف pods باستمرار استجابةً للحمل — يحدد هذا الفرق ما إذا كان التوسع التلقائي يمكنه مواكبة ارتفاعات الحركة.
  • الكثافة: جهاز افتراضي بـ 4 vCPU / 16 GB RAM يفقد قرابة 1-2 غيغابايت لنظام التشغيل قبل أن يرى تطبيقك بايتاً. الحاويات لا تُضيف تقريباً أي عبء OS (نواة المضيف تعمل بالفعل). على نفس العتاد ربما تُشغّل 5 أجهزة افتراضية أو 150 حاوية، وهو المحرك الاقتصادي وراء تنسيق الحاويات.
  • قابلية نقل الصورة: صورة الحاوية تُجمّع بالضبط المكتبات والملفات الثنائية التي يحتاجها التطبيق. الصورة التي تجتاز pipeline CI الخاص بك هي نفس الحزمة الثنائية الحرفية التي تعمل في الإنتاج. مع الأجهزة الافتراضية، كانت مشكلة "يعمل على جهازي" قد استُبدلت جزئياً بـ "يعمل في التجهيز" — الانجراف في الإعداد بين بنيات الصور وصور الجهاز الافتراضي الأساسية كان عبئاً تشغيلياً مستمراً.
ممارسة احترافية: في الإنتاج، الحاويات والأجهزة الافتراضية متكاملة، ليست متنافسة. تُشغّل Google وAWS وAzure الحاويات داخل أجهزة افتراضية. الجهاز الافتراضي يوفر حدود الأمان على مستوى المشرف الافتراضي (عزل متعدد المستأجرين) بينما توفر الحاويات الكثافة والجدولة السريعة داخل تلك الحدود. عقد Kubernetes هي أجهزة افتراضية؛ pods التي تعمل عليها هي حاويات. افهم الطبقتين.

المقايضة الأمنية — ما ليست عليه الحاويات

النواة المشتركة هي مصدر سرعة الحاويات — وقيدها الأمني الرئيسي. إذا استغلت عملية داخل حاوية ثغرة نواة (هروب من الحاوية)، يمكنها الوصول إلى المضيف وجميع الحاويات الأخرى عليه. سطح الهجوم هذا غير موجود مع الأجهزة الافتراضية، لأن ثغرة النواة في جهاز افتراضي ضيف لا يمكنها عبور المشرف الافتراضي.

يعالج gVisor من Google وFirecracker من Amazon هذا بإضافة طبقة عزل إضافية — gVisor يتوسط في استدعاءات النظام بنواة في فضاء المستخدم، Firecracker يشغّل الحاويات داخل أجهزة افتراضية خفيفة (microVMs) تُقلع في 125 ملليثانية. يدعم Kubernetes نفسه واجهة برمجية RuntimeClass لجدولة pods محددة على بيئات تشغيل أكثر عزلاً.

لمعظم الأحمال على كتلة خاصة، توفر مساحات أسماء Linux + cgroups + ملفات تعريف seccomp + AppArmor/SELinux عزلاً كافياً. لـ SaaS متعدد المستأجرين (تشغيل كود عملاء غير موثوق) أو أي شيء يعالج بيانات منظَّمة حساسة على بنية تحتية مشتركة، تكون الحجة الأمنية المتعمقة لـ gVisor أو Firecracker قوية.

الفكرة الأساسية: الحاوية هي آلية عزل العمليات، ليست حدود أمان بنفس معنى الجهاز الافتراضي. معرفة هذا الفرق تمنع التقليل من أمان الحاويات (مساحات الأسماء توفر فعلاً عزلاً حقيقياً ضد الأخطاء العرضية) والمبالغة في تقدير أمانها (CVE في النواة يمكنه مع ذلك تعطيل كل شيء).

ربط الأجزاء: ما يحدث عند تشغيل حاوية

عند تنفيذ docker run nginx:alpine، يحدث التسلسل التالي — معظمه في أقل من 300 ملليثانية:

  1. يُرسل Docker CLI طلب gRPC إلى Docker daemon (dockerd).
  2. يُفوّض dockerd إلى containerd (بيئة التشغيل القياسية للصناعة، وهي مشروع CNCF الآن).
  3. يستدعي containerd runc (بيئة التشغيل منخفضة المستوى المتوافقة مع OCI) لإنشاء الحاوية.
  4. يستدعي runc نداء clone(2) مع علامات مساحات الأسماء لإنشاء عملية جديدة في مساحات أسماء جديدة.
  5. يكتب runc إدخالات cgroup تحت /sys/fs/cgroup/ لفرض حدود المعالج والذاكرة.
  6. تُحمَّل طبقات الصورة كنظام ملفات تراكبي (OverlayFS) على المضيف وتُقدَّم للحاوية كنظام ملفاتها الجذري.
  7. تبدأ عملية الحاوية — ترى PID 1 وواجهة شبكة خاصة ونظام ملفات معزول.

هذا هو النموذج الذهني الكامل: مساحات الأسماء لعزل الرؤية، ومجموعات التحكم لفرض الموارد، ونظام ملفات طبقي لقابلية نقل الصورة. كل شيء آخر في Docker — ملفات Dockerfile والوحدات التخزينية والشبكات وCompose — مبني فوق هذه البدائيات الثلاثة.