open atlas
↑ К треку
Linux: операционная система LIN · 12 · 03

Namespaces и cgroups — примитивы контейнера

Namespaces изолируют глобальные ресурсы ядра на группу процессов (pid, net, mnt, uts, ipc, user, cgroup). В сочетании с cgroups для лимитов и корневой файловой системой они образуют полный примитив контейнера. Никакой магии — именно здесь начинается трек Docker.

LIN Senior ◷ 24 min
Уровень
ОсновыJuniorMiddleSenior

Каждый senior-инженер, отлаживавший проблему с сетью контейнера, контейнер, поедающий CPU хоста, или эскалацию привилегий внутри контейнера, рано или поздно приходит к одному и тому же осознанию: контейнер — это не виртуальная машина, не песочница и не магия. Это процесс (или дерево процессов), которому ядро показывает другую картину мира. Процесс думает, что он один на машине, что у него PID 1, собственный сетевой стек, и видит конкретное дерево директорий как свой корень. Ядро достигает этого двумя механизмами: namespaces (разрезают глобальные ресурсы на изолированные представления) и cgroups (enforc-ат лимиты ресурсов). Корневая файловая система завершает картину. Эти три вещи вместе — именно то, что Docker, containerd и каждый другой контейнерный рантайм собирает при запуске контейнера. После этого урока никакой тайны не остаётся — и трек Docker начинается именно здесь.

Цель

После этого урока ты сможешь назвать семь типов namespace и что каждый изолирует, использовать unshare для создания нового namespace и nsenter для входа в существующий, объяснить, как cgroups v2 ограничивают CPU и память процесса, и описать контейнер как namespaces + cgroups + корневая файловая система — без оставшихся чёрных ящиков.

1

Семь типов namespace. Namespace оборачивает глобальный ресурс ядра так, что процессы внутри видят собственную изолированную копию. Linux предоставляет семь типов namespace.

# Список namespace для текущего процесса оболочки:
ls -la /proc/$$/ns/
# lrwxrwxrwx 1 user user 0 Jun 22 09:00 cgroup -> cgroup:[4026531835]
# lrwxrwxrwx 1 user user 0 Jun 22 09:00 ipc    -> ipc:[4026531839]
# lrwxrwxrwx 1 user user 0 Jun 22 09:00 mnt    -> mnt:[4026531840]
# lrwxrwxrwx 1 user user 0 Jun 22 09:00 net    -> net:[4026531992]
# lrwxrwxrwx 1 user user 0 Jun 22 09:00 pid    -> pid:[4026531836]
# lrwxrwxrwx 1 user user 0 Jun 22 09:00 user   -> user:[4026531837]
# lrwxrwxrwx 1 user user 0 Jun 22 09:00 uts    -> uts:[4026531838]

# Два процесса с одинаковым inode-номером для типа namespace
# находятся в ОДНОМ namespace. Разные inode = разные namespace.
NamespaceФлагЧто изолирует
pidCLONE_NEWPIDПространство PID — PID 1 внутри может быть любым процессом
netCLONE_NEWNETСетевые интерфейсы, таблица маршрутизации, правила iptables
mntCLONE_NEWNSТаблица монтирования — какие файловые системы видны
utsCLONE_NEWUTSИмя хоста и NIS-домен
ipcCLONE_NEWIPCSystem V IPC (семафоры, очереди сообщений, разделяемая память)
userCLONE_NEWUSERМаппинг UID/GID — root внутри маппится на непривилегированный UID снаружи
cgroupCLONE_NEWCGROUPПредставление иерархии cgroup для процесса
2

Создание namespace через unshare. unshare запускает новый процесс в новом namespace без какого-либо контейнерного рантайма. Это и есть голая возможность ядра.

# Создать новый UTS namespace и изменить hostname внутри:
sudo unshare --uts /bin/bash
# Внутри нового namespace:
hostname container-test
hostname
# container-test

# Открыть другой терминал на хосте и проверить:
hostname
# myserver    ← hostname хоста не изменился

# Создать новый PID namespace — оболочка становится PID 1 внутри:
sudo unshare --pid --fork --mount-proc /bin/bash
# Внутри:
echo $$
# 1    ← эта оболочка — PID 1 в новом namespace
ps aux
# USER  PID %CPU %MEM VSZ   RSS TTY   STAT TIME COMMAND
# root    1  0.0  0.0 4236 3492 pts/1 S    0:00 /bin/bash
# root    2  0.0  0.0 7484 2992 pts/1 R+   0:00 ps aux
# Видны только два процесса — 200+ процессов хоста скрыты

# Создать новый network namespace (изолированный сетевой стек):
sudo unshare --net /bin/bash
# Внутри:
ip link
# 1: lo: <LOOPBACK> mtu 65536 ...   ← только loopback, нет eth0, нет docker0

unshare с несколькими флагами комбинирует namespace: --pid --net --uts --ipc --mount — это примерно то, что делает контейнерный рантайм при старте контейнера, минус смена корня.

3

