Namespaces и cgroups
Два независимых механизма ядра. Первый ограничивает видимость, второй — объём.
Что делает каждый
ваш процесс
│
┌─────────────────┴─────────────────┐
│ │
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:
# Изнутри 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:
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
/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.max | memory.high | |
|---|---|---|
| Что делает | Убивает при превышении | Тормозит при приближении |
| Задаёт ли Docker | Да | Нет |
Задаёт ли systemd | MemoryMax= | MemoryHigh= |
| Следствие | Отказ мгновенный | Деградация перед отказом |
Отсюда практическое: у container'а нет «предупредительной полосы». Приложение работает нормально ровно до момента, когда его убивает ядро — без записи в собственный лог.
Единственный способ увидеть приближение заранее — счётчики:
docker exec имя cat /sys/fs/cgroup/memory.events
max 4213 ← упирался в предел 4213 раз
oom_kill 3 ← убит 3 раза
Подробнее
Урок 17.3. Namespaces
Урок 17.4. Cgroups v2
Урок 11.4. Ограничения ресурсов
Навигация
Раздел 17. Docker internals
Раздел 02. Основы
Главное оглавление