پرش به مطلب اصلی

Docker In Action

نام کتاب: داکر در عمل

ویرایش: دوم

سال چاپ:۱۳۹۶

سال خوندن من: ۱۴۰۴ بهار


مقدمه

این قسمت بیشتر در مورد فلسفه داکر و چرایی اش هست و اینکه چرا داکر باعث serviceability longevity در برنامه هامون میشه

داکر چیه؟

داکر یک ابزار لجستیکی که برای بسته بندی و حمل و نقل کانتینر ها هستش و همه این هارو به کمک مفهموی به اسم containers که در سیستم عامل ها هست انجام میده. داکر ابزار جدیدی اختراع نکرده و و همه چیزهای که که در کنار هم قرار داده وجود داشته صرفا پیچیدگی پیاده سازی کانتینر ها یا jail هارو از دید ما پنهون کرده که باعث شده ایزوله سازی برای ما کار راحتی باشه مثل اکتشاف کانتینر برای اولین بار. داکر از این مفاهیم برای ایزوله کردن استفاده میکنه:

  • UTS namespace -> host and domain name
  • MNT namespace -> filesystem access and structure
  • IPC namespace -> process communication over shared memory
  • NET namespace -> network access and structure
  • USR namespace -> usernames and identifier
  • chroot syscall -> control the location of filesystem root
  • cgroup -> resource protection
  • CAP drop -> OS feature restriction
  • security modules

بخاطر همین موارد داکر توی ویندوز فقط با VM یا WSL اجرا میشه

کانتینر چیه؟

به صورت تاریخی اگه بخوایم بگیم سیستم عامل های UNIX-style مثل لینوکس از عبارتی مثل jail برای این موضوع استفاده میکردن. این باعث میشد که یه ران تایم داشته باشیم که محدوده دسترسی یک برنامه ای که jail شده مدیریت کنیم.

آیا کانتینر همون مفهوم VM یا Virtualization هستش؟ جواب کوتاه نه

جواب بلند: برخلاف VM ها داکر از هیچ منابع virtual سخت افزاری استفاده نمیکنه. برنامه های که داخل کانتینر هاست شدن مستقیما با کرنل در ارتباط هستن (با واسطه سیستم عامل) خیلی از برنامه های در isolation اجرا میشن بدون اینکه نیاز باشه مثل VM ها سیستم عامل کاملا بوت بشن.

با توجه به پیشرفت سخت افزار VM ها هم گزینه سریعی هستن. ولی فقط وقتی که OS کاملا بوت شود ولی این delay رو در داکر نداریم و از طرفی VM یک برنامه user space هستش ولی داکر kernel space هستش که باعث میشه syscall هامون سریعتر بشه و درنهایت از منابع بهینه استفاده میشه

تفاوت image با کانتینر چیه؟ به کامپوننتی که یه کانیتنر رو پر میکنه میگن image. داکر کانتینر هارو با image ها میسازن

  • یک Docker image مثل یک کانتینر فیزیکی حمل بار هست که اپلیکیشن و وابستگی‌هاش رو (به‌جز هسته سیستم‌عامل) با خودش حمل می‌کنه.

  • Image: الگوی فقط-خواندنی که شامل تمام وابستگی‌ها و تنظیمات لازم برای اجرای برنامه است

  • کانتینر: نمونه‌ی درحال اجرا از یک Image که لایه نوشتنی نیز به آن اضافه شده است

چه مشکلاتی رو داکر حل میکنه؟

انسجام

داکر مثل یک پرتال میمونه :) بعد از پایانش چیزی روی ذخیره نمیکن مگر ما بهش گفته بشیم که ذخیره کنه که این باعث میشه انسجام در سیستم خودمون داشته باشیم

پرتابیلیتی
  • اجرای یکسان روی هر سیستمی که داکر داشته باشه
  • استقرار در محیط‌های مختلف بدون تغییرات
امنیت

مثل زندان های معمولی(‍jail) هر چیزی که داخل کانتینر هستش فقط به چیزهای دسترسی داخلش وجود داره. مگر اینکه یه مجوزی به صورت explicit از سمت یوزر داده بشه. کانتینر ها محمدوده تاثیر روی برنامه های دیگه رو محدود میکنن، دیتای که میتونن دسترسی داشته باشن، شبکه ای که میتونن باشن، و ریسورس های که میتونن داشته باشن

