Linux Namespaces: ізолюємо процес за 50 рядків Go¶
Written by:
Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect
Stanislav Gorovyy
HBO-ICT Student at Hogeschool Saxion | Software Engineer
Контейнер - це процес з обмеженим видом на систему. Ось і все. Жодної магії, жодних віртуальних машин. Ядро Linux дає процесу окремий "простір імен" (namespace), і процес думає, що він один у системі.
Я побудував контейнерний рантайм Sheep з нуля на Go, і в цій серії покажу як це працює зсередини. Почнемо з найважливішого - namespace'ів.
П'ять namespace'ів, які роблять контейнер контейнером¶
Linux дає кілька типів namespace'ів. Кожен ізолює свій аспект:
| Namespace | Прапорець | Що ізолює |
|---|---|---|
| UTS | CLONE_NEWUTS |
hostname - контейнер має своє ім'я |
| PID | CLONE_NEWPID |
процеси - контейнер бачить тільки свої PID |
| Mount | CLONE_NEWNS |
файлову систему - свій корінь, свої mount'и |
| IPC | CLONE_NEWIPC |
міжпроцесну комунікацію - shared memory, semaphores |
| Network | CLONE_NEWNET |
мережу - свій мережевий стек, IP, порти |
Як це виглядає в коді¶
Ось ключова функція з Sheep, яка запускає ізольований процес:
func startContainer(c *Container) (int, error) {
cmd := reexecCommand(c)
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWNET,
Unshareflags: syscall.CLONE_NEWNS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return 0, fmt.Errorf("start namespaced process: %w", err)
}
pid := cmd.Process.Pid
if err := setupCgroups(c, pid); err != nil {
cmd.Process.Kill()
return 0, fmt.Errorf("setup cgroups: %w", err)
}
if err := setupNetworkForContainer(c, pid); err != nil {
fmt.Fprintf(os.Stderr, "warning: network setup failed: %v\n", err)
}
go cmd.Wait()
return pid, nil
}
Все тримається на одному рядку - SysProcAttr.Cloneflags. Ми передаємо ядру набір прапорців, і воно створює дочірній процес в окремих namespace'ах.
Що робить кожен прапорець¶
CLONE_NEWUTS - ізоляція hostname¶
Процес у новому UTS namespace отримує свій hostname. Без цього sethostname() змінив би hostname всієї машини.
if hostname != "" {
if err := syscall.Sethostname([]byte(hostname)); err != nil {
return fmt.Errorf("sethostname: %w", err)
}
}
CLONE_NEWPID - ізоляція процесів¶
Всередині PID namespace процес думає, що його PID = 1. Він не бачить жодного процесу хоста. Це як окрема операційна система, тільки з одним ядром.
CLONE_NEWNS - ізоляція файлової системи¶
Mount namespace дає процесу свою таблицю монтувань. Контейнер може монтувати /proc, /sys, /tmp без впливу на хост.
CLONE_NEWIPC - ізоляція IPC¶
Shared memory segments, semaphores, message queues - все своє. Контейнер не може підслухати IPC хоста.
CLONE_NEWNET - ізоляція мережі¶
Свій мережевий стек: свої інтерфейси, IP-адреси, routing table, iptables. Поки не налаштуємо veth пару і bridge, контейнер не має мережі взагалі.
Як все працює разом¶
graph TB
subgraph "Хост"
HOST_PROC["Процеси хоста<br/>PID 1, 2, 3..."]
HOST_NET["Мережа хоста<br/>eth0, 192.168.1.x"]
HOST_FS["Файлова система<br/>/, /home, /var"]
end
subgraph "Контейнер (нові namespace'и)"
C_PROC["PID 1 (init)"]
C_NET["Мережа контейнера<br/>eth0, 10.20.0.x"]
C_FS["Своя файлова система<br/>/, /proc, /tmp"]
C_HOST["hostname: sheep-abc123"]
end
HOST_PROC -.->|"CLONE_NEWPID"| C_PROC
HOST_NET -.->|"CLONE_NEWNET"| C_NET
HOST_FS -.->|"CLONE_NEWNS"| C_FS
Хост бачить контейнерний процес як звичайний процес з високим PID. Але зсередини контейнер бачить тільки себе.
Що може піти не так¶
Namespace'и дають ізоляцію, але не обмеження ресурсів. Процес у новому PID namespace все одно може з'їсти всю пам'ять машини. Для обмежень потрібні cgroups - про це буде в частині 4.
І ще одна річ. Go створює кілька потоків ще до виклику main(). Це означає, що clone() з прапорцями namespace'ів працює не зовсім коректно, якщо викликати його напряму. Потрібен re-exec pattern, про який поговоримо в наступній частині.
Спробуй сам¶
git clone https://github.com/igorovh/sheep && cd sheep
go build ./cmd/sheep
sudo ./sheep bootstrap minimal
sudo ./sheep run --name test minimal /bin/sh
# Всередині контейнера:
hostname # побачиш скорочений ID контейнера
ps aux # тільки один процес - /bin/sh
ip addr # свій мережевий інтерфейс
Далі розберемо, чому Go і clone() конфліктують і як re-exec pattern це вирішує.
Наступна: Re-Exec Pattern
