Операционные системы

Контейнеризация изнутри

Docker изменил мир разработки. "Works on my machine" стало анахронизмом. Kubernetes управляет миллиардами контейнеров в дата-центрах Google, AWS, Microsoft. Но мало кто понимает, что происходит под капотом. Контейнеры - это комбинация трёх механизмов Linux: namespaces (изоляция), cgroups (ограничения), OverlayFS (эффективное хранение). Понимание этих механизмов - это ключ к отладке продакшн инцидентов, оптимизации производительности и проектированию cloud-native систем.

  • **Google запускает 2+ миллиарда контейнеров в неделю** через Borg (предшественник Kubernetes). Вся инфраструктура Google (Search, Gmail, YouTube) работает в контейнерах. Понимание namespaces и cgroups критично для SRE в FAANG.
  • **Почему Kubernetes Pod падает с OOMKilled?** Это не баг Kubernetes - это cgroup memory limit в действии. Процесс съел больше памяти, чем разрешено в `resources.limits.memory`. OOM killer убил процесс. Debugging: `kubectl describe pod` → Events → OOMKilled. Fix: увеличить лимит или оптимизировать приложение.
  • **Docker образ весит 2GB, но билд занимает 10 секунд.** Это OverlayFS в действии. Docker переиспользует закэшированные слои. Изменил одну строку кода - пересобрался только последний слой (копирование файлов). Остальные слои (apt-get install, npm install) взяты из cache. Понимание слоёв - это 10× ускорение CI/CD.

Цели урока

  • Знать Linux namespaces: PID, NET, MNT, UTS, IPC, USER, CGROUP
  • Cgroups v1 vs v2: иерархия, memory/cpu/io контроллеры
  • OverlayFS: union mount, copy-up при записи, layered images
  • Container runtime stack: runc (low-level), containerd, Docker, Podman
  • Безопасность: rootless containers, user namespaces, seccomp-bpf профили

Linux Namespaces - изоляция процессов

**Linux Namespaces** - механизм изоляции ресурсов системы, который создаёт иллюзию, что процесс работает в отдельной операционной системе. Это фундамент контейнеризации: Docker, Kubernetes, containerd - все построены на namespaces.

Аналогия: многоквартирный дом. Каждая квартира (namespace) изолирована, имеет свою дверь (PID 1), свои окна (network), свои счётчики (filesystems). Жильцы одной квартиры не видят, что происходит в соседних. Но физически все квартиры в одном здании (одно ядро Linux).

**Типы namespaces в Linux:** - **PID namespace** - изоляция ID процессов (контейнер видит свой PID 1) - **Network namespace** - отдельный сетевой стек (IP, routes, firewall) - **Mount namespace** - изоляция точек монтирования файловых систем - **UTS namespace** - отдельное hostname и domain name - **IPC namespace** - изоляция межпроцессного взаимодействия (SysV IPC, POSIX queues) - **User namespace** - маппинг UID/GID (root в контейнере ≠ root на хосте) - **Cgroup namespace** - скрытие иерархии cgroups от процесса

**PID namespace:** Каждый контейнер имеет свою изолированную иерархию процессов. Процесс с PID 1 внутри контейнера может быть процессом 15234 на хосте. Процессы внутри контейнера не видят процессы хоста.

Network namespace - как Docker создаёт виртуальные сети

Когда запускаешь `docker run -p 8080:80 nginx`, Docker создаёт: 1. **Новый network namespace** с собственным loopback (127.0.0.1) и сетевым стеком 2. **Пару veth устройств** (виртуальный Ethernet кабель): один конец в контейнере (eth0), другой на хосте (vethXXX) 3. **Bridge docker0** на хосте, к которому подключаются все veth 4. **iptables правила** для проброса порта 8080 (хост) → 80 (контейнер) Каждый контейнер думает, что у него есть реальная сетевая карта eth0 с IP адресом. На самом деле это виртуальное устройство в изолированном namespace.

**User namespace - безопасность контейнеров:** Без user namespace процесс с UID 0 в контейнере = root на хосте. Если процесс сбежит из контейнера, он получит полный контроль над системой. С user namespace: - UID 0 в контейнере → UID 1000 на хосте (обычный пользователь) - Даже если процесс вырвется, у него нет root прав Docker использует user namespaces опционально (`--userns-remap`), Kubernetes/Podman включают по умолчанию.

