مبانی کانتینرها و معماری Docker

مشکل کلاسیک: «روی سیستم من کار می‌کرد!»

فرض کنید توسعه‌دهنده‌ای کدی را با Python 3.11 و نسخه‌ای خاص از کتابخانه‌ها روی سیستم شخصی خود اجرا و تست کرده است. کد بدون هیچ مشکلی کار می‌کند. اما زمانی که همین کد روی سرور توزیع می‌شود:

  • سیستم‌عامل سرور از Python 3.8 قدیمی‌تر استفاده می‌کند.
  • پکیج‌های سیستم‌عامل (System Libraries) یا نسخه‌های متفاوتی از وابستگی‌ها روی سرور نصب هستند.
  • متغیرهای محیطی، فایل‌های پیکربندی یا دسترسی‌های دایرکتوری‌ها همخوانی ندارند.

نتیجه چیست؟ سرور به خطا می‌خورد و زمان زیادی از تیم برای تطبیق محیط و پیدا کردن مغایرت‌ها تلف می‌شود. به این چالش Environmental Drift (تغییر تدریجی و ناهماهنگی محیط‌ها) می‌گویند.

کانتینرها (Containers) برای حل همین مشکل به وجود آمدند. یک کانتینر نه‌تنها کد برنامه، بلکه تمام نیازمندی‌ها، باینری‌ها، پکیج‌ها و تنظیمات را در قالب یک واحد ایزوله، سبک و قابل‌حمل بسته‌بندی می‌کند تا در هر جایی (روی لپ‌تاپ توسعه‌دهنده، سرور تست یا کلاود) دقیقاً به یک شکل اجرا شود.


تفاوت کانتینر با ماشین مجازی (Virtual Machine)

برای درک بهتر کانتینر، مقایسه‌ی آن با ماشین مجازی (VM) بهترین نقطه شروع است.

ماشین مجازی (VM)

ماشین‌های مجازی به کمک لایه‌ای به نام Hypervisor (مانند KVM، VMware یا VirtualBox) سخت‌افزار سرور فیزیکی را شبیه‌سازی می‌کنند. درون هر ماشین مجازی یک سیستم‌عامل کامل (Guest OS) مستقل با هسته (Kernel) اختصاصی خود نصب می‌شود.

  • مزیت: ایزولاسیون کامل و قوی در سطح سخت‌افزار.
  • معایب: مصرف بالای منابع (هر ماشین نیازمند رم، دیسک و پردازنده‌ی اختصاصی برای اجرای سیستم‌عامل کامل خود است) و بوت طولانی (چند ده ثانیه یا چند دقیقه).

کانتینر (Container)

کانتینر یک سیستم‌عامل جداگانه نیست! تمام کانتینرهای روی یک سرور، از هسته‌ی سیستم‌عامل میزبان (Host OS Kernel) به‌صورت مشترک استفاده می‌کنند. هر کانتینر صرفاً یک یا چند پردازش معمولی لینوکس است که به کمک قابلیت‌های هسته‌ی لینوکس در یک فضای کاربر (User Space) ایزوله شده‌اند.

  • مزیت: حجم دیسک فوق‌العاده اندک (مگابایت به‌جای گیگابایت)، راه‌اندازی در کسری از ثانیه (میلی‌ثانیه)، و چگالی بالای استقرار روی سرور.
  • معایب: اشتراک‌گذاری هسته (Kernel) به این معنی است که نمی‌توان هسته‌ی متفاوتی (مثل ویندوز روی هاست لینوکسی) را درون کانتینر اجرا کرد.
ویژگیماشین مجازی (VM)کانتینر (Container)
سطح ایزولاسیونشبیه‌سازی سخت‌افزاری (Hypervisor)ایزولاسیون در سطح پردازش هسته (OS Kernel)
هسته‌ی سیستم‌عامل (Kernel)اختصاصی برای هر ماشین مجازی (Guest OS)مشترک با سیستم‌عامل میزبان (Host OS)
زمان راه‌اندازی (Boot Time)دقیقه‌ها یا ده‌ها ثانیهمیلی‌ثانیه‌ها تا چند ثانیه
مصرف منابعگیگابایت حافظه و دیسک برای هر VMمگابایت، بسیار سبک و اشتراکی
تعداد روی یک سرورده‌ها عددصدها تا هزاران کانتینر

پایه‌های کانتینر در لینوکس: Namespaces و Cgroups

