Стройте ответ вокруг владения состоянием, лимитов, отказа и восстановления. Определение становится инженерным ответом только в конкретном production-сценарии.
Вопросы и ответы
8 подробных ответов
01Что такое контейнер на уровне ядра Linux и какие механизмы его реализуют?
concept
Короткий ответ: Контейнер — это обычный процесс Linux, которому ядро подсунуло изолированный «вид» системы: неймспейсы прячут чужое, cgroups ограничивают ресурсы, слоёная файловая система даёт свой rootfs. Гостевого ядра нет — ядро одно на всех, и в этом настоящая разница с VM.
Подробно:
- Namespaces — изоляция видимости: каждый тип прячет свой кусок системы.
- cgroups — лимиты CPU, памяти, IO: сколько процессу можно, а не что он видит.
- Слоёный rootfs (overlayfs) — собственная файловая система поверх общих read-only слоёв образа.
| Неймспейс | Что изолирует |
|---|---|
| pid | дерево процессов: внутри свой PID 1 |
| net | сетевой стек: интерфейсы, маршруты, свои порты |
| mnt | точки монтирования, свой rootfs |
| uts | hostname и domainname |
| ipc | System V IPC, POSIX-очереди |
| user | маппинг UID/GID: root внутри ≠ root снаружи |
| cgroup | видимую иерархию cgroups |
⚠️ Частая ошибка: «контейнер — это лёгкая VM». У VM гипервизор и своё гостевое ядро; контейнер делит ядро хоста — поэтому стартует за миллисекунды, но изоляция слабее: уязвимость ядра пробивает все контейнеры сразу.
02Как overlayfs хранит слои образа и почему rm в позднем шаге Dockerfile не уменьшает образ?
middle
Короткий ответ: overlayfs склеивает несколько read-only слоёв (lowerdir) и один записываемый (upperdir) в единый merged-вид. Слои образа неизменяемы: rm в позднем шаге не трогает нижний слой, а лишь кладёт whiteout-файл в верхний — файл скрыт из merged, но его байты по-прежнему лежат в образе.
Подробно:
- lowerdir — слои образа: только чтение, контент-адресуемы, шарятся между образами и контейнерами.
- upperdir — записываемый слой контейнера (или следующего шага сборки).
- copy-up — первое изменение файла копирует его целиком из lower в upper; дальше правится копия.
- Удаление — whiteout-файл (символьное устройство 0:0) в верхнем слое маскирует одноимённый файл ниже.
merged (то, что видит контейнер)
────────────────────────────────────
upperdir: rm big.bin → .wh.big.bin ← whiteout, файл «исчез»
lowerdir: RUN wget big.bin (100 MB) ← слой неизменяем, 100 MB в образе
lowerdir: FROM ubuntu
⚠️ Частая ошибка: чистить кэш отдельным шагом RUN rm -rf /var/cache/.... Уменьшает образ только удаление в том же RUN, где файлы создавались (wget ... && rm ...), либо multi-stage build.
03cgroups v2: что изменилось по сравнению с v1 и как реально ограничиваются CPU и память?
senior
Короткий ответ: v2 заменила россыпь отдельных иерархий (по одной на контроллер) единым деревом: у cgroup один путь и все контроллеры сразу. CPU ограничивается через cpu.max = «quota period» (50000 100000 = пол-ядра), память — через memory.max: превышение вызывает OOM-kill внутри cgroup, а не по всему хосту. Плюс PSI-метрики давления и честный учёт памяти.
Подробно:
| v1 | v2 | |
|---|---|---|
| Иерархия | своя на каждый контроллер | единое дерево |
| CPU-лимит | cpu.cfs_quota_us / cfs_period_us | cpu.max = «quota period» |
| Память | limit_in_bytes, кривой учёт page cache | memory.max/high + memory.min/low |
| Давление | — | PSI: cpu/memory/io.pressure |
- memory.high vs memory.max — high мягко тормозит через reclaim, max жёстко убивает; на этом k8s строит memory QoS.
- PSI — доля времени, которую задачи ждали ресурс: сигнал деградации ещё до OOM.
- Кому это важно — JVM и другие cgroup-aware рантаймы читают эти файлы, чтобы видеть лимиты контейнера, а не железо хоста.
⚠️ Частая ошибка: считать OOM в контейнере общесистемным. memory.max запускает OOM-killer локально в cgroup: умирает процесс контейнера, хост продолжает жить.
04CPU контейнера ниже лимита, а латентность скачет. В чём причина?
senior
Короткий ответ: Это CFS-троттлинг. Квота CPU выдаётся на период (по умолчанию 100 мс): многопоточное приложение сжигает всю квоту за первые миллисекунды периода и остаток сидит throttled. Средний usage при этом ниже лимита, а хвосты латентности растут скачками. Диагноз — nr_throttled и throttled_usec в cpu.stat.
Подробно:
- Механика — лимит «2 CPU» = квота 200 мс на период 100 мс; 16 активных потоков съедают её за ~12 мс и ждут следующего периода.
- Симптом — средний CPU 30–50% от лимита, но p99 прыгает с шагом, кратным периоду (~100 мс).
- Диагностика — /sys/fs/cgroup/cpu.stat: nr_throttled, throttled_usec; в k8s — container_cpu_cfs_throttled_periods_total.
- Лечение — поднять или убрать CPU-лимит, ужать параллелизм под квоту (GOMAXPROCS, размер пулов потоков), уменьшить период.
период 100 мс, quota = 20 мс, 8 потоков:
|■■■.......................|■■■.......................|
↑ квота сгорела за ~3 мс ↑ снова ~3 мс работы
throttled ~97 мс throttled ~97 мс
средний CPU низкий, p99 ≈ +100 мс
⚠️ Частая ошибка: смотреть только на средний CPU usage и «раз низкий — лимит ни при чём». Троттлинг виден только в cpu.stat и throttled-метриках — именно при низком среднем usage.
05Как трафик попадает с сетевой карты хоста внутрь контейнера?
middle
Короткий ответ: Контейнер живёт в своём network namespace, а наружу торчит veth-пара: один конец — eth0 внутри контейнера, второй воткнут в bridge на хосте (docker0/cni0). Публикация порта — это DNAT-правило в PREROUTING: пакет на порт хоста переписывается на IP:порт контейнера, а обратные пакеты сопоставляет conntrack.
Подробно:
- netns — собственный сетевой стек: интерфейсы, маршруты, правила iptables, свои 65535 портов.
- veth-пара — виртуальный «патч-корд»: пакет, вошедший в один конец, выходит из другого.
- bridge — L2-коммутатор в ядре: соединяет veth всех контейнеров и держит их подсеть.
- DNAT + conntrack —
-p tcp --dport 8080 -j DNAT --to 172.17.0.2:80; conntrack запоминает соединение и разворачивает адреса у ответных пакетов.
клиент → NIC хоста :8080
→ PREROUTING: DNAT → 172.17.0.2:80
→ bridge docker0 → veth(host) ⇄ veth(eth0 в netns)
→ процесс в контейнере; ответ разворачивает conntrack
⚠️ Частая ошибка: думать, что -p 8080:80 «открывает порт в контейнере». Приложение слушает свой порт всегда; публикация лишь добавляет DNAT на хосте — поэтому с самого хоста и из соседних контейнеров сервис доступен и без -p.
06Распутайте стек: Docker, containerd, runc и CRI — кто за что отвечает?
middle
Короткий ответ: runc — низкоуровневый OCI-рантайм: именно он создаёт неймспейсы и cgroups и запускает процесс, после чего завершается. containerd — демон жизненного цикла: тянет образы, управляет снапшотами и вызывает runc. Docker — CLI и демон с UX поверх containerd. Kubernetes говорит с containerd напрямую по CRI — dockershim удалён в 1.24.
Подробно:
docker CLI ──► dockerd ─┐
kubelet ─── CRI ────────┴► containerd ─► containerd-shim ─► runc ─► процесс
(runc вышел; shim держит контейнер живым)
| Слой | Роль |
|---|---|
| runc | OCI runtime spec: namespaces, cgroups, pivot_root, запуск процесса |
| containerd | образы, снапшоты, lifecycle, gRPC API |
| Docker | сборка по Dockerfile, CLI, volumes/compose — UX |
| CRI | gRPC-интерфейс kubelet ↔ рантайм (containerd, CRI-O) |
- shim — прослойка между containerd и процессом: контейнеры переживают рестарт самого containerd.
- «k8s выпилил Docker» — удалили только прослойку dockershim; сам стек снизу и так был containerd + runc.
⚠️ Частая ошибка: «Kubernetes больше не запускает Docker-образы». Формат образов — OCI-стандарт и не зависит от того, кто их собрал: образ из docker build работает в containerd как раньше.
07Почему приложение в контейнере видит CPU и память всего хоста, и что от этого ломается?
middle
Короткий ответ: /proc не виртуализируется неймспейсами: /proc/cpuinfo и /proc/meminfo показывают хост. nproc на машине с 64 ядрами вернёт 64, даже если лимит контейнера — 1 CPU. Рантаймы сайзят по этим числам пулы потоков и кучи → троттлинг и OOM.
Подробно:
- Что ломается — JVM считает heap как долю «всей» памяти и заводит GC-потоки по 64 ядрам; Go ставит GOMAXPROCS=64 при квоте 2 CPU → CFS-троттлинг; nginx с worker_processes auto плодит воркеры.
- Правильный источник — файлы cgroup: /sys/fs/cgroup/cpu.max и memory.max — это лимиты контейнера, а не железо хоста.
- Фиксы — современная JVM cgroup-aware (UseContainerSupport включён с 10/8u191); в Go — uber-go/automaxprocs или явный GOMAXPROCS; лимиты передавать явно через env и флаги.
- LXCFS — FUSE-подмена /proc значениями из cgroup; применяется в LXC и части платформ.
| Источник | Что показывает |
|---|---|
| /proc/meminfo, nproc | железо хоста |
| /sys/fs/cgroup/* | лимиты контейнера |
⚠️ Частая ошибка: дать контейнеру memory.max=512M и удивляться OOMKilled у JVM: без cgroup-awareness она отсайзила heap от 64 GB хоста.
08Что значит быть PID 1 внутри контейнера и почему там копятся зомби-процессы?
middle
Короткий ответ: Первый процесс pid-неймспейса получает PID 1 и обязанности init: ядро не применяет к нему дефолтные обработчики сигналов (SIGTERM без явного handler игнорируется), и он обязан reap'ить осиротевших детей через wait(). Наивное приложение как PID 1 → docker stop висит 10 с и добивает SIGKILL, а зомби накапливаются.
Подробно:
- Сигналы — kill -TERM для PID 1 молча проглатывается, если приложение не поставило handler → нет graceful shutdown.
- Зомби — осиротевшие процессы репарентятся к PID 1; без waitpid() их записи остаются в таблице процессов и копятся.
- Фиксы — tini (docker run --init), dumb-init либо собственный handler SIGTERM + reaping; в k8s при shared PID namespace роль reaper'а играет pause-контейнер пода.
- Связка с k8s — terminationGracePeriodSeconds работает, только если PID 1 реально обрабатывает SIGTERM; иначе под всегда умирает по SIGKILL.
docker stop → SIGTERM → PID 1 (нет handler'а → игнор)
ждём 10 с … → SIGKILL (соединения оборваны)
с tini: SIGTERM → форвард ребёнку → graceful exit + reap зомби
⚠️ Частая ошибка: shell-форма ENTRYPOINT: PID 1 — это /bin/sh, который не форвардит SIGTERM приложению. Использовать exec-форму или exec в стартовом скрипте.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.