Namespaces и cgroups
В уроке 1.1 мы говорили: контейнер — это процесс с namespaces и cgroups. Сейчас посмотрим на это чуть подробнее. Не на уровне реализации ядра (это deep dive для kernel-разработчиков), а на уровне «что какой namespace изолирует, и что какой cgroup ограничивает». Junior DE этого достаточно, чтобы понимать, что происходит, когда docker run --memory=512m — и почему этот лимит работает.
В конце урока — практический раздел с lsns, который покажет namespaces, созданные для контейнера, прямо на твоей машине (если на Linux). На mac часть из этого видна только внутри Docker VM.
Восемь namespaces
Каждый namespace создаётся отдельным флагом в системном вызове clone(). Docker (через runc) делает примерно так:
clone(child_func, child_stack,
CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET |
CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER |
CLONE_NEWCGROUP, NULL);
Это не магия — это обычный Linux API. Любой root-процесс может создать свой namespace и поместить туда дочерний процесс.
fork() и exec() — как запускается программа
PID namespace — собственная нумерация процессов
PID namespace — самый наглядный. Внутри контейнера главный процесс становится PID 1 — как init на обычной системе. Все его дочерние процессы получают PID 2, 3, и т.д. — но в контейнерном namespace.
Тот же процесс с точки зрения хоста имеет совершенно другой PID, например 38491. Это и есть «двойная нумерация»: один процесс, два PID.
Особенность: PID 1 в Linux получает специальную обработку. Сигналы, которые ядро посылает PID 1, обрабатываются по-другому (например, SIGTERM игнорируется по умолчанию, если процесс не установил обработчик). Поэтому если ты пишешь Dockerfile с CMD ["python", "app.py"] — твой Python становится PID 1, и нужно правильно ловить SIGTERM, иначе docker stop не сработает корректно. Об этом подробнее в модуле 7.
Network namespace — собственная сеть
Network namespace — это полный сетевой стек: интерфейсы, IP-адреса, маршруты, ARP, iptables правила.
Когда ты запускаешь docker run -d nginx, Docker создаёт для контейнера:
- Новый сетевой namespace.
- Пару virtual ethernet интерфейсов (
veth). Один конец — внутри namespace контейнера (eth0), другой — на хосте, прикреплён к bridgedocker0. - Назначает IP-адрес из
docker0подсети (обычно 172.17.0.x).
С точки зрения контейнера у него один интерфейс eth0 с IP. С точки зрения хоста — это просто один из многих интерфейсов в bridge.
Сети — это отдельный модуль 10, там разберём подробнее.
IPv4 — 32 бита, dotted decimal и адресацияMount namespace и rootfs
Mount namespace изолирует список mount points. Когда контейнер стартует, Docker:
- Создаёт пустой mount namespace.
- Монтирует overlayfs (image layers + writable layer) в какой-то временной директории на хосте.
- Делает
pivot_root— корнем для процесса становится эта overlay-директория. - Дополнительно монтирует tmpfs для
/tmp, специальные mounts для/proc,/sys,/dev, и явно указанные volumes/bind mounts.
С точки зрения процесса внутри — он видит обычный Linux-корень. На самом деле это смонтированный overlayfs из распакованного image. Подробнее об этом в модуле 6 («Слои и хранение»).
Cgroups v2 — лимиты ресурсов
Cgroups (control groups) — это отдельный механизм от namespaces. Если namespaces — про «что процесс видит», то cgroups — про «сколько процесс может потребить».
В современном Linux (Ubuntu 22.04+, Debian 11+) используется cgroup v2 — единая иерархия. Старая v1 имела отдельные деревья для cpu, memory, blkio, и это было сложно. v2 всё унифицировал.
Что cgroups может ограничить:
- memory — максимальная память. При превышении — OOM kill.
- cpu — доля CPU (cpu.weight) или жёсткий лимит (cpu.max).
- io — лимиты на дисковую IO.
- pids — максимальное число процессов.
Команды Docker для ограничений
Конкретный пример:
docker run -d --name capped \
--memory=512m \
--cpus=2 \
--pids-limit=100 \
nginx
Этот nginx не сможет съесть больше 512 МБ RAM, использовать больше двух ядер CPU, и породить больше 100 процессов. Если что-то из этого попытается превысить — ядро вмешается:
- Memory: процесс получит OOM kill, контейнер остановится с кодом 137.
- CPU: процесс будет throttle’нут (его время выполнения замедлится).
- pids: новые
fork()будут возвращать ошибку.
Что покажет lsns
На Linux есть утилита lsns, которая показывает все namespaces в системе. Запустив контейнер, ты можешь увидеть «контейнерные» namespaces в её выводе.
Пример (на Linux-хосте, не из контейнера):
docker run -d --name demo nginx
sudo lsns | grep nginx
Вывод будет примерно такой (упрощённо):
NS TYPE NPROCS PID USER COMMAND
4026532245 mnt 3 38491 root nginx
4026532246 uts 3 38491 root nginx
4026532247 ipc 3 38491 root nginx
4026532248 pid 3 38491 root nginx
4026532249 net 3 38491 root nginx
Цифры в начале — уникальные ID namespaces. PID 38491 — это PID nginx-master с точки зрения хоста (внутри контейнера он PID 1).
В Docker Desktop / OrbStack на macOS lsns на хосте не покажет ничего интересного, потому что контейнеры живут внутри Linux VM. Можно зайти в эту VM:
# OrbStack
orb shell docker
sudo lsns
Попробуй сам
Если ты на Linux, или у тебя есть OrbStack с возможностью зайти в Linux VM:
docker run -d --memory=256m --cpus=1 --name cap nginx
Посмотри лимиты:
docker inspect cap --format='{{json .HostConfig}}' | jq '.Memory, .NanoCpus'
Должно вывести 268435456 (256 МБ в байтах) и 1000000000 (1.0 CPU в наноядрах).
Посмотри cgroup-файлы для контейнера. Сначала найди cgroup path:
docker inspect cap --format='{{.Id}}'
# например, abc123...
# на Linux:
cat /sys/fs/cgroup/system.slice/docker-abc123....scope/memory.max
# вывод: 268435456
Это и есть тот самый лимит, который кладёт cgroup v2 в memory.max. Docker не делает магии — он просто пишет нужное значение в этот файл.
Прибери:
docker rm -f cap
Связь namespaces, cgroups и Docker
Финальная картина:
- Namespaces изолируют процесс контейнера от других процессов: свой PID, своя сеть, свой mount.
- Cgroups ограничивают, сколько контейнер может потреблять: память, CPU, IO.
- Image rootfs даёт контейнеру файловое содержимое.
- Docker (через containerd + runc) оркеструет это: создаёт namespaces, прикручивает cgroups, монтирует rootfs, запускает процесс.
Когда что-то ломается («контейнер не видит мой сервис на хосте», «контейнер съел всю память хоста»), ты возвращаешься к этой модели:
- «Не видит сервис» — это про network namespace и сетевую конфигурацию.
- «Съел память» — это про cgroup memory limit, который ты не выставил.
- «Конфликт PID» — это про PID namespace.
- «Не видит мой файл» — это про mount namespace и volumes.