Запущено 3 Docker контейнера. Что увидит процесс с PID 1 внутри первого контейнера при выполнении `ps aux`?

Control Groups - управление ресурсами

**Control Groups (cgroups)** - механизм ядра Linux для ограничения, учёта и изоляции ресурсов (CPU, память, диск I/O, сеть) для групп процессов. Если namespaces отвечают за изоляцию видимости, то cgroups контролируют доступ к ресурсам.

Namespaces говорят: "процесс не видит соседей". Cgroups говорят: "процесс может использовать максимум 512MB RAM и 0.5 CPU cores". Вместе они создают контейнер: изолированное окружение с гарантированными ресурсами.

**Основные cgroup controllers (v2):** - **cpu** - лимиты процессорного времени (CFS bandwidth) - **memory** - ограничение RAM (hard/soft limits, OOM killer) - **io** - контроль дисковых операций (bandwidth, IOPS) - **pids** - максимальное количество процессов в группе - **cpuset** - привязка к конкретным CPU cores - **devices** - разрешения на доступ к устройствам (/dev) - **freezer** - приостановка всех процессов в группе

Как Kubernetes использует cgroups для ресурсных лимитов

Манифест Kubernetes: ```yaml resources: limits: memory: "512Mi" cpu: "500m" # 0.5 CPU cores requests: memory: "256Mi" cpu: "250m" ``` Kubernetes (через containerd/CRI-O) создаёт cgroup: - `memory.max = 512 * 1024 * 1024` (hard limit - OOM kill при превышении) - `memory.low = 256 * 1024 * 1024` (soft limit - гарантированная память) - `cpu.max = 50000 100000` (50% CPU time) Если под пытается съесть 600 MB - **OOM killer убивает процесс**. Если пытается загрузить CPU на 100% - **throttling** ограничивает до 50%.

**Memory cgroup и OOM Killer:** Когда процесс в cgroup превышает `memory.max`, ядро запускает Out-Of-Memory killer. OOM killer выбирает процесс для убийства на основе: 1. **oom_score** - эвристика "насколько вреден процесс" (много памяти + низкий приоритет = высокий score) 2. **oom_score_adj** - ручная корректировка (от -1000 до +1000) Docker устанавливает `oom_score_adj` так, чтобы контейнер убивали раньше системных процессов.

**CPU throttling в действии:** Если контейнер с лимитом `cpu.max = 50000 100000` (50% одного ядра) пытается использовать больше CPU: - Ядро отслеживает CPU time за period (100ms) - Как только процесс израсходовал quota (50ms), он **throttled** (заморожен) - Остаток периода процесс не получает CPU time - В следующем периоде quota обновляется Это гарантирует, что "шумный сосед" не украдёт CPU у других контейнеров.

Контейнер с лимитом памяти 512 MB пытается аллоцировать 600 MB RAM. Что произойдёт?

OverlayFS - слоёная файловая система

**OverlayFS** - union файловая система, которая объединяет несколько директорий в одну виртуальную файловую систему. Этот приём позволяет Docker образам быть маленькими, а контейнерам - запускаться мгновенно.

Аналогия: прозрачные плёнки с рисунками. Если положить одну на другую - получается объединённая картина. Нижние слои (read-only) - базовый образ. Верхний слой (read-write) - изменения контейнера. При удалении контейнера верхний слой исчезает, базовый образ остаётся нетронутым.

**Структура OverlayFS:** - **Lower layers (lowerdir)** - read-only слои базового образа (Ubuntu, Nginx, код приложения) - **Upper layer (upperdir)** - read-write слой для изменений контейнера - **Work directory (workdir)** - временная директория для atomic операций - **Merged view** - объединённая файловая система, которую видит контейнер

Как Docker использует слои для экономии места

10 контейнеров с Ubuntu 22.04 базой (размер 77 MB). Без OverlayFS нужно 770 MB. С OverlayFS: - **1 lower слой** с Ubuntu (77 MB) - shared между всеми контейнерами - **10 upper слоёв** (по одному на контейнер) - только изменения (обычно килобайты) Итого: ~77 MB вместо 770 MB. Экономия 90%! При старте контейнера Docker не копирует образ. Просто монтирует lower слои read-only и создаёт пустой upper слой. Запуск контейнера - мгновенный.

