9. Linux и контейнеры
namespaces, cgroups, overlayfs и capabilities: как устроены контейнеры в Linux, чем отличаются от chroot, и как смотреть namespaces/лимиты ресурсов.
Namespaces
Namespace — изоляция вида ресурсов так, что процесс внутри namespace «видит» только свои сущности. Контейнер — процесс (или дерево процессов) в наборе namespace’ов.
| Namespace | Изолируемый ресурс |
|---|---|
| PID | Дерево процессов: внутри контейнера свой PID 1. |
| Network | Сетевые интерфейсы, сокеты, маршруты. |
| Mount | Точки монтирования (своё дерево ФС). |
| UTS | Hostname и domainname. |
| IPC | Очереди сообщений, семафоры. |
| User | UID/GID (опционально; маппинг на непривилегированного на хосте). |
Без namespace контейнер был бы обычным процессом на хосте. Создание namespace’ов и запуск процесса внутри них выполняет runtime (runc, containerd); Docker поверх этого даёт образ, сеть и удобный CLI. Просмотр namespace процесса на хосте: ls -la /proc/<pid>/ns/ — там симлинки на соответствующие ns; у процессов в одном контейнере они совпадают.
cgroups
cgroups (control groups) — ограничение и учёт ресурсов (CPU, память, I/O) для группы процессов. Контейнеру создаётся своя группа; в неё записываются лимиты (memory.max, cpu.max и т.д. в cgroups v2, или аналог в v1). При превышении лимита памяти ядро может убить процесс (OOM); CPU ограничивается долей или квотой. Docker/Kubernetes задают лимиты через свой API; в итоге они попадают в cgroups на хосте. Без cgroups контейнер мог бы занять все CPU и память хоста; с ними — изоляция по ресурсам и предсказуемое поведение при нескольких контейнерах на одной машине.
OverlayFS
OverlayFS — тип ФС в ядре: несколько нижних слоёв (read-only) и один верхний (read-write) объединяются в одно видимое дерево. Изменения пишутся только в верхний слой. Docker использует драйвер overlay2: слои образа — нижние слои, слой контейнера — верхний. Так один и тот же образ разделяется между контейнерами (нижние слои общие), а изменения в каждом контейнере изолированы. Понимание overlay помогает при отладке «почему не вижу файл» или «где физически лежат данные контейнера» (upper dir на хосте).
Capabilities
Capabilities — разбиение полномочий root на отдельные права (например, CAP_NET_BIND_SERVICE — бинд на порты < 1024, CAP_SYS_ADMIN — админские операции). Контейнер по умолчанию получает ограниченный набор capabilities; при необходимости добавляют только нужные (например, --cap-add=NET_RAW для ping), а не дают полный --privileged. Понимание capabilities нужно для безопасной настройки контейнеров и для разбора «permission denied» внутри контейнера при попытке привилегированной операции.
chroot vs контейнер
chroot — смена корня файловой системы для процесса: процесс «видит» другой каталог как /. Изоляции процессов, сети, UTS, пользователей нет — только другое дерево файлов. Контейнер = namespaces (изоляция процессов, сети, mount, UTS и т.д.) + cgroups (лимиты) + своё дерево ФС (часто на overlay). chroot — лишь один из элементов (смена корня в mount namespace), но не достаточный для изоляции. Поэтому «chroot в каталог» — не контейнер; для полноценной изоляции нужен runtime (runc, Docker и т.д.), создающий все namespace’ы и cgroups.
Практика
Посмотреть namespaces контейнера
На хосте найти PID основного процесса контейнера (например, docker inspect <id> --format '{{.State.Pid}}' или crictl inspect <id>). Затем:
# Подставить PID процесса контейнера
PID=$(docker inspect --format '{{.State.Pid}}' <container_id>)
ls -la /proc/$PID/ns/
Вывод покажет симлинки на ns для pid, net, mnt, uts, ipc, user (если есть). Сравнить с процессом на хосте — у контейнера свои иноды ns (другое «окружение»). Зайти в namespace’ы с хоста можно через nsenter -t $PID -a (осторожно, это как вход «в» контейнер с хоста).
Ограничить ресурсы контейнера и сравнить с bare metal
Запуск контейнера с лимитами CPU и памяти:
docker run -d --name limit-test \
--cpus=0.5 \
--memory=128m \
--memory-swap=128m \
stress-ng --cpu 2 --vm 1 --vm-bytes 64M --timeout 30s
На хосте посмотреть cgroups контейнера (v2 пример):
# Путь к cgroup контейнера (Docker)
CONTAINER_ID=$(docker inspect -f '{{.Id}}' limit-test)
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/cpu.max
Сравнение с bare metal: тот же stress-ng без контейнера может занять все ядра и много памяти; в контейнере при превышении memory процесс будет убит (OOM), а CPU будет ограничен 0.5 ядра. Так видно, что лимиты реально применяются ядром через cgroups.
!!! tip "Практика"
Для DevOps важно: контейнер — это не виртуальная машина, а изолированный процесс на том же ядре. Всё, что умеет ядро (namespaces, cgroups, overlay), использует Docker/containerd. При проблемах с сетью, правами или ресурсами смотреть с хоста: /proc/<pid>/ns/, cgroups контейнера, overlay верхний слой.
Паттерны и антипаттерны
| Паттерн | Описание |
|---|---|
| Задавать лимиты CPU/памяти контейнерам | Иначе один контейнер может положить хост. |
| Добавлять только нужные capabilities | Не использовать --privileged без необходимости. |
| Понимать разницу chroot и контейнера | chroot — только смена корня ФС, не изоляция. |
| Антипаттерн | Почему плохо | Что делать |
|---|---|---|
| Считать контейнер «машиной» | Общее ядро с хостом; уязвимость ядра может затронуть хост. | Считать контейнер изолированным процессом и не давать лишних прав. |
| Игнорировать cgroups при «тормозах» | Контейнер может упираться в лимит CPU/памяти. | Смотреть использование и лимиты (docker stats, cgroup файлы). |
Дополнительные материалы
- namespaces(7)
- cgroups v2 — kernel
- overlayfs — kernel
- capabilities(7)
- Docker — 1. Базис — те же механизмы в контексте Docker