کانتینر ساختار سخت‌افزاری مستقلی ندارد؛ بلکه بر پایه‌ی دو قابلیت اصلی در هسته‌ی لینوکس شکل گرفته است:

  1. Namespaces (ایزولاسیون دیدگاه): تعیین می‌کنند که یک پردازش چه چیزهایی را می‌تواند ببیند. برای مثال، Namespace پردازش‌ها (PID) باعث می‌شود پردازش داخل کانتینر شماره PID=1 داشته باشد و پردازش‌های خارج از آن فضای نام را نبیند. همچنین Network Namespace رابط‌های شبکه و پورت‌های اختصاصی ایجاد می‌کند.
  2. Control Groups یا Cgroups (محدودیت منابع): تعیین می‌کنند که یک پردازش چه مقدار منبع می‌تواند مصرف کند. هسته‌ی لینوکس از طریق cgroups سقف مصرف رم، CPU، و Disk I/O را برای پردازش‌های یک کانتینر محدود و مانیتور می‌کند.

معماری Docker: پشت صحنه چه می‌گذرد؟

بسیاری از افراد تصور می‌کنند Docker یک برنامه‌ی بزرگ یکپارچه است، اما در واقعیت یک اکوسیستم چندلایه‌ای ماژولار است که بر اساس استانداردهای بین‌المللی بنیاد OCI (Open Container Initiative) طراحی شده است.

معماری موتور Docker از اجزای زیر تشکیل شده است:

 1[ Docker CLI (Client) ]
 2          │
 3          │ (REST API / Socket: /var/run/docker.sock)
 4          ▼
 5[ Docker Daemon (dockerd) ]
 6          │
 7          │ (gRPC)
 8          ▼
 9[ containerd ]
10    ├── containerd-shim
11    │        │
12    │        ▼
13    │   [ runc ] ──► [ Container Process ]
14    └── ...

۱. کلاینت داکر (Docker CLI)

دستور docker که در ترمینال وارد می‌کنید (مثل docker run یا docker ps)، کلاینت Docker است. این برنامه کانتینرها را اجرا نمی‌کند، بلکه دستورات کاربر را از طریق REST API به Daemon ارسال می‌کند.

۲. دیمن داکر (Docker Daemon / dockerd)

سرویسی پایدار در پس‌زمینه که به سوکت یونیکس /var/run/docker.sock گوش می‌دهد. دیمن مسئول دریافت درخواست‌ها، ساخت ایمیج‌ها (Build)، مدیریت حجم‌ها (Volumes) و پیکربندی شبکه‌های داکر (Networks) است. دیمن مدیریت مستقیم کانتینرها را به لایه‌ی پایین‌تر واگذار می‌کند.

۳. کانتینر-دی (containerd)

یک Container Runtime سطح بالا و استاندارد (پروژه‌ی مستقل تحت بنیاد CNCF). وظیفه‌ی containerd مدیریت چرخه‌ی کامل حیات کانتینر، دانلود و ذخیره‌سازی ایمیج‌ها از رجیستری، و نظارت بر وضعیت پردازش‌هاست. (همین containerd مستقیماً توسط Kubernetes نیز استفاده می‌شود).

۴. ران‌سی (runc) و استاندارد OCI

یک Container Runtime سطح پایین و سبک بر اساس استاندارد OCI. وقتی containerd می‌خواهد کانتینری بسازد، runc را فراخوانی می‌کند. ابزار runc مستقیماً با هسته‌ی لینوکس صحبت می‌کند، Namespaces و Cgroups را می‌سازد، پردازش را استارت می‌زند و بلافاصله خارج می‌شود.

۵. شیم (containerd-shim)

برای اینکه اگر Docker Daemon یا containerd کرش کند یا به‌روزرسانی (Restart) شود، کانتینرهای در حال اجرا قطع نشوند، یک پردازش واسط بسیار سبک به نام containerd-shim برای هر کانتینر فعال می‌ماند تا ورودی/خروجی (I/O) و کد وضعیت خروج کانتینر را نگه دارد.


خلاصه

  • کانتینرها وابستگی‌ها و کدهای نرم‌افزار را یکدست می‌کنند تا روی هر سیستمی قابل پیش‌بینی اجرا شوند.
  • کانتینر ماشین مجازی نیست؛ سیستم‌عامل مستقل ندارد، بلکه از هسته‌ی مشترک لینوکس با تکیه بر Namespaces و Cgroups استفاده می‌کند.
  • داکر از کلاینت (docker)، دیمن (dockerd)، لایه‌ی رانتایم (containerd) و مجری نهایی لینوکسی (runc) تشکیل شده است.

منابع یادگیری

مستندات و مقالات

ویدیو