Namespaces va cgroups: har bir container ortidagi kernel mexanikasi
Linux containerlari maqolasida container kernel majburlaydigan ikki turdagi cheklovga ega oddiy jarayon ekani aniqlangan edi: tizimning cheklangan ko'rinishi va cheklangan resurs byudjeti. Bu maqola kernel ikkalasini aynan qanday amalga oshirishini Docker va Podman ichkarida chaqiradigan o'sha quyi darajadagi vositalar yordamida ochib beradi. Shunda container endi qora quti bo'lmay qoladi — u qo'lda ishga tushirish mumkin bo'lgan aniq, takrorlanadigan buyruqlar birikmasiga aylanadi.
Nimalarni o'rganasiz
Bu maqola oxirida siz quyidagilarni qila olasiz:
- Linux namespace’larining yettita turini va ularning har biri nimani izolyatsiya qilishini aytish;
- hech qanday container runtime’siz,
unsharebilan qo'lda izolyatsiya qilingan jarayon yaratish; - ishlab turgan container’ning namespace’larini to'g'ridan-to'g'ri host’dan tekshirish va ularga kirish;
- cgroups v1 va v2 o'rtasidagi farqni tushuntirish va tizim qaysi biridan foydalanayotganini tekshirish;
- cgroups’dan bevosita foydalanib jarayonga qat’iy CPU va xotira cheklovini o'rnatish.
Namespaces: jarayon nimani ko'ra olishini boshqarish
Namespace global kernel resursini shunday o'raydiki, namespace ichidagi jarayon o'sha resursning o'ziga xos shaxsiy nusxasini ko'radi va u shu hostdagi boshqa barcha namespace’lardan izolyatsiya qilingan bo'ladi. Kernel hozirda container’larga tegishli yettita namespace turini belgilaydi:
| Namespace | Nimani izolyatsiya qiladi | Kernel flag'i |
|---|---|---|
| PID | Jarayon ID’lari va jarayonlar daraxti | CLONE_NEWPID |
| Network | Tarmoq interfeyslari, routing jadvallari, portlar | CLONE_NEWNET |
| Mount | Fayl tizimi mount nuqtalari | CLONE_NEWNS |
| UTS | Hostname va NIS domen nomi | CLONE_NEWUTS |
| IPC | System V IPC obyektlari, POSIX xabarlar navbatlari | CLONE_NEWIPC |
| User | Foydalanuvchi va guruh ID’larining moslashtirilishi (mapping) | CLONE_NEWUSER |
| Cgroup | Jarayonning o'z cgroup ierarxiyasini ko'rinishi | CLONE_NEWCGROUP |
Namespace yo yangi jarayonni ishga tushirishda clone(2) tizim chaqiruviga mos CLONE_NEW* flag'ini uzatish orqali, yoki allaqachon ishlayotgan jarayonni yangi namespace’ga ko'chirish uchun unshare(2) ni chaqirish orqali yaratiladi. Buyruq qatori vositasi bo'lgan unshare aynan shu tizim chaqiruvi ustidagi yupqa o'ram. Ya’ni u namespace izolyatsiyasini umuman Docker yoki Podman o'rnatilmagan holda ham ko'rsata oladi.
Sakkizinchi namespace: Time
Linux 5.6 (2020) Time namespace’ini (CLONE_NEWTIME) qo'shdi. U CLOCK_MONOTONIC va CLOCK_BOOTTIME soatlarini izolyatsiya qiladi, shunda jarayon host’nikidan boshqa tizim ishlash vaqtini (uptime) ko'rishi mumkin. U yuqoridagi yettitadan ataylab chiqarib qo'yilgan, chunki bu container runtime’lari standart holatda deyarli ishlatmaydigan yagona namespace. Docker va Podman container’larni alohida time namespace’ga joylashtirmaydi, shuning uchun container’ning uptime qiymati host’nikini aks ettiradi. Agar intervyuer "nechta namespace bor" deb so'rasa, aniq javob: "kernel’da sakkiztasi belgilangan, ulardan yettitasini har bir container runtime ishlatadi, time namespace — istisno".
PID namespace: shaxsiy jarayonlar daraxti
Bu shell ichida bash ning o'zi PID 1 — odatda hostning init/systemd i egallaydigan o'sha PID — va haqiqiy hostdagi boshqa hech bir jarayon umuman ko'rinmaydi. --mount-proc yangi namespace ichida /proc ni qayta mount qiladi, shunda ps host’nikini emas, namespace’ning o'z ko'rinishini o'qiydi. Usiz ps baribir hostning to'liq jarayonlar ro'yxatini ko'rsatardi, chunki /proc ning o'zini PID namespace emas, mount namespace boshqaradi va ishonarli izolyatsiya qilingan ko'rinish uchun ikkalasini birga sozlash kerak. Bu shell’dan chiqish (exit) darhol oddiy host PID namespace’iga qaytaradi.
Intervyuda: container ichida PID 1 nega muhim?
PID 1 har qanday Unix’ga o'xshash tizimda maxsus vazifalarga ega: u yetim qolgan jarayonlar uchun standart manzil (ota jarayoni o'lgan bola jarayon PID 1 ga qayta biriktiriladi) va zombie jarayonlarni wait() bilan yig'ib olishi (reap) kerak. PID namespace tufayli o'zini PID 1 deb hisoblaydigan container’ning asosiy jarayoni ko'pincha bu vazifalarni bajarmaydi — ko'pchilik ilova binary’lari hech qachon init tizimi bo'lish uchun yozilmagan. Oddiy ilova binary’sini bevosita ishga tushiradigan container’larda zombie jarayonlar to'planishining va docker run --init (yoki tini kabi aniq kichik init) mavjudligining sababi aynan shu. U ilovadan oldin minimal, to'g'ri ishlaydigan PID 1 ni qo'yadi va PID namespace xatti-harakatining bevosita, amaliy oqibati bo'lgan muammoni hal qiladi.
Network namespace: shaxsiy interfeyslar to'plami
Yangi yaratilgan network namespace’da faqat loopback interfeysi bo'ladi va u o'chiq holatda boshlanadi — hech qayerga, hatto uni yaratgan hostga ham route yo'q. Har bir container’ning network namespace’i aynan shu boshlang'ich nuqtadan boshlanadi. Container runtime’ining vazifalaridan biri container’ning network namespace’i va hostdagi bridge o'rtasida virtual Ethernet juftligini (veth) ulashdir. Container’ga docker run -p va standart bridge tarmog'i tayanadigan chiquvchi ulanishni aslida aynan shu beradi.
Mount namespace: fayl tizimi daraxtining shaxsiy ko'rinishi
Mount namespace jarayonga host’nikidan mustaqil, o'z mount nuqtalari to'plamiga ega bo'lish imkonini beradi. Bu container’ning hostnikidan butunlay alohida o'z root fayl tizimiga (/) ega bo'lib ko'rinishi ortidagi primitiv. Linux containerlari maqolasida aniqlanganidek, bunda alohida kernel yoki fayl tizimi drayveri ishtirok etmaydi: bu o'sha host fayl tizimi, faqat shu bitta jarayonlar daraxti uchun boshqa, cheklangan ko'rinish mount qilingan.
User namespace: ichkaridagi root’ni tashqaridagi root bo'lmagan foydalanuvchiga moslashtirish
User namespace container xavfsizligi bilan eng bevosita bog'liq. U jarayonga namespace ichida root bo'lish — shu namespace boshqaradigan har qanday narsa uchun to'liq root imkoniyatlari bilan — va shu bilan birga hostda imtiyozsiz, oddiy foydalanuvchiga moslashtirilgan bo'lish imkonini beradi.
Oxirgi qator — moslashtirish: namespace ichidagi UID 0 uning tashqarisidagi UID 1000 ga (oddiy foydalanuvchi) mos keladi, 1 ta UID oralig'i uchun. Bu namespace ichida root bo'lgan jarayon qandaydir yo'l bilan boshqa izolyatsiya chegaralaridan chiqib ketsa, hostga haqiqiy root sifatida emas, UID 1000 sifatida tushadi. Bu container’dan chiqib ketish holatida blast radius’ni sezilarli darajada kamaytiradi. Bu rootless container’lar ortidagi aynan o'sha kernel mexanizmi. Docker va Podman: amaliy ishlash maqolasi uni vositalar tomonidan ko'rib chiqadi: Podman’ning rootless rejimi butun container’ni, jumladan o'zini root deb hisoblaydigan jarayonni ham, container engine’ining o'ziga hostda haqiqiy root imtiyozlari umuman kerak bo'lmagan holda ishga tushiradi.
Haqiqiy container’ning namespace’larini tekshirish
Ishlab turgan jarayon tegishli bo'lgan har bir namespace /proc/<pid>/ns/ ostida ko'rinadi:
lrwxrwxrwx 1 root root 0 pid -> 'pid:[4026532451]'
lrwxrwxrwx 1 root root 0 net -> 'net:[4026532454]'
lrwxrwxrwx 1 root root 0 mnt -> 'mnt:[4026532449]'
lrwxrwxrwx 1 root root 0 uts -> 'uts:[4026532452]'
Har bir manzil (pid:[4026532451]) — noyob namespace identifikatori. Bir xil identifikatorga ega ikki jarayon ta’rifga ko'ra bir namespace’da bo'ladi. nsenter host’dan mavjud container’ning namespace’lariga qo'shilish uchun aynan shu /proc/<pid>/ns/ yozuvlaridan foydalanadi — docker exec ostida aynan shunday amalga oshirilgan:
Container namespace’larini nishonga olgan nsenter orqali hostname ni ishga tushirish host’nikini emas, container’ning o'z hostname’ini (standart holatda uning qisqa container ID’si) qaytaradi. Bu docker exec web hostname Docker’ning maxsus imkoniyati emas, balki do'stonaroq buyruqqa o'ralgan aynan shu nsenter mexanizmi ekanining bevosita, amaliy isboti.
cgroups: jarayon qancha iste’mol qila olishini boshqarish
Namespace’lar "bu jarayon nimani ko'ra oladi?" degan savolga javob beradi. Control groups (cgroups) esa boshqa savolga javob beradi: "bu jarayonga qancha CPU, xotira va I/O ishlatishga ruxsat berilgan?" Resurs cheklovisiz nazoratdan chiqqan bitta container jarayoni butun hostning xotirasi yoki CPU’sini tugatib, boshqa har bir ish yukini resurssiz qoldirishi mumkin. cgroups — kernel’ning aynan shu nosozlikka qarshi majburlash mexanizmi. Shuningdek, systemd resurs boshqaruvlari va umuman jarayonlar ustuvorligi kernel darajasida aynan shu orqali amalga oshiriladi.
cgroups v1 va v2
Har bir controller (cpu, memory, blkio va hokazo) uchun bir nechta alohida cgroup qatori emas, bitta cgroup2 mount qatori cgroups v2 ni bildiradi. Bu hozirgi Ubuntu Server LTS va Red Hat Enterprise Linux relizlari standart holatda foydalanadigan yagona ierarxiya. cgroups v1 har bir resurs controller’ini butunlay alohida ierarxiya sifatida tashkil qilgan. Bu jarayonni CPU bo'yicha bitta daraxtda, xotira bo'yicha esa unga aloqasiz boshqa daraxtda cheklashga imkon berardi. Bu moslashuvchan, lekin chalkash edi va controller’lar cheklov qaysi jarayonga tegishli ekanida kelisha olmasligiga moyil edi. cgroups v2 har bir jarayon uchun barcha controller’larni bitta ierarxiyaga birlashtiradi. Bu haqida fikr yuritish soddaroq va endi u har qanday hozirgi distributivda standart. Eski cgroup (v1) fayl tizimi hali ham eski tizimlarda yoki container runtime aniq v1 mosligini so'raganda paydo bo'lishi mumkin.
Bu joriy yagona cgroup ierarxiyasida mavjud bo'lgan har bir controller’ni sanab o'tadi. Quyidagi misollarda memory va cpu ishlatiladi.
Xotira va CPU cheklovini qo'lda o'rnatish
Warning
Quyidagi buyruqlar /sys/fs/cgroup/ ostida cgroups’ni bevosita yaratadi va o'zgartiradi. Cheklanayotgan jarayon xotira cheklovidan oshib ketsa, kernel’ning OOM mexanizmi uni o'ldiradi. Buni production jarayoniga qarshi emas, bir martalik lab VM yoki container’da ishga tushiring va unga murojaat qiladigan har qanday buyruqni ishga tushirishdan oldin maqsadli PID’ni tasdiqlang.
Yangi yaratilgan cgroup uchun memory.max standart holatda max — cheklanmagan. Aniq qiymat o'rnatish qat’iy yuqori chegarani majburlaydi:
echo 104857600 | sudo tee /sys/fs/cgroup/demo/memory.max
echo 50000 100000 | sudo tee /sys/fs/cgroup/demo/cpu.max
Bu yerda memory.max 104857600 baytga (100 MiB) o'rnatilgan — bu cgroup’dagi undan oshishga uringan har qanday jarayonning xotirasi kernel tomonidan qaytarib olinadi yoki jarayon o'ldiriladi, shunchaki sekinlashtirilmaydi. cpu.max mikrosoniyalarda <quota> <period> juftligidan foydalanadi. 50000 100000 host’da aslida nechta yadro borligidan qat’i nazar, har 100 000 mikrosoniyadan 50 000 tasiga — bitta CPU yadrosining yarmiga — ruxsat beradi.
cgroup.procs ga PID yozish (bu yerda $$ — joriy shell’ning o'z PID’i) o'sha jarayonni — va shu paytdan boshlab u fork qiladigan har qanday narsani — demo cgroup’iga ko'chiradi va u darhol ikkala cheklovga ham bo'ysuna boshlaydi.
cgroup katalogini faqat uning ichida hech qanday jarayon qolmaganda o'chirish mumkin. rmdir dan oldin qolgan har qanday jarayonni tashqariga ko'chiring (yoki uning tugashini kuting).
Bu docker run --memory bilan qanday bog'liq
docker run -d --name limited --memory=256m --cpus=0.5 nginx:alpine
docker inspect -f '{{.State.Pid}}' limited
cat /sys/fs/cgroup/system.slice/docker-*/memory.max 2>/dev/null || \
systemd-cgls | grep -A2 limited
docker run --memory=256m --cpus=0.5 yuqoridagi qo'lda bajarilgan buyruqlar qilgan ishning aynan o'zini qiladi: u container uchun cgroup yaratadi va unga mos ravishda memory.max va cpu.max ni yozadi. Flag — hozirgina qo'lda ko'rsatilgan o'sha kernel interfeysi ustidagi qulaylik qatlami. Ishlab turgan container uchun /sys/fs/cgroup/ ostidagi haqiqiy cgroup fayllarini tekshirib, buni tasdiqlash haqiqatan foydali debug qilish odati. Container kutilmaganda OOM tomonidan o'ldirilganda, birinchi navbatda container’ning o'z ilova log'lariga emas, dmesg ga va tegishli cgroup’ning memory.events fayliga (xususan oom_kill hisoblagichiga) qarash kerak. Ilova log'lari ko'pincha umuman hech narsa ko'rsatmaydi, chunki kernel jarayonni u biror narsa yozib ulgurmasidan oldin o'ldirgan bo'ladi.
Keng tarqalgan xatolar
- Namespace’lar resurs cheklovlarini beradi deb hisoblash. Namespace’lar faqat ko'rinishni boshqaradi. O'z PID va network namespace’iga ega, lekin cgroup cheklovi yo'q jarayon baribir host CPU va xotirasining 100% ini iste’mol qila oladi. Ikki mexanizm turli muammolarni hal qiladi va haqiqiy izolyatsiya uchun ikkalasi ham kerak.
unshare --pidbilan--mount-procni unutish. Usiz/prochali ham host’ni aks ettiradi va yangi PID namespace ichidagipskabi vositalar adashtiruvchi tarzda hostning to'liq jarayonlar ro'yxatini ko'rsatadi.- Faqat user namespace jarayonni "root" sifatida ishga tushirishni xavfsiz qiladi deb o'ylash. User namespace ichkaridagi root’ni shu namespace’ning o'z resurslari uchun tashqaridagi root bo'lmagan foydalanuvchiga moslashtiradi. Lekin hali ham hostning user namespace’ini bo'lishadigan container’da (ko'pchilik Docker konfiguratsiyalarida rootless bo'lmagan standart holat) uning root’i to'g'ridan-to'g'ri host root’iga mos keladi. Har qanday xavfsizlik tekshiruvida rootless xatti-harakatni taxmin qilish o'rniga buni aniq tekshirish muhim.
- cgroups v1 va v2 sintaksisini chalkashtirish.
memory.max(v2) vamemory.limit_in_bytes(v1) bir xil fayl emas va bir-birining o'rnini bosa olmaydi. Biriga qarab yozilgan skriptlar ikkinchisini ishlatadigan tizimda jimgina hech narsa qilmaydi.
Mashqlar
sudo unshare --pid --fork --mount-proc bashni ishga tushiring va host’dagi ikkinchi terminaldanps aux | grep bashbilan o'shabashjarayonining haqiqiy host PID’ini toping. Namespace ichida ko'rinadigan PID va host’dan ko'rinadigan PID nega farq qilishini tushuntiring.docker run -d --name test nginx:alpinebilan container ishga tushiring, uning PID’inidocker inspectbilan toping, keyindocker execishlatmasdannsenteryordamida uning namespace’lari ichidahostnamevaip addrni ishga tushiring. Chiqishdocker exec test hostnamevadocker exec test ip addrko'rsatadigan natijaga mos kelishini tasdiqlang.memory.max50 MiB bo'lgan cgroup yarating, unga xotirani ko'p talab qiladigan test jarayonini ko'chiring (bir martalik VM’da siklda xotira ajratadigan kichik skript yaxshi ishlaydi) va u cheklovdan oshgach o'ldirilishini kuzating. Keyindmesgda OOM kill xabarini tekshiring.- Lab tizimingizda
mount | grep cgroupni ishga tushiring va u cgroups v1 yoki v2 dan foydalanayotganini hamda buni faqat chiqishning o'zidan qanday bilish mumkinligini ayting.
Xulosa
Namespaces va cgroups — har bir container runtime tayanadigan ikki mustaqil kernel mexanizmi. Namespace’lar jarayon nimani ko'ra olishini cheklaydi (o'z PID daraxti, tarmoq steki, mount’lar, hostname va — user namespace bilan — o'z UID moslashtirishi), cgroups esa jarayon qancha iste’mol qila olishini cheklaydi (CPU, xotira, I/O). Hozirgi distributivlarda bu yagona cgroups v2 ierarxiyasi orqali majburlanadi. unshare va nsenter hech qanday container vositasi o'rnatilmagan holda ham ikkalasini ko'rsatadi, docker inspect/nsenter esa docker exec va docker run --memory alohida sehr emas, aynan shu primitivlar ustidagi qulaylik o'ramlari ekanini ko'rsatadi. Endi Docker va Podman: amaliy ishlash kundalik vositalarni aynan shu poydevor ustiga quradi.