چرا داکر مهمه

داکر یک انتزاع ایجاد میکنه. انتزاعی که به شما کمک میکنه روی مسايل سخت تر کار کنید. یعنی به جای اینکه فکرمون رو معطوف به چگونگی نصب و استقرار برنامه مون کنیم به نوشتن خوده برنامه فکر کنیم


بخش اول: Process Isolation And Enviroment Independet Computing

داکر از یک چیزی به اسم PID namespace برای ایزوله کردن پراسس های داخل کانتیر استفاده کرده(برای جزئییات داک PID رو پیدا کن)

کانتینر های داکر توی ۶ تا وضعیت هستن Created, Running, Restarting, Paused, Removing and Exited به صورت پیشفرض داکر توی کامند docker ps فقط کانتینر های در حال اجر رو نشون میده

داکر جلوی Circular Depdencies رو میگیره

داکر image layers

وقتی توی Dockerfile مثلاً از دستوراتی مثل RUN, COPY, ADD استفاده می‌کنی، Docker برای هر کدوم یه لایه جدید می‌سازه. به این لایه های میگن image layers(برای جزییات دنبال Union File system بگرد)

ساخت و تغییرات🏗️
  • وقتی از یک Dockerfile استفاده می‌کنی، هر دستور مثل RUN یا COPY باعث ساختن یک لایه جدید می‌شه.
  • همچنین می‌تونی داخل یک container تغییر بدی و بعد با ‍docker container commit‍‍ اون تغییرات رو به شکل یک لایه جدید ذخیره کنی.
  • وقتی داخل یک container فایلی رو تغییر بدی یا حذف کنی، اون تغییر توی بالا‌ترین لایه ثبت می‌شه. Docker از چیزی به‌اسم union filesystem استفاده می‌کنه تا این لایه‌ها رو ترکیب کنه.
  • اگر فایلی تغییر کنه، معمولاً نسخه‌ی جدیدش کپی می‌شه توی لایه بالا و تغییر روی اون اعمال می‌شه (این رو می‌گن copy-on-write). این می‌تونه روی performance و حجم imag تأثیر بذاره.
  • اگه تغییری در لایه ای اتفاق بیافته لایه های بعدی هم از کش استفاده نمیکنن و دوباره حساب میکنن
  • هر دستور در Dockerfile (مثل COPY, RUN, ADD, …) به همراه داده‌هایی که روی اون اثر می‌ذاره، تبدیل به یک هش (hash) می‌شه. و این یه جا ذخیره میشه داکر از روی این میفهمه که یه لایه تغییر کرده یا نه

داخل کانتینر ها ساختار ایزوله ای دقیقا مشابه با ساختار سیستم های unixی ساخته میشه و این به کمک mnt namespace برای ایجاد ایزولیشن هستش

Storage and Volume

هر داکر فایل در زمان اجرا یک writeable layer داره که روی همه لایه ها هست برای نوشتن تغییرات داخل یه کانتینر و وقتی کانتینر ها به دلیلی خارج میشن، هر چی که در اینجا نوشته شده دور ریخته میشه به همین دلیل هستش که ما برای دیتا های که طولانی مدت لازم داریم به volume احتیاج داریم

Union filesystem که داکر ازش استفاده می‌کنه برای اجرای موقت برنامه‌ها خوبه، چون از چند لایه ساخته شده و تغییرات فقط روی لایه بالایی نوشته می‌شن. اما برای نگه‌داری داده‌های بلندمدت یا اشتراک‌گذاری داده بین کانتینرها و سیستم میزبان مناسب نیست، چون این تغییرات ناپایدارن و با پاک شدن کانتینر از بین می‌رن.

در مقابل، لینوکس با استفاده از mount point به ما اجازه می‌ده مسیرهایی در فایل‌سیستم رو به دیسک‌های واقعی یا مجازی متصل کنیم. این ساختار باعث می‌شه داده‌ها مستقل از کانتینر باقی بمونن و بتونیم اون‌ها رو بین کانتینرها یا با میزبان به اشتراک بذاریم.

