Перейти к содержанию
Бэкенд и системы

8 вопросов по теме «DevOps: Внутренности контейнеров» на собеседовании

В этом материале — 8 вопросов из русской колоды RecallDeck по теме «DevOps: Внутренности контейнеров». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

7 мин чтения8 подробных ответовПроверено 24 августа 2026
Главная мысль

Стройте ответ вокруг владения состоянием, лимитов, отказа и восстановления. Определение становится инженерным ответом только в конкретном production-сценарии.

Вопросы и ответы

8 подробных ответов

01

Что такое контейнер на уровне ядра Linux и какие механизмы его реализуют?

Короткий ответ: Контейнер — это обычный процесс Linux, которому ядро подсунуло изолированный «вид» системы: неймспейсы прячут чужое, cgroups ограничивают ресурсы, слоёная файловая система даёт свой rootfs. Гостевого ядра нет — ядро одно на всех, и в этом настоящая разница с VM.

Подробно:

  1. Namespaces — изоляция видимости: каждый тип прячет свой кусок системы.
  2. cgroups — лимиты CPU, памяти, IO: сколько процессу можно, а не что он видит.
  3. Слоёный 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 не уменьшает образ?

Короткий ответ: overlayfs склеивает несколько read-only слоёв (lowerdir) и один записываемый (upperdir) в единый merged-вид. Слои образа неизменяемы: rm в позднем шаге не трогает нижний слой, а лишь кладёт whiteout-файл в верхний — файл скрыт из merged, но его байты по-прежнему лежат в образе.

Подробно:

  1. lowerdir — слои образа: только чтение, контент-адресуемы, шарятся между образами и контейнерами.
  2. upperdir — записываемый слой контейнера (или следующего шага сборки).
  3. copy-up — первое изменение файла копирует его целиком из lower в upper; дальше правится копия.
  4. Удаление — 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.

03

cgroups v2: что изменилось по сравнению с v1 и как реально ограничиваются CPU и память?

Короткий ответ: 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
  1. memory.high vs memory.max — high мягко тормозит через reclaim, max жёстко убивает; на этом k8s строит memory QoS.
  2. PSI — доля времени, которую задачи ждали ресурс: сигнал деградации ещё до OOM.
  3. Кому это важно — JVM и другие cgroup-aware рантаймы читают эти файлы, чтобы видеть лимиты контейнера, а не железо хоста.

⚠️ Частая ошибка: считать OOM в контейнере общесистемным. memory.max запускает OOM-killer локально в cgroup: умирает процесс контейнера, хост продолжает жить.

04

CPU контейнера ниже лимита, а латентность скачет. В чём причина?

Короткий ответ: Это CFS-троттлинг. Квота CPU выдаётся на период (по умолчанию 100 мс): многопоточное приложение сжигает всю квоту за первые миллисекунды периода и остаток сидит throttled. Средний usage при этом ниже лимита, а хвосты латентности растут скачками. Диагноз — nr_throttled и throttled_usec в cpu.stat.

Подробно:

  1. Механика — лимит «2 CPU» = квота 200 мс на период 100 мс; 16 активных потоков съедают её за ~12 мс и ждут следующего периода.
  2. Симптом — средний CPU 30–50% от лимита, но p99 прыгает с шагом, кратным периоду (~100 мс).
  3. Диагностика — /sys/fs/cgroup/cpu.stat: nr_throttled, throttled_usec; в k8s — container_cpu_cfs_throttled_periods_total.
  4. Лечение — поднять или убрать CPU-лимит, ужать параллелизм под квоту (GOMAXPROCS, размер пулов потоков), уменьшить период.
период 100 мс, quota = 20 мс, 8 потоков:
|■■■.......................|■■■.......................|
 ↑ квота сгорела за ~3 мс   ↑ снова ~3 мс работы
      throttled ~97 мс           throttled ~97 мс
средний CPU низкий, p99 ≈ +100 мс

