Что на самом деле запускает docker run, чем контейнер отличается от виртуальной машины, откуда берутся OOMKilled, медленные сборки, steal time и тормоза Docker на Mac.
Финал серии «Как работает компьютер». Листайте стрелками ← →, свайпом или кнопками внизу. В схемах нажимайте кнопки и двигайте ползунки.
Виртуальная машина изображает целый компьютер: у каждой своё ядро, своя ОС, свои драйверы. Контейнер — обычный процесс хоста, которому ядро показывает урезанный мир. Ядро одно на всех.
Переключайте вид и двигайте число экземпляров. Сервер — 64 ГБ RAM, приложению нужно 200 МБ.
Namespace подменяет для процесса одну глобальную таблицу ядра своей копией. Их восемь типов, контейнер — процесс, у которого почти все свои. Создаются флагами clone() и unshare(), вход в чужие — setns().
Выключите namespace — увидите, что процесс в контейнере видит вместо своей копии. Это ровно то, что делают --pid=host, --network=host и подобные флаги.
PID-namespace вложенные: процесс виден в своём и во всех родительских, и в каждом у него свой номер. Хост видит всё, контейнер — только своё поддерево. Оба номера — в строке NSpid файла /proc/PID/status.
docker exec не создаёт контейнер: он входит в существующие namespaces через setns().Нажимайте кнопки и следите за обеими колонками.
Namespaces решают, что процесс видит, cgroups — сколько он потребляет. Контейнер — это каталог в /sys/fs/cgroup, лимиты — файлы в нём. У каждого контроллера своя реакция на превышение.
cpu.stat, memory.events, pids.events, давление — в *.pressure (PSI).Выберите контроллер и двигайте нагрузку. Основы cpu.max — в «Процессах и потоках», memory.max — в «Виртуальной памяти».
Контейнер запущен с --memory=512m --cpus=2 на сервере с 64 ГБ и 32 ядрами. Внутри выполнили free -m и nproc. Что они покажут?
free читает /proc/meminfo, а он не виртуализирован: это память хоста. nproc смотрит маску sched_getaffinity, а --cpus — это квота в cpu.max, не привязка к ядрам. Поэтому и os.cpu_count(), и runtime.NumCPU() видят 32. Правда — в /sys/fs/cgroup/memory.max и cpu.max; JVM и .NET читают их сами, Go с 1.25 — только cpu.max для GOMAXPROCS; остальным передайте размеры пулов явно.Каждая инструкция Dockerfile, меняющая файлы, даёт слой — tar-архив с разницей. При запуске слои становятся read-only каталогами lowerdir, сверху кладётся пустой upperdir контейнера, а процесс видит их склейку — merged.
Выберите файл в merged и действие.
Шаг берётся из кэша, если не изменились сама инструкция, родительский слой и — для COPY — контрольные суммы копируемых файлов. Первый промах инвалидирует все шаги ниже.
.dockerignore обязателен: без него COPY . . зависит и от .git, и от логов.RUN --mount=type=cache,target=/root/.cache/pip.Поменяйте файл, соберите, затем переключите порядок.
Размер — это время скачивания на каждом новом узле при автоскейлинге, а каждый лишний пакет — ещё строки в отчёте сканера уязвимостей. Слои с одинаковым хешем скачиваются один раз и делятся между образами.
manylinux не встают, пакеты собираются из исходников, другой malloc и DNS-резолвер.kubectl debug с эфемерным контейнером.python:3.12~1 ГБpython:3.12-slim~130 МБpython:3.12-alpine~50 МБdistroless/python3~50 МБgolang:1.23~820 МБdistroless/static + Go~15 МБ
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/api
FROM gcr.io/distroless/static:nonroot
COPY --from=build /app /app
ENTRYPOINT ["/app"] # ~15 МБ вместо ~850
В Dockerfile: COPY id_rsa /root/.ssh/, затем RUN git clone …, затем RUN rm /root/.ssh/id_rsa. Образ опубликовали. Ключ в безопасности?
rm лишь добавляет whiteout в новый слой. docker save или crane export — и ключ на руках. То же с ENV TOKEN=… и ARG: они видны в docker history. Секреты при сборке передают через RUN --mount=type=secret или --mount=type=ssh — они не попадают ни в один слой.docker runDocker — это цепочка: CLI просит демона, демон — containerd, containerd запускает shim и runc. Сам контейнер создаёт маленькая программа runc за несколько системных вызовов и уходит. В Kubernetes вместо dockerd — kubelet, говорящий с containerd через CRI.
Нажимайте «Дальше» — шаг за шагом от команды до вашего PID 1.
У контейнера свой сетевой namespace с одним eth0. Это конец veth-пары — виртуального кабеля, второй конец которого воткнут в мост docker0 на хосте. Наружу — через NAT, внутрь — через DNAT опубликованного порта.
Выберите сценарий и проведите пакет по шагам. Как работает сам NAT — в «Сетях».
Pod — это не контейнер, а группа контейнеров с общей сетью. Первым запускается крошечный pause: он держит net-, ipc- и uts-namespace. Остальные контейнеры входят в них через setns().
localhost — так работают sidecar-прокси вроде Envoy в Istio.shareProcessNamespace.
$ crictl pods
POD ID NAME STATE
7f3c1a9e2b10 api-7d9f8-xk2lp Ready
$ crictl ps --pod 7f3c1a9e2b10
CONTAINER IMAGE NAME
a41d… api api
9be0… envoy istio-proxy
$ lsns -t net
NS TYPE NPROCS PID COMMAND
4026532871 net 9 51207 /pause
# api и envoy — в том же namespace,
# что и pause: 9 процессов на один IP
$ nsenter -t 51207 -n ss -ltn
LISTEN 0.0.0.0:8080 # api
LISTEN 0.0.0.0:15001 # envoy
В pod два контейнера: основное приложение и sidecar с метриками. Оба по умолчанию слушают 0.0.0.0:8080. Что будет при запуске?
bind(), тот и занял порт; второй контейнер уйдёт в CrashLoopBackOff. Поле containerPort в манифесте ничего не настраивает — это документация. Порт меняют в конфигурации самого приложения.mount, reboot, init_module, kexec_load…--privileged снимает всё сразу. Это уже не изоляция, а root на хосте.Выберите режим запуска и посмотрите, что получится у процесса с UID 0 внутри.
Гостевой код исполняется прямо на процессоре — никакой эмуляции. VT-x и AMD-V добавили режим «гость»: ядро гостя работает в своём кольце 0, а чувствительные инструкции вызывают VM exit — выход в гипервизор, который их эмулирует и возвращает управление.
Запускайте гостевые операции и смотрите на счётчик выходов.
В ВМ адрес переводится дважды: виртуальный гостя → физический гостя по таблицам гостевого ядра, затем → физический хоста по таблицам гипервизора (EPT у Intel, NPT у AMD). Без них гипервизор вёл бы теневые таблицы и ловил каждую запись в таблицы гостя.
Цена — промах TLB: каждый адрес гостевой таблицы тоже надо перевести. Выберите размеры страниц и нажмите «Пройти». Про таблицы страниц и TLB — в «Виртуальной памяти».
vCPU — это поток на хосте. Если соседи хотят больше процессора, чем есть физических ядер, ваш vCPU стоит в очереди, хотя готов работать. Гипервизор сообщает гостевому ядру, сколько времени vCPU простоял в очереди, а ядро показывает это как st в top.
Двигайте нагрузку соседей. Хост — 16 потоков, у вас 4 vCPU.
Чужой код в контейнере на общем ядре — риск. Полная ВМ — дорого. Облака выбрали промежуточные варианты.
Lambda держит тысячи microVM на одном сервере и умеет восстанавливать их из снимков памяти (SnapStart) — отсюда холодный старт в сотни миллисекунд, а не секунды.
Контейнеру нужно ядро Linux, а у macOS его нет. Docker Desktop, OrbStack, Colima запускают Linux в лёгкой ВМ через Virtualization.framework, и контейнеры живут там.
-v .:/app — это общий каталог через virtiofs. Каждый stat и open пересекает границу ВМ, поэтому npm install и Python-импорты из 30 000 файлов тормозят в разы.node_modules и .venv — в именованный том внутри ВМ; синхронизация файлов вместо монтирования.linux/amd64 на Apple Silicon работает через Rosetta или QEMU — медленнее и с сюрпризами. Собирайте linux/arm64.docker stats не видит Mac.
$ uname -sm # на Mac
Darwin arm64
$ docker run --rm alpine uname -sr
Linux 6.10.14-linuxkit # ядро ВМ
# медленно: node_modules идёт через virtiofs
$ docker run -v .:/app node npm ci
# быстро: node_modules в томе внутри ВМ
$ docker run -v .:/app \
-v nm:/app/node_modules node npm ci
$ docker run --platform linux/amd64 …
WARNING: The requested image's platform
(linux/amd64) does not match the detected
host platform (linux/arm64/v8)
lsns, ls -l /proc/PID/ns. Зайти в namespace контейнера с инструментами хоста — nsenter -t PID -n (только сеть: ss, tcpdump внутри distroless).systemd-cgls, systemd-cgtop, файлы в /sys/fs/cgroup/…/: cpu.stat, memory.events, *.pressure.docker inspect (PID на хосте, слои overlay, IP), в Kubernetes — crictl inspect, crictl ps.st в top и vmstat 1, systemd-detect-virt, на хосте KVM — perf kvm stat покажет VM exits по причинам.
$ PID=$(docker inspect -f '{{.State.Pid}}' api)
$ grep NSpid /proc/$PID/status
NSpid: 48213 1
$ ls -l /proc/$PID/ns | awk '{print $NF}'
cgroup:[4026532880] ipc:[4026532814]
mnt:[4026532812] net:[4026532817] …
$ nsenter -t $PID -n ss -ltnp # сеть контейнера
$ cat /proc/$PID/cgroup
0::/system.slice/docker-3f9c….scope
$ cd /sys/fs/cgroup/system.slice/docker-3f9c….scope
$ cat cpu.stat | grep throttled
nr_throttled 1840
throttled_usec 92314411
$ cat memory.events pids.events
$ docker inspect -f '{{.GraphDriver.Data.UpperDir}}' api
/var/lib/docker/overlay2/9d1e…/diff
/sys/fs/cgroup: пулы, кучи и воркеры настраивайте явно.fork.Контейнер — это не маленькая виртуальная машина, а процесс, которому ядро недоговаривает.