**Copy-on-Write (CoW):** Когда процесс в контейнере модифицирует файл из lower слоя, OverlayFS копирует его в upper слой и изменяет копию. Оригинал в lower остаётся нетронутым. Это называется Copy-on-Write.

**Whiteout files - удаление в OverlayFS:** Как удалить файл из lower слоя (read-only)? Нельзя модифицировать lower. Решение: в upper создаётся **whiteout file** (character device 0/0): ```bash rm overlay/merged/file.txt # В upper появился whiteout marker ls -l overlay/upper/ c--------- 1 root root 0, 0 Dec 25 12:00 file.txt ``` OverlayFS видит whiteout в upper и скрывает файл из lower. Файл "удалён" в merged view, но физически остался в lower (образ не тронут).

Почему сборка Docker образа должна быть оптимизирована по слоям

Плохой Dockerfile: ```dockerfile FROM node:18 COPY . /app # Весь код в один слой RUN npm install # Зависимости после кода ``` При изменении **одного файла** кода инвалидируется слой COPY и всё после него. Docker пересобирает `npm install` (заново скачивает пакеты) - потеря времени. Хороший Dockerfile: ```dockerfile FROM node:18 COPY package*.json /app/ # Зависимости отдельно RUN npm install # Кэшируется, если package.json не менялся COPY . /app # Код в последнем слое ``` Теперь изменение кода не инвалидирует `npm install`. Docker переиспользует закэшированный слой. Сборка за секунды вместо минут.

5 контейнеров запущены из одного образа Ubuntu (размер 100 MB). Каждый контейнер записал 10 MB логов. Сколько дискового пространства занято?

Container Runtime - от Docker до Kubernetes

**Container Runtime** - программа, которая управляет жизненным циклом контейнеров: создание namespaces, настройка cgroups, монтирование OverlayFS, запуск процесса. Docker, containerd, CRI-O, runc - все это container runtimes на разных уровнях абстракции.

**Уровни container runtimes:** - **High-level runtime** - управляет образами, сетью, volume (Docker, containerd, CRI-O) - **Low-level runtime** - создаёт контейнер из bundle (runc, crun, kata-runtime) - **CRI (Container Runtime Interface)** - стандарт Kubernetes для работы с runtimes

**runc - референсная реализация OCI (Open Container Initiative).** OCI Runtime Spec описывает JSON config: - Какой rootfs использовать (OverlayFS merged) - Какие namespaces создать (pid, net, mount, uts, ipc, user) - Какие cgroups настроить (memory.max, cpu.max) - Какой процесс запустить (entrypoint + args) - Какие capabilities дать процессу (CAP_NET_ADMIN, CAP_SYS_ADMIN)

От Docker к Kubernetes: эволюция container runtime

**2013-2016: Docker моноплатформа** Docker Engine - всё в одном: build, pull, run, networking. Kubernetes использовал Docker через `docker-shim` (прослойка для вызова Docker API). **2016: containerd выделен из Docker** Docker разделён на компоненты: - `dockerd` - CLI, API, build - `containerd` - управление контейнерами, образами - `runc` - low-level runtime Kubernetes начал поддерживать containerd напрямую (минуя Docker). **2020: Docker устарел в Kubernetes** Kubernetes удалил `dockershim`. Теперь используют containerd или CRI-O напрямую через CRI. Контейнеры остались теми же (OCI standard), только runtime изменился. **Почему containerd, а не Docker?** Docker - слишком "жирный" для Kubernetes (build, volumes, networks, swarm). Kubernetes нужен только runtime. containerd - минималистичный, только запуск контейнеров.

**CNI (Container Network Interface):** Плагины для настройки сети контейнеров. Когда containerd создаёт контейнер, он вызывает CNI плагин (bridge, flannel, calico): 1. Создать network namespace 2. Создать veth пару (виртуальный кабель) 3. Один конец подключить к bridge, другой в namespace контейнера 4. Назначить IP адрес (через IPAM плагин) 5. Настроить маршруты и iptables

