Tarkibga o'tish

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, unshare bilan 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

sudo unshare --pid --fork --mount-proc bash
ps aux
USER       PID  COMMAND
root         1  bash
root        12  ps aux

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

sudo ip netns add lab-ns
sudo ip netns exec lab-ns ip addr show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default

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.

sudo ip netns delete lab-ns

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.

sudo unshare --user --map-root-user bash
id
uid=0(root) gid=0(root) groups=0(root)
cat /proc/self/uid_map
         0       1000          1

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:

docker run -d --name web nginx:alpine
docker inspect -f '{{.State.Pid}}' web
48213
sudo ls -l /proc/48213/ns/
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:

sudo nsenter --target 48213 --pid --net --mount --uts hostname
7f3a9b2e1c44

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

mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

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.

cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma

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.

sudo mkdir /sys/fs/cgroup/demo
cat /sys/fs/cgroup/demo/memory.max
max

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.

echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs
cat /sys/fs/cgroup/demo/cgroup.procs
51302

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.

cd /
sudo rmdir /sys/fs/cgroup/demo

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 --pid bilan --mount-proc ni unutish. Usiz /proc hali ham host’ni aks ettiradi va yangi PID namespace ichidagi ps kabi 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) va memory.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

  1. sudo unshare --pid --fork --mount-proc bash ni ishga tushiring va host’dagi ikkinchi terminaldan ps aux | grep bash bilan o'sha bash jarayonining haqiqiy host PID’ini toping. Namespace ichida ko'rinadigan PID va host’dan ko'rinadigan PID nega farq qilishini tushuntiring.
  2. docker run -d --name test nginx:alpine bilan container ishga tushiring, uning PID’ini docker inspect bilan toping, keyin docker exec ishlatmasdan nsenter yordamida uning namespace’lari ichida hostname va ip addr ni ishga tushiring. Chiqish docker exec test hostname va docker exec test ip addr ko'rsatadigan natijaga mos kelishini tasdiqlang.
  3. memory.max 50 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. Keyin dmesg da OOM kill xabarini tekshiring.
  4. Lab tizimingizda mount | grep cgroup ni 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.

Manbalar