Перейти к содержанию

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 файлы).

Дополнительные материалы