پس برای کار با داده‌های مهم و دائمی، باید از mount point (مثل volumeها در داکر) استفاده کنیم، نه صرفاً union filesystem.

فرض کن یه فلش USB داری که می‌خوای توی لینوکس بهش دسترسی داشته باشی.

🔹 بدون اینکه بدونی از کدوم دیسک استفاده می‌کنی:

وقتی فلش رو به سیستم وصل می‌کنی، لینوکس اون رو به یه مسیر مثل /media/mahdi/usb-drive mount می‌کنه.

از اون لحظه به بعد، تو فقط با اون مسیر کار می‌کنی، مثلا:

cd /media/mahdi/usb-drive

🧩 ارتباطش با کانتینر:

همین مفهوم توی کانتینر هم هست. مثلاً اگه بخوای یه دایرکتوری از سیستم میزبان رو داخل کانتینر ببری، انگار اون USB رو mount کردی تو کانتینر. از دید برنامه داخل کانتینر، انگار اون مسیر بخشی از فایل‌سیستمشه، ولی در واقع داره از یه جای دیگه میاد.

Networking

وقتی کانتینرها روی یک ماشین (هاست) اجرا میشن، باید بتونن با هم و با خارج هاست ارتباط برقرار کنن. داکر برای این ارتباط، یک لایه انتزاعی شبکه می‌سازه که به کانتینرها امکان می‌ده بدون وابستگی مستقیم به شبکه میزبان، به صورت ایزوله ولی قابل دسترسی کار کنن.

انواع شبکه‌های در داکر:

  • Bridge (پیش‌فرض): کانتینرها آدرس IP خصوصی منحصر به فرد می‌گیرن و می‌تونن با هم روی همین شبکه داخلی صحبت کنن. داکر با NAT ترافیک خروجی رو به بیرون منتقل می‌کنه.
  • Host: کانتینر شبکه میزبان رو مستقیماً استفاده می‌کنه، یعنی مثل یه برنامه عادی روی سیستم اجرا می‌شه و تمام شبکه میزبان رو داره. این حالت ایزوله نیست و کمتر توصیه می‌شه.
  • None: کانتینر هیچ شبکه‌ای نداره، فقط loopback داخلی داره. برای برنامه‌هایی که به شبکه نیازی ندارن مناسب است.

نقش پورت‌ها: برای دسترسی به برنامه‌های داخل کانتینر از خارج، باید پورت‌های کانتینر به پورت‌های میزبان متصل (publish) بشن. مثلاً -p 8080:80 یعنی پورت ۸۰ کانتینر روی پورت ۸۰۸۰ میزبان قرار می‌گیره.

وقتی یک کانتینر ایجاد می‌کنی، داکر براش یک network namespace جداگانه می‌سازه. این یعنی کانتینر:

  • کارت شبکه خودش رو داره (مثل eth0)
  • جدول routing خودش رو داره
  • آدرس‌های IP خودش رو داره بنابراین از دید سیستم‌عامل، کانتینر یه سیستم مستقل با شبکه مجزاست.

بخش دوم: Packaging software for distribution

تعیین کردن مموری لیمیت برای اینه که یک کانتینر نتونه از رم استفاده کنه که باعث پدیده گرسنگی starvation بشه. نکته اش اینه که مقداری که تعیین میکنیم، مقدار رزرو شده نیست یا حتی اینم تضمین نمیکنه در اون لحضه اونقدر RAM وجود داشته باشه فقط قراره جلوی over consumpation رو بگیره

ولی برای cpu قضیه کمی متفاوت هستش به این علت که برای cpu ما وزن تعیین میکنیم. به صورت پیشفرض همه کانتینرها ۱۰۲۴ هستنم که یعنی بار به طور مساوی تقسیم میشه اگه ما cpu یکی رو بزاریم ۲۰۴۸ وزن بیشتری دریافت میکنه

مانیتورینگ استفاده از cpu توسط کانتیرها برای اطمینان اینکه cpu share بدرستی کار میکنه ضروری هستش


