Главная/Материалы/Материал

Namespaces и cgroups

Два независимых механизма ядра. Первый ограничивает видимость, второй — объём.

Что делает каждый

text
                     ваш процесс
                          │
        ┌─────────────────┴─────────────────┐
        │                                   │
   namespaces                            cgroups
   ЧТО ВИДНО                          СКОЛЬКО МОЖНО
        │                                   │
  ┌─────┴──────┐                     ┌──────┴──────┐
  │ pid        │ свои номера         │ memory.max  │ предел памяти
  │ net        │ свои интерфейсы     │ cpu.max     │ доля процессора
  │ mnt        │ своя ФС             │ pids.max    │ число задач
  │ uts        │ своё имя узла       │ io.max      │ ввод-вывод
  │ ipc        │ своя очередь        └─────────────┘
  │ cgroup     │ свой корень
  │ user       │ ← хостовый: root внутри = root снаружи
  │ time       │ ← не свой и не хостовый (см. ниже)
  └────────────┘

Как читать

Слева и справа — разные вопросы. Процесс может видеть только свои файлы и при этом съесть всю память машины; может быть ограничен по памяти и видеть все процессы хоста. Одно не заменяет другого.

Что здесь неочевидно

Docker создаёт шесть namespace из восьми. Не создаётся user — он остаётся хостовым.

С time сложнее, и на реальном запуске это видно: inode time внутри container'а отличается от хостового, но совпадает у разных container'ов. То есть своего time namespace container не получает, а хостовым не пользуется (verify/FACTS.md).

Отсутствие user namespace — самое существенное: преобразования идентификаторов нет, и UID 0 внутри container'а это UID 0 на хосте. Разделяют их namespaces и capabilities, а не разные пространства пользователей.

Проверить, какие namespace различаются, можно сравнением inode:

bash
# Изнутри container'а — прав root не требуется
docker run --rm alpine sh -c 'for n in pid net mnt uts ipc user time cgroup; do
  printf "%s %s\n" "$n" "$(readlink /proc/self/ns/$n)"; done' > /tmp/ns_in
# То же на хосте
for n in pid net mnt uts ipc user time cgroup; do
  printf "%s %s\n" "$n" "$(readlink /proc/self/ns/$n)"; done > /tmp/ns_out
paste /tmp/ns_in /tmp/ns_out | awk '{print "  "$1": "($2==$4 ? "ОБЩИЙ с хостом" : "отличается")}'

Сравнение изнутри лучше, чем чтение /proc/PID/ns снаружи: второе требует прав root, потому что процесс в container'е принадлежит другому пользователю.

Сравнивать нужно по inode, а не по поведению: поведение обманчиво.

Ограничения читаются изнутри без прав root:

bash
docker exec имя cat /sys/fs/cgroup/memory.max     # предел
docker exec имя cat /sys/fs/cgroup/memory.current # текущее
docker exec имя cat /sys/fs/cgroup/memory.events  # счётчики давления
docker exec имя cat /sys/fs/cgroup/cpu.max        # квота и период

docker inspect показывает запрошенное, а не применённое. Расхождение с чтением изнутри означает, что запрос не выполнился: отключён контроллер, несовместимо ядро, отсутствует возможность.

Иерархия cgroup v2

text
/sys/fs/cgroup/                        корень
├── cgroup.controllers                 какие контроллеры доступны
├── cgroup.subtree_control             какие включены для потомков
└── system.slice/
    └── docker-ID.scope/               cgroup вашего container'а
        ├── memory.max                 256M
        ├── memory.high                max ← Docker его НЕ задаёт
        ├── memory.current             183M
        ├── memory.events              счётчики
        └── cpu.max                    50000 100000  (= 0.5 CPU)

Внутри container'а тот же путь выглядит как 0::/ — своё пространство cgroup.

memory.max и memory.high

memory.maxmemory.high
Что делаетУбивает при превышенииТормозит при приближении
Задаёт ли DockerДаНет
Задаёт ли systemdMemoryMax=MemoryHigh=
СледствиеОтказ мгновенныйДеградация перед отказом

Отсюда практическое: у container'а нет «предупредительной полосы». Приложение работает нормально ровно до момента, когда его убивает ядро — без записи в собственный лог.

Единственный способ увидеть приближение заранее — счётчики:

bash
docker exec имя cat /sys/fs/cgroup/memory.events
text
max 4213        ← упирался в предел 4213 раз
oom_kill 3      ← убит 3 раза

Подробнее

Урок 17.3. Namespaces
Урок 17.4. Cgroups v2
Урок 11.4. Ограничения ресурсов


Навигация

Раздел 17. Docker internals
Раздел 02. Основы
Главное оглавление

Markdown на GitHub ↗