VM vs контейнер
«Контейнер — это как виртуалка, только лучше» — это популярное упрощение, и оно немного вводит в заблуждение. Правильнее так: контейнер и виртуальная машина решают одну и ту же задачу (изолированный запуск чужого софта), но на разных уровнях стека. У них разные накладные расходы, разная сила изоляции и разные сценарии.
В этом уроке посмотрим, что внутри VM и контейнера, чем они отличаются по ресурсам, и когда какой инструмент правильный.
Архитектура: что внутри
Главное отличие — уровень виртуализации:
Kernel space vs user space — ring 0 и ring 3- VM виртуализирует железо. Гостевая ОС думает, что у неё свой CPU, своя RAM, свои диски. Под капотом hypervisor транслирует это всё на реальное железо хоста.
- Контейнер виртуализирует userspace вид ядра. Процесс думает, что он один в системе, у него свой PID 1 и свой
/. Но ядро одно — хостовое.
Из этого следует всё остальное.
Накладные расходы — RAM, диск, время старта
Числа примерные и зависят от софта внутри, но порядок такой:
| Параметр | VM (Ubuntu 24.04 minimal) | Контейнер (alpine с одной программой) |
|---|---|---|
| Disk overhead | 800-1500 MB | 5-30 MB |
| RAM overhead | 256-512 MB | 5-20 MB (только сам процесс) |
| Boot time | 15-60 секунд | 100-500 миллисекунд |
| Density (на 16 ГБ RAM хосте) | ~10-20 VM | ~500-2000 контейнеров |
Это объясняет, почему весь cloud-нативный мир (Kubernetes, CI-раннеры, serverless под капотом) построен на контейнерах: можно поднять и опустить контейнер за полсекунды, а VM за это время не успеет даже начать загрузку.
Memory hierarchy — registers, cache, RAM, disk и реальная latencyКонкретный пример: GitHub Actions раннер крутит каждый job в отдельной VM. Время старта VM — около 30 секунд из 5-минутного бюджета job’а, это 10%. Если бы это был контейнер — было бы 1%.
Изоляция: VM сильнее
Контейнер слабее изолирован, чем VM. Это фундаментально.
В VM граница между гостем и хостом проходит через hypervisor. Чтобы вырваться из VM, нужно эксплуатировать уязвимость hypervisor’а — это редкие, дорогие баги, обычно с CVE на полгода работы команды Red Hat / VMware.
В контейнере граница проходит через namespaces в ядре. Уязвимость в ядре = выход из контейнера. Уязвимостей в Linux kernel находят десятки в год. Многие из них так или иначе могли быть использованы для container escape.
Поэтому:
- Для multi-tenant сценариев (запуск кода разных клиентов на одной машине, как у AWS Lambda или CI типа GitHub Actions для public репозиториев) — обычно используется VM, или специальный изолирующий runtime (gVisor, Kata Containers, Firecracker), который запускает контейнер внутри минимальной VM.
- Для single-tenant (свой проект, свой Postgres, свой Airflow на своём сервере) — контейнеры безопасны достаточно.
Когда нужна именно VM
Несмотря на повсеместное распространение контейнеров, VM остаются нужны в нескольких случаях:
Когда контейнер достаточно
Большинство современных задач разработки и production отлично решаются контейнерами. Особенно для Data Engineer:
- Локальный Postgres для разработки.
docker run -d -p 5432:5432 postgres:16.4— есть Postgres за 5 секунд. VM здесь будет overkill. - CI integration tests.
testcontainers-pythonподнимает Postgres/Kafka/Redis для теста, тест работает, контейнеры уезжают. С VM это было бы 20 секунд старта на каждый тест. - Airflow worker. Один контейнер = один процесс worker’а. Несколько контейнеров = горизонтальное масштабирование.
- ETL-задача как образ. Build однажды, deploy куда угодно (k8s, ECS, Airflow KubernetesPodOperator).
- Reproducible dev environments. Один
docker-compose up— и у всей команды одинаковое окружение, неважно у кого Mac, у кого Linux.
Гибрид: VM-внутри-контейнера и наоборот
В реальности часто стек получается комбинированный:
- macOS Docker Desktop / OrbStack — Docker-контейнеры запускаются внутри Linux VM (потому что macOS не имеет Linux kernel). Когда ты на маке запускаешь
docker run, на самом деле контейнер крутится в VM, которую ты не видишь. - AWS Fargate, Google Cloud Run — managed контейнерные платформы, под капотом используют micro-VM (Firecracker на AWS) для изоляции между разными клиентами.
- gVisor, Kata Containers — runtime’ы, которые запускают контейнеры в очень лёгких VM (~100 МБ overhead вместо 500 МБ). Используются там, где нужна и легкость контейнеров, и сильная изоляция.
- k8s nodes — это VM, на которых крутятся контейнеры. Облачный k8s — это GCP/AWS-VM с containerd внутри.
То есть на практике «VM vs container» — это не дихотомия, а слои. Снизу часто VM (для isolation и multi-tenancy), сверху контейнеры (для гибкости и плотности).
Попробуй сам
Если у тебя macOS с Docker Desktop или OrbStack, посмотри, сколько ресурсов берёт Linux VM, в которой крутятся контейнеры:
# OrbStack
orb info
Или для Docker Desktop — открой settings -> Resources, увидишь, сколько RAM и CPU выделено VM.
Запусти 10 контейнеров nginx:
for i in {1..10}; do
docker run -d --rm --name nginx-$i nginx:alpine
done
docker stats --no-stream
Посмотри MEM USAGE — каждый nginx ест ~3-8 МБ. Это и есть «плотность контейнера»: 10 одинаковых nginx занимают меньше памяти, чем одна Linux VM с одним nginx внутри.
Убери:
docker rm -f $(docker ps -q --filter "name=nginx-")