قسمت صفر در مورد اینکه هدف از این نوشته چی هستش و اینکه مسیر احتمالیمون به چه شکل هست صحبت کردیم. شروع کنیم.
کانتینر ها در واقع پروسس های عادی هستن که ایزوله و محدود شدن [با استفاده از یک سری از subsystem های کرنل و البته که همیشه یک سری ابزار و روش برای بهتر شدن وجود داره و اگه نداشته باشه، ساخته میشه]. برای یادگیری کانتینر ها تصمیم گرفتم از یک بخش برای شروع استفاده کنم و بر مبنای اون جلو برم، برای شروع تصمیم گرفتم از بخش هایی از کرنل که برای ایجاد کردن کانتینر ها نیاز داریم [namespace ها و cgroups] شروع کنم و جلو برم.
فقط قبل از شروع دو تا موضوع:
۱- توی متن بجای کلمه namespace از دو کلمه <فضای نام> و <ns> هم استفاده شده که هر سه یک معنی رو دارن توی این متن.
۲- سیستم عاملی که من ازش استفاده میکنم ubuntu 20.04 و کرنل 5.8.0 هستش. بعضی از مواردی که بررسی میکنیم به مرور زمان به کرنل اضافه شدند و ممکنه بعضی کرنل ها اون هارو ساپورت نکنه.
با استفاده از فضا های نام(namespaces) میتونیم یک سری از منابع رو برای پروسس ها محدود کنیم بصورتی که پروسس هامون فقط همون بخش از منابع رو که بهشون اختصاص دادیم رو بتونن ببینن و در واقع پروسس نه تنها فقط میتونه به منابع موجود در فضای نام خودش دسترسی داشته باشه بلکه از سایر فضا های نامِ موجود اطلاعی نداشته باشه.
البته کلمه منابع شاید کمی گمراه کننده بنظر بیاد، اینجا منظور از منابع، میزان استفاده از رم و CPU نیستش (اون ها رو با استفاده از cgroups انجام میدیم). منظور از منابع چیز هایی مثل ماونت پوینت هایی که هر پروسس میبینه و یا hostnameی که هر پروسس میبینه است.
میشه گفت محدودیت اینجا بیشتر به معنی ایزوله کردن هستش.
تا الان ۸ تا فضای نام به کرنل اضافه شده که همراه یه توضیح خلاصه و نهچندان دقیق ببینیم چی هستن تا در ادامه جزییات بیشتری رو باهم بررسی کنیم:
mnt: ماونت پویتت هارو ایزوله میکنه برای پروسس (کلا هر کانتینر میتونه یه روت دایرکتوری متفاوت داشته باشه اما خب خیلی امن تر از chroot. توی chroot امکان ایزوله کردن وجود نداره)
pid: دقیقا خود process id هارو ایزوله میکنه (در هر کانتینر میتونی process hierachy مجزایی داشته باشیم)
net: دستگاه های شبکه، جداول روتینگ، iptables و... رو ایزوله میکنه. (هر کانتینر بتونه شبکه خودش رو داشته باشه و مثلا بتونیم روی یک سیستم همزمان دو کانتینر داشته باشیم که به پورت ۸۰ خودشون گوش میدن)
ipc: این فضای نام System V IPC, POSIX message queues رو ایزوله میکنه.
UTS: مقدار hostname رو ایزوله میکنه (داخل هر کانتینر میتونیم hostname متفاوتی داشته باشیم)
user: آیدی های یوزر ها و گروه هارو ایزوله میکنه. (میتونیم داخل کانتینر یوزر روت داشته باشیم که بیرون از کانتینر یک یوزر معمولی هستش)
cgroups namespace: این رو بعد از اینکه با cgroups آشنا شدیم بهتر میتونیم متوجه بشیم اما یک سری فایل و دایرکتوری مرتبط با cgroup هارو ایزوله میکنه.
time namspace: ساعت های Boot and monotonic رو ایزوله میکنه (یکیشون شامل تایمی که suspend باشه هم میشه و اون یکی نه).
هر پروسس داخل یک فضای نام از هر هشت نوع فضای نامی که وجود داره هستش و فقط میتونه همون منابع رو ببینه و تغییر ایجاد کنه. پروسس هایی که خارج از اون فضای نام ها هستند نمیتونن تغییر رو ببینن اون ها توی دنیای کوچیک و متفاوت خودشون زندگی میکنن.
این به این معنی نیست که ما وقتی از داکر استفاده میکنیم از تمام فضا های نام استفاده میشه. مثلا در مورد فضای نام time که میتونه uptime سیستم رو ایزوله کنه این موضوع رو تست کنیم:
$ docker run -it ubuntu:20.04 /bin/bash
خب الان میتونیم توی شل یک سری موارد رو چک کنیم. مثلا با نوشتن hostname ببینیم هاست نیممون چی هستش و البته اینکه uptime داخل کانتینر چقدر هستش. عکس زیر رو ببینید. بالایی سیستم عامل هستش و پایینی کانتینر:
خب همونطور که میبینید با اینکه hostname ها متفاوت هستند (داخل ۲ فضای نام uts متفاوت قرار دارن)، اما uptime ها یکی هستن. فضای نام time اخیرا به کرنل اضافه شده.
و خب سیستم عامل موقع بوت شدن از هر نوع فضای نام یکی رو داره که بهشون intial-namespace اون نوع از فضای نام گفته میشه (میشه اینطوری بهش نگاه کرد که عملا ما سیستم عاملمون داره از کانتینر ها بهرحال استفاده میکنه و یه کانتینر غول پیکره که کانتینر های دیگه میشه داخلش باشن).
بریم بررسی کنیم نحوه ارتباطمون با کرنل رو برای استفاده از فضا های نام.
The /proc/[pid]/ns/ directory:
خب داخل این فولدر یک تعدادی symbolic link وجود داره که نشون میده هر پروسسی در کدام یکی از فضا های نام قرار داره.
با استفاده از دستور ls -l و یا readlink میتونیم محتویاتشون رو نگاه کنیم.
$ ls -l /proc/$$/ns | awk '{print $1, $9, $10, $11}'
که خب خروجی زیر رو خواهیم دید:
lrwxrwxrwx cgroup -> cgroup:[4026531835] lrwxrwxrwx ipc -> ipc:[4026531839] lrwxrwxrwx mnt -> mnt:[4026531840] lrwxrwxrwx net -> net:[4026531992] lrwxrwxrwx pid -> pid:[4026531836] lrwxrwxrwx pid_for_children -> pid:[4026531836] lrwxrwxrwx time -> time:[4026531834] lrwxrwxrwx time_for_children -> time:[4026531834] lrwxrwxrwx user -> user:[4026531837] lrwxrwxrwx uts -> uts:[4026531838]
همونطوری که دیدیم محتویات هر کدوم از فایل ها به این صورت هستش:
ns-type : inode number
خب الان اگر پروسس دیگه ای باشه که داخل همین فضای نام uts باشه با خوندن فایل /proc/[PID]/ns /uts باید خروجی یکسانی ببینیم. بیاید برگردیم به همون مثالی که داخل اوبونتو چک کردیم برای uptime ها. دوباره یک کانتینر میسازیم و bash رو داخلش اجرا میکنیم.
خب همونطور که دیدیدم هردو پروسس داخل یک time ns قرار داشتند برای همین uptime یکسانی میدیدیم اما بخاطر اینکه توی uts ns های متفاوتی بودن، hostname متفاوتی رو میدیدن.
داخل این فولدر فایل هایی وجود داره که حداکثر تعداد ns ها رو به ازای هر user ns مشخص میکنه.
max_cgroup_namespaces max_ipc_namespaces max_mnt_namespaces max_net_namespaces max_pid_namespaces max_time_namespaces max_user_namespaces max_uts_namespaces
این محدودیت ها بر اساس user namespace ای که پروسس در اون قرار داره اعمال میشه و اینکه هر فضای نام جدیدی که میسازید برای فضای نام قبلی هم محسابه میشه . برای همین با ساختن user namespace جدید نمیشه این محدودیت رو دور زد.
مقدار اولیه برای intial user namespace نصف تعداد نخ هایی هستش که میتونن ساخته بشن (/proc/sys/kernel/thrads-max).
یک سری system call وجود دارن که کارهایی که میخوایم رو با nsها انجام میدن[کلا syscall هارو به چشم یک اینترفیس برای کار کردن با کرنل در نظر بگیرید بعنوان کسی که قراره ازش استفاده کنه]. بصورت خلاصه یک نگاهی بهشون بکنیم بعد از تموم شدن مرور namespaces و cgroups ازشون استفاده میکنیم و وارد جزییات بیشتری میشیم.
clone: یک process ایجاد میکنه اما داخل ns های جدیدی که بهش میگیم.
setns: به یک پروسس اجازه میده بین ns هایی که وجود دارن جابجا بشن.
unshare: یک سری نیم اسپیس جدید ایجاد میکنه و پروسسی که فراخوانیش کرده رو میبره به اون ها.
ioctl: با استفاده ازش میشه یک سری اطلاعات رو بدست آورد مثل اینکه user ns یک پروسس چی هستش.
دستورات شل:
یک سری دستور هم وجود دارن که از اون ها میتونیم داخل کامندلاین خودمون استفاده کنیم برای کار با فضا های نام.
nsenter: وارد نیم اسپیسی که وجود داره میشه و یه کامند اجرا میکنه.
unshare: یک سری نیم اسپیس ایجاد میکنه و داخلش یه کامند اجرا میمیکنه.
درحالت عادی ns ها با بسته شدن تمام پروسس های اون ns (یا اینکه با setns اون ns رو ترک کنن)، از بین میرن. اما خب عواملی هستن که باعث میشن این انفاق نیوفته. اینکه فایل موجود در پوشه /proc/[pid]/ns/ در جایی bind mount شده باشه یا باز باشن و یا اینکه اون ns خودش دارای یک ns دیگه باشه.
الان وقتشه که هر کدوم از فضای نام هامون رو بررسی کنیم و ازشون استفاده کنیم.
با UTS میتونیم دو تا از identifier های سیستممون رو ایزوله کنیم برای پروسسمون.
1- hostname: برای ما hostname رو ایزوله میکنه و خب ما میتونیم داخل هر کانتینر hostname خودمون رو داشته باشیم.
2- NIS domain name: از این ویژگی استفاده ای نمیکنیم ما اما برای به اشتراک گذاشتن یک سری دیتا از سرور ها استفاده میشه.
شاید این سوال پیش بیاد خب ما وقتی یک UTS ns جدید ایجاد کردیم، مقدار اولیه hostname ما چی میشه؟ خب برای جواب این سوال باید برگردیم به کمی عقب تر، وقتی یک پروسس فضای نام UTS جدیدی ایجاد میکنه، مقادیر identifier ها از فضای نام پروسسی که clone و یا unshare رو برای ساخت اون فضای نام فراخوانی کرده رو کپی میکنه برای UTS ns جدید.
بریم کمی از ns ها استفاده کنیم.برای این کار از دو تا ترمینال استفاده میکنیم تا راحت تر بتونیم مقایسه کنیم.
خب داخل هر دو ترمینال میتونیم با استفاده از $$ آیدی پروسسی که ازش استفاده میکنیم رو ببینیم و با readlink محتویات symlink رو نگاه کنیم.
از اونجایی که هردو پروسس در یک UTS ns قرار دارن طبیعی هستش که که hostname یکسانی داشته باشن. بیاید حالا یک شل داخل یک uts ns جدید ایجاد کنیم در ترمینال بالایی.
از -u برای ایجاد uts ns جدید استفاده میکنیم و bash کامندی هست که در اون میخوایم اجرا کنیم. hostname پروسس bash مون رو تغییر میدیم به test اما hostname داخل شل پایینی هنوز تغییر نکرده. خب به این دلیل هستش که UTS ns متفاوتی با پروسس بالایی داره. بیاید از شل پایینی وارد UTS ns شل بالایی بشیم.
خب اینجا با nsenter وارد uts ns پروسس شل بالایی میشیم. برای اینکار اول PID پروسس بالایی رو بدست میاریم. با سوییچ -t میتونیم به nsenter بگیم که وارد فضای نامِ مرتبط با کدوم پروسس بشیم که میگیم ۳۵۴۹۹ و با -u مشخص میکنیم کدوم ns از اون پروسس رو میخوایم واردش بشیم که گفتیم uts و با bash گفتیم چه پروسسی رو داخل اون uts ns اجرا بکنه.
همونطوری که میبینیم اینجا hostname تغییر میکنه به test بخاطر تغییر فضای نام که با readlink قابل چک کردن هستش این موضوع.
خب فکر کنم برای قسمت اول کافی باشه. اگر انتقاد و یا پیشنهادی داشتید اطلاع بدید، مرسی. قسمت بعدی بقیه فضای نام ها میریم و بررسی میکنیمشون.