-ا Image شامل چندین Layer است که به صورت stack روی هم قرار دارند

  • هر دستور در Dockerfile یک لایه جدید می‌سازه.
  • اTag اسم قابل خواندن و نسخه‌ای برای Image هست (مثال: nginx:latest).
  • اLayers جداگانه ذخیره می‌شن و هنگام تغییر فقط لایه جدید ساخته می‌شه، نه کل image.
  • هر Image محدودیتی در تعداد Layer داره (معمولاً 127 لایه).
  • کاهش تعداد Layer باعث بهبود performance، سرعت pull و push و کاهش استفاده از فضا می‌شه.
  • روش‌های کاهش سایز و لایه: ادغام چند دستور RUN در یک بلوک (مثلاً RUN apt-get update && apt-get install -y …). حذف فایل‌های موقت و کش در همان لایه مثلاً پاک کردن apt cache بعد نصب. استفاده از Base Image سبک مثل alpine به جای ubuntu. حذف لایه‌ها یا فایل‌های غیرضروری از Image.

ا entry point چیه؟

معمولا entry point برای startup اسکریپت ها مثلا مثلا seeder برای دیتابیس استفاده میشه پس فرقش با cmd چیه؟

  • ا**ENTRYPOINT** مثل فرمان اصلی کانتینره (ثابت، مگر با --entrypoint عوضش کنی).
  • ا**CMD** مثل پارامترهای پیش‌فرضه (با آرگومان‌های docker run میشه عوضش کرد).
FROM alpine
ENTRYPOINT ["echo"]
CMD ["Hello friend"]

output -> Hello friend ولی اگه حالا ایطوری رانش کنیم

docker run my-image "Hello Docker!"

output -> Hello Docker!

اARG چیه؟
پارامتریه که فقط در زمان ساخت ایمیج (دوران docker build) قابل استفاده است و بعد از ساخت ایمیج، دیگر در دسترس نیست. ولی ENV هم زمان build و هم در زمان Run در دسترسه

docker build --build-arg APP_VERSION=2.0 .

مدیریت پراسس ها در داکر

توی سیستم عامل ما یک systemd داریم که به عنوان PID1 اجرا میشه که وظیفه اش پاک کردن فرایند های زامبی هستش ولی توی داکر این پراسسی که اینارو مدیریت کنه نداریم. چیزی که ما توی entry point مینویسیم به عنوان PID 1 اجرا میشه ما میتونیم بیایم یک پراسس منیجر نصب کنیم که کار برای درست کنه.tini یک پراسس منیجر سبکه که میتونیم داشته باشیمش

RUN apt-get update && apt-get install -y tini
ENTRYPOINT ["tini", "--", "python", "app.py"]

ولی خب خوب اینه که لازم نیست این کار رو بکنیم چون داکر پیشفرض اینکارو برای ما کرده. حالا چجوری این مشکل رو حل کرده:

  • مشکل: وقتی به کانتینر docker stop بزنید، سیگنال SIGTERM فقط به PID 1 میرسه. اگر process اصلی شما این سیگنال را به فرزندهایش منتقل نکنه، به زور کیل میشن (SIGKILL بعد از ۱۰ ثانیه) و grace full shut down نداریم

  • راه‌حل Tini:

    • به عنوان PID 1 قرار می‌گیرد.
    • سیگنال‌ها را به درستی به تمام فرآیندهای فرزند منتقل می‌کند.

پس سه تا کار میکنه برامون

  1. گریسفول: اگر فرآیند اصلی شما این سیگنال را به فرزندها منتقل نکند، آن‌ها بعد از ۱۰ ثانیه با SIGKILL کیل می‌شوند (غیرگرسیفول)
  2. -پردارش زامبی حافظه اشغال نمیکنه، اما اسلات Process ID سیستم رو پر میکنه. در کانتینرهای طولانی‌مدت ممکنه به حداکثر تعداد فرآیند (pid_max) برسیم
  3. سیگنال‌ مثل: کلا هر سیگنال دیگه ای مثل SIGHUP و ... به child process ها نمیرسه اگه برناممون اینارو پردازش نکنه ممکنه درست کار نکنه

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

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