**Security: capabilities и seccomp** Linux capabilities - гранулярные привилегии вместо "all or nothing" root. По умолчанию Docker даёт контейнеру ограниченный набор capabilities: - `CAP_NET_RAW` - создавать raw sockets (ping) - `CAP_CHOWN` - менять владельца файлов Но **НЕ даёт**: - `CAP_SYS_ADMIN` - монтировать файловые системы, менять namespaces - `CAP_NET_ADMIN` - настраивать сеть (iptables) **seccomp** (Secure Computing Mode) - whitelist системных вызовов. Docker блокирует опасные syscalls: `reboot()`, `swapon()`, `mount()`. Даже если процесс скомпрометирован, он не может навредить хосту.

Контейнеры - это лёгкие виртуальные машины

Контейнеры - это изолированные процессы на хост-системе, использующие одно ядро Linux

Ключевые идеи

  • **Namespaces изолируют видимость ресурсов.** PID namespace - контейнер видит только свои процессы (PID 1 внутри ≠ PID на хосте). Network namespace - отдельный IP стек (veth пары + bridge). Mount namespace - изоляция файловых систем. User namespace - root в контейнере ≠ root на хосте (безопасность).
  • **Cgroups ограничивают потребление ресурсов.** Memory cgroup: `memory.max = 512MB` → OOM killer при превышении. CPU cgroup: `cpu.max = 50000/100000` → throttling до 50% одного ядра. IO cgroup: ограничение bandwidth диска. Kubernetes `resources.limits` → напрямую маппятся в cgroup файлы.
  • **OverlayFS экономит место и ускоряет запуск.** Базовый образ (Ubuntu, Nginx) - read-only слои, shared между контейнерами. Изменения контейнера - отдельный read-write слой (upper). Copy-on-Write: модификация файла из lower → копируется в upper. 10 контейнеров с одним образом = 1× размер образа + N× размер изменений (обычно MB).
  • **Container runtime - слои абстракции.** Docker/containerd (high-level): pull образов, управление сетью, storage. runc (low-level): создание namespaces, настройка cgroups, exec процесса. Kubernetes использует containerd/CRI-O через CRI API. Docker образы = OCI образы (универсальный стандарт).

Связанные темы

Контейнеризация стоит на фундаменте операционных систем и связана с распределёнными системами:

  • Процессы и namespaces — Контейнер - это группа процессов в изолированных namespaces. PID namespace создаёт иллюзию отдельной системы для fork/exec
  • Виртуальная память — User namespace маппит UID/GID через страницы памяти. OverlayFS использует page cache для эффективного чтения слоёв
  • Файловые системы — OverlayFS - union filesystem, объединяющий несколько директорий. Mount namespace изолирует mount points контейнера
  • Виртуализация — Контейнеры - это OS-level virtualization (vs hardware virtualization у VM). Kata Containers комбинирует контейнеры и micro-VM

Вопросы для размышления

  • Docker контейнеры считаются менее изолированными, чем VM. Какие атаки возможны через общее ядро Linux? Как защититься (user namespaces, seccomp, AppArmor)?
  • Kubernetes Pod с 3 контейнерами: они шарят PID namespace или каждый имеет свой? А network namespace? Почему контейнеры в одном Pod могут общаться через localhost?
  • Serverless платформа (AWS Lambda, Cloud Run). Как быстро стартовать контейнер для обработки HTTP запроса (cold start < 100ms)? Какие оптимизации нужны (pre-warmed containers, snapshot restore, firecracker microVM)?

Связанные уроки

  • net-47-container-networking
  • net-48-kubernetes-networking
  • rt-96
Контейнеризация изнутри

0

1

Войти

VM эмулирует железо: каждая VM имеет свой kernel, init system, полный OS. Hypervisor изолирует VM через hardware virtualization (Intel VT-x, AMD-V). Контейнер - это обычный процесс Linux, изолированный через namespaces и cgroups. **Одно ядро** обслуживает все контейнеры. Контейнер стартует за миллисекунды (просто fork + exec), VM - за секунды (нужно загрузить kernel). Поэтому: - Контейнер с Ubuntu не может запуститься на Windows без WSL2 (нужен Linux kernel) - VM с Ubuntu работает на любом hypervisor (VMware, VirtualBox, KVM) Контейнеры легче (MB вместо GB), быстрее (ms вместо секунд), но менее изолированы (общее ядро = общая attack surface). Для максимальной изоляции используют Kata Containers (контейнеры в микро-VM).

Kubernetes удалил поддержку Docker в версии 1.24. Означает ли это, что Docker образы больше не работают в Kubernetes?