Learning Platform
Глоссарий Troubleshooting
Урок 03.04 · 24 мин
Начальный
dockernamespacescgroupslinux

Namespaces и cgroups

В уроке 1.1 мы говорили: контейнер — это процесс с namespaces и cgroups. Сейчас посмотрим на это чуть подробнее. Не на уровне реализации ядра (это deep dive для kernel-разработчиков), а на уровне «что какой namespace изолирует, и что какой cgroup ограничивает». Junior DE этого достаточно, чтобы понимать, что происходит, когда docker run --memory=512m — и почему этот лимит работает.

В конце урока — практический раздел с lsns, который покажет namespaces, созданные для контейнера, прямо на твоей машине (если на Linux). На mac часть из этого видна только внутри Docker VM.


Восемь namespaces

Восемь типов Linux namespaces — каждый изолирует один ресурс
pidPID namespace. Изолирует список процессов. Внутри контейнера ps aux покажет только процессы этого контейнера. Главный — PID 1
mountMount namespace. Изолирует список mount points. Контейнер видит только свои монтирования — overlay rootfs + явно проброшенные volumes
netNetwork namespace. Свой stack: eth0, lo, маршруты, iptables. Можно создать пустой сетевой namespace и поднять там свою сеть
utsUTS namespace. Изолирует hostname и domainname. Internalize: внутри контейнера hostname обычно равен короткому ID контейнера
ipcIPC namespace. Изолирует System V IPC и POSIX message queues. Контейнер не видит IPC хоста
userUser namespace. UID/GID маппинг — root в контейнере не равен root на хосте. Docker по умолчанию не включает, OrbStack и Podman — часто включают
cgroupCgroup namespace. Изолирует view на cgroup иерархию. Внутри контейнера видна только своя cgroup-ветка
timeTime namespace. Свой системный время. Появился в kernel 5.6 (2020), Docker пока редко использует

Каждый 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 создаёт для контейнера:

  1. Новый сетевой namespace.
  2. Пару virtual ethernet интерфейсов (veth). Один конец — внутри namespace контейнера (eth0), другой — на хосте, прикреплён к bridge docker0.
  3. Назначает IP-адрес из docker0 подсети (обычно 172.17.0.x).

С точки зрения контейнера у него один интерфейс eth0 с IP. С точки зрения хоста — это просто один из многих интерфейсов в bridge.

Сети — это отдельный модуль 10, там разберём подробнее.

IPv4 — 32 бита, dotted decimal и адресация

Mount namespace и rootfs

Mount namespace изолирует список mount points. Когда контейнер стартует, Docker:

  1. Создаёт пустой mount namespace.
  2. Монтирует overlayfs (image layers + writable layer) в какой-то временной директории на хосте.
  3. Делает pivot_root — корнем для процесса становится эта overlay-директория.
  4. Дополнительно монтирует 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 для лимитов через cgroups
--memory=512mЖёсткий лимит памяти. При превышении процесс получит OOMKilled. Значение можно в k/m/g
--memory-swap=1gЛимит на сумму memory + swap. Если равен --memory, swap запрещён. Часто оставляют как есть
--cpus=2Эквивалент 2 ядер. Под капотом cpu.max = 200000 100000 (2 CPU из квоты в 100мс)
--cpuset-cpus=0,1Привязать к конкретным CPU-ядрам. Полезно для NUMA-awareness, обычно не нужно junior'у
--pids-limit=100Максимум 100 процессов в контейнере. Защита от fork bomb. Полезно в multi-tenant сценариях
--blkio-weight=500Относительный вес дисковой IO. Значение 10-1000. Меньше Junior'у не пригодится

Конкретный пример:

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.

Проверка знанийKnowledge check
В чём разница между namespaces и cgroups, и какую задачу решает каждый из этих механизмов?
ОтветAnswer
Namespaces и cgroups — два разных механизма ядра Linux, которые вместе образуют контейнер, но решают противоположные задачи. Namespaces отвечают на вопрос «что процесс видит»: они изолируют его взгляд на систему по восьми измерениям — PID (нумерация процессов), mount (монтированные файловые системы), net (сетевой стек), uts (hostname), ipc (System V IPC), user (UID/GID маппинг), cgroup (view на cgroup-иерархию), time (системное время). Cgroups отвечают на вопрос «сколько процесс может потребить»: они ограничивают ресурсы — память (memory.max), CPU (cpu.max и cpu.weight), IO (io), число процессов (pids.max). Когда ты пишешь docker run --memory=512m --cpus=2, namespaces создают изолированный «мир» для процесса, а cgroups говорят «и в этом мире не больше 512 МБ RAM и не больше 2 ядер». Если процесс попытается превысить memory.max — ядро убьёт его OOM-сигналом. Это всё обычный Linux kernel API, без какой-либо магии Docker.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Какая задача у namespaces и какая у cgroups?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5