Вход в существующие namespace через nsenter. nsenter подключает процесс к namespace уже запущенного процесса. Именно так отлаживают контейнеры с хоста без docker exec.

# Найти PID контейнера на хосте:
# (В Docker: docker inspect <container> --format '{{.State.Pid}}')
CPID=12345

# Войти в сетевой namespace контейнера для инспекции снаружи:
sudo nsenter --net --target $CPID ip addr
# 1: lo: <LOOPBACK,UP>
# 15: eth0@if16: <BROADCAST,MULTICAST,UP> ...
#   inet 172.17.0.3/16

# Войти в PID namespace контейнера для просмотра его дерева процессов:
sudo nsenter --pid --target $CPID ps aux

# Войти во ВСЕ namespace контейнера (эквивалент raw exec):
sudo nsenter --target $CPID --mount --uts --ipc --net --pid -- /bin/bash
# Ты теперь внутри полного окружения контейнера без Docker

# Именно так работает 'docker exec' под капотом —
# он вызывает nsenter для входа в namespace контейнера.

nsenter незаменим, когда контейнер не имеет оболочки (образы FROM scratch, distroless) — входишь в его сетевой namespace с хоста, но запускаешь инструменты из файловой системы хоста.

4

cgroups v2: лимиты ресурсов на группу процессов. Namespaces контролируют что процесс видит. cgroups контролируют сколько ресурсов хоста он может использовать. Урок 08 покрыл cgroups в глубину; здесь мы связываем их с картиной контейнера.

# Иерархия cgroup v2 находится в /sys/fs/cgroup (единая иерархия):
ls /sys/fs/cgroup/
# cgroup.controllers  memory.current  memory.max  system.slice  user.slice

# Найти, к какому cgroup принадлежит PID контейнера:
cat /proc/$CPID/cgroup
# 0::/system.slice/docker-abc123.scope

# Инспектировать лимит памяти для этого cgroup:
cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max
# 536870912   ← лимит 512 МБ, установленный Docker

# Инспектировать лимит CPU:
cat /sys/fs/cgroup/system.slice/docker-abc123.scope/cpu.max
# 100000 1000000   ← квота 100мс на период 1000мс = 10% одного CPU

# Создать cgroup вручную и ограничить его (показывает сырой механизм, который использует Docker):
sudo mkdir /sys/fs/cgroup/demo
echo 268435456 | sudo tee /sys/fs/cgroup/demo/memory.max   # 256 МБ
echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs        # добавить эту оболочку

# Эта оболочка теперь не может выделить более 256 МБ суммарно.
# OOM killer (из урока 08) сработает при попытке.

Флаги Docker --memory, --cpus и --cpu-shares записывают именно эти файлы cgroup. Другого механизма нет.

5

Уравнение контейнера: namespaces + cgroups + rootfs. Контейнерный рантайм выполняет три действия последовательно при запуске контейнера:

# 1. NAMESPACES — создать изолированные представления ядра:
#    --pid:   новое пространство PID (процесс контейнера — PID 1)
#    --net:   новый сетевой стек (veth-пара к bridge хоста)
#    --mnt:   новая таблица монтирования
#    --uts:   новое имя хоста
#    --ipc:   новое IPC-пространство
#    --user:  (опционально) переназначение UID для rootless-контейнеров

# 2. CGROUPS — применить лимиты ресурсов:
#    memory.max   → --memory в Docker
#    cpu.max      → --cpus в Docker
#    pids.max     → ограничение fork bomb

# 3. ROOTFS — предоставить другую файловую систему как корень:
#    pivot_root() или chroot() в стек слоёв образа контейнера
#    Образ контейнера — просто дерево директорий (tar-слои)
#    overlayfs объединяет read-only слои образа с записываемым верхним слоем

# Собрать минимальный контейнер вручную (образовательно — не для продакшна):
sudo unshare --pid --fork --mount-proc --net --uts \
  chroot /path/to/rootfs /bin/sh
# Это контейнер. Без демона Docker, без containerd, без магии.
# Единственное отличие от настоящего контейнерного рантайма:
# - нет назначения cgroup (нет лимитов ресурсов)
# - нет veth-пары (нет сетевой связности)
# - нет управления образами / слоями
# Настоящий рантайм автоматизирует эти три шага, добавляет управление
# жизненным циклом, дистрибуцию образов и API управления.

# Проверить с хоста, что у "контейнера" свой PID namespace:
ls -la /proc/<pid-контейнера>/ns/pid
# pid:[4026532345]   ← другой inode = другой namespace
ls -la /proc/$$/ns/pid
# pid:[4026531836]   ← PID namespace хоста

Это мост: трек Docker начинается с docker run, но теперь ты знаешь, что под капотом он вызывает clone() с флагами namespace, записывает лимиты cgroup и вызывает pivot_root() в стек слоёв образа. Каждая сессия отладки контейнера — docker exec, nsenter, лимиты памяти cgroup, инспекция сетевого namespace — обретает смысл, когда видишь три примитива.

Разбор примера

Отладка: у контейнера нет интернета, но у хоста есть. Диагностика с хоста через инструменты namespace.

# Контейнер запущен, но curl внутри падает:
# docker exec myapp curl https://example.com
# curl: (6) Could not resolve host: example.com

# Шаг 1: получить PID контейнера на хосте:
CPID=$(docker inspect myapp --format '{{.State.Pid}}')
echo $CPID
# 8821

# Шаг 2: войти в сетевой namespace контейнера и инспектировать оттуда:
sudo nsenter --net --target $CPID ip route
# default via 172.17.0.1 dev eth0
# 172.17.0.0/16 dev eth0 proto kernel

# Шаг 3: проверить доступность шлюза по умолчанию:
sudo nsenter --net --target $CPID ping -c1 172.17.0.1
# 64 bytes from 172.17.0.1: icmp_seq=0 ttl=64 time=0.15 ms
# Шлюз доступен — подозреваем DNS

# Шаг 4: проверить разрешение DNS из namespace:
sudo nsenter --net --target $CPID cat /etc/resolv.conf
# nameserver 127.0.0.11   ← встроенный DNS-резолвер Docker

sudo nsenter --net --target $CPID nslookup example.com 127.0.0.11
# ;; connection timed out; no servers could be reached

# Шаг 5: проверить ip_forward на ХОСТЕ:
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0   ← нашли

# Хост потерял ip_forward (перезагрузка без постоянного sysctl).
# Встроенный DNS Docker на 127.0.0.11 пересылает запросы через NAT хоста —
# что требует ip_forward.

# Исправление:
sudo sysctl -w net.ipv4.ip_forward=1
sudo tee /etc/sysctl.d/99-docker-forwarding.conf <<'EOF'
net.ipv4.ip_forward = 1
EOF

# Повторить из контейнера:
# docker exec myapp curl https://example.com
# <!DOCTYPE html>...   ← работает

Первопричина: два урока сходятся — ip_forward (урок sysctl) потерян при перезагрузке, диагностирован через nsenter для входа в сетевой namespace контейнера прямо с хоста. Это стандартный playbook для проблем с сетью контейнеров.

Почему это работает

На macOS нет Linux namespaces — Docker Desktop запускает Linux VM. При выполнении docker run на macOS Docker Desktop запускает лёгкую Linux VM (через Apple Hypervisor framework). Ядро Linux внутри этой VM предоставляет namespaces и cgroups. Docker CLI на macOS — просто удалённый клиент, общающийся с демоном внутри VM. Это значит: nsenter с хоста macOS не работает (на macOS нет Linux namespaces), /proc не существует, а параметры sysctl для Docker (net.ipv4.ip_forward и др.) живут внутри VM, а не на Mac. Трек Docker работает полностью с VM; это различие важно при отладке продакшн Linux-хостов в сравнении с локальной средой разработки на macOS.

Частая ошибка

Считать root в user namespace равным root на хосте. Когда контейнер использует user namespace (rootless-контейнеры, умолчание Podman), UID 0 внутри контейнера маппится на непривилегированный UID на хосте (например, UID 100000). Процесс, вырвавшийся из mount namespace контейнера, но остающийся в его user namespace, имеет на хосте UID 100000 — не root. В этом и состоит ценность user namespaces для безопасности. Однако многие контейнерные рантаймы (классический Docker) по умолчанию НЕ используют user namespaces — UID 0 внутри маппится напрямую на UID 0 хоста. В таких развёртываниях побег из контейнера даёт доступ root на хосте. Всегда проверяй, использует ли твой рантайм user namespaces, если безопасность имеет значение.

Проверь себя
Викторина

Ты запускаешь `sudo unshare --pid --fork --mount-proc /bin/bash` и видишь только два процесса в `ps aux`. На хосте при этом 200 запущенных процессов. Какой механизм ядра создаёт это отличие, и что добавить, чтобы у этой оболочки было своё имя хоста, отдельное от хоста?

Итог

Контейнеры Linux построены из трёх примитивов ядра без какой-либо дополнительной магии. Namespaces дают группе процессов изолированное представление семи глобальных ресурсов: pid (идентификаторы процессов), net (сетевой стек), mnt (таблица монтирования), uts (hostname), ipc (IPC-объекты), user (маппинг UID/GID), cgroup (представление иерархии cgroup). unshare создаёт новый namespace; nsenter входит в существующий — полезно для отладки контейнеров с хоста без docker exec. cgroups v2 enforc-ят лимиты ресурсов на группу процессов через файлы в /sys/fs/cgroup/: memory.max, cpu.max, pids.max — именно то, что записывают флаги Docker --memory и --cpus. Корневая файловая система (стек слоёв образа, смонтированный через overlayfs с pivot_root) завершает контейнер. Каждый контейнерный рантайм — Docker, containerd, Podman — это API и менеджер жизненного цикла поверх этих трёх примитивов. Трек Docker начинается здесь: docker run вызывает clone() с флагами namespace, записывает файлы cgroup и делает pivot_root в образ. Ничего другого под капотом нет.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.