⚠️ Частая ошибка: смотреть только на средний CPU usage и «раз низкий — лимит ни при чём». Троттлинг виден только в cpu.stat и throttled-метриках — именно при низком среднем usage.

05

Как трафик попадает с сетевой карты хоста внутрь контейнера?

Короткий ответ: Контейнер живёт в своём network namespace, а наружу торчит veth-пара: один конец — eth0 внутри контейнера, второй воткнут в bridge на хосте (docker0/cni0). Публикация порта — это DNAT-правило в PREROUTING: пакет на порт хоста переписывается на IP:порт контейнера, а обратные пакеты сопоставляет conntrack.

Подробно:

  1. netns — собственный сетевой стек: интерфейсы, маршруты, правила iptables, свои 65535 портов.
  2. veth-пара — виртуальный «патч-корд»: пакет, вошедший в один конец, выходит из другого.
  3. bridge — L2-коммутатор в ядре: соединяет veth всех контейнеров и держит их подсеть.
  4. 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 — кто за что отвечает?

Короткий ответ: 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)
  1. shim — прослойка между containerd и процессом: контейнеры переживают рестарт самого containerd.
  2. «k8s выпилил Docker» — удалили только прослойку dockershim; сам стек снизу и так был containerd + runc.

⚠️ Частая ошибка: «Kubernetes больше не запускает Docker-образы». Формат образов — OCI-стандарт и не зависит от того, кто их собрал: образ из docker build работает в containerd как раньше.

07

Почему приложение в контейнере видит CPU и память всего хоста, и что от этого ломается?

Короткий ответ: /proc не виртуализируется неймспейсами: /proc/cpuinfo и /proc/meminfo показывают хост. nproc на машине с 64 ядрами вернёт 64, даже если лимит контейнера — 1 CPU. Рантаймы сайзят по этим числам пулы потоков и кучи → троттлинг и OOM.

Подробно:

  1. Что ломается — JVM считает heap как долю «всей» памяти и заводит GC-потоки по 64 ядрам; Go ставит GOMAXPROCS=64 при квоте 2 CPU → CFS-троттлинг; nginx с worker_processes auto плодит воркеры.
  2. Правильный источник — файлы cgroup: /sys/fs/cgroup/cpu.max и memory.max — это лимиты контейнера, а не железо хоста.
  3. Фиксы — современная JVM cgroup-aware (UseContainerSupport включён с 10/8u191); в Go — uber-go/automaxprocs или явный GOMAXPROCS; лимиты передавать явно через env и флаги.
  4. LXCFS — FUSE-подмена /proc значениями из cgroup; применяется в LXC и части платформ.
Источник Что показывает
/proc/meminfo, nproc железо хоста
/sys/fs/cgroup/* лимиты контейнера

⚠️ Частая ошибка: дать контейнеру memory.max=512M и удивляться OOMKilled у JVM: без cgroup-awareness она отсайзила heap от 64 GB хоста.

08

Что значит быть PID 1 внутри контейнера и почему там копятся зомби-процессы?

Короткий ответ: Первый процесс pid-неймспейса получает PID 1 и обязанности init: ядро не применяет к нему дефолтные обработчики сигналов (SIGTERM без явного handler игнорируется), и он обязан reap'ить осиротевших детей через wait(). Наивное приложение как PID 1 → docker stop висит 10 с и добивает SIGKILL, а зомби накапливаются.

Подробно:

  1. Сигналы — kill -TERM для PID 1 молча проглатывается, если приложение не поставило handler → нет graceful shutdown.
  2. Зомби — осиротевшие процессы репарентятся к PID 1; без waitpid() их записи остаются в таблице процессов и копятся.
  3. Фиксы — tini (docker run --init), dumb-init либо собственный handler SIGTERM + reaping; в k8s при shared PID namespace роль reaper'а играет pause-контейнер пода.
  4. Связка с 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, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS