Главная/Проверка знаний/Проверка знаний

Итоговый теоретический тест

Сорок вопросов по всем разделам курса. Проверяется понимание механизмов, а не запоминание флагов: то, что отвечает --help, здесь не спрашивается.

Проходной результат: 30 из 40.
Время: около часа.
Вес в итоговом балле: 15 %.

Правила

  1. Ответ формулируется письменно до открытия разбора.
  2. Ответ «не знаю» лучше угаданного: угаданный верный ответ не попадает в список тем для повторения.
  3. Справочные материалы не запрещены — вопросы составлены так, что поиск по документации не даёт ответа быстрее понимания.

Разбор устроен по блокам из пяти вопросов: так проще не заглянуть вперёд.


Блок 1. Основы и образы (вопросы 1–5)

1. Процесс в container'е и процесс на хосте — чем они различаются с точки зрения ядра и чем не различаются?

2. Почему приложение внутри container'а с Ubuntu видит ядро хоста, и что из этого следует для совместимости?

3. Слои образа неизменяемы. Что тогда происходит, когда приложение перезаписывает файл, пришедший из базового образа?

4. У одного образа несколько digest'ов. Какие именно и какой годится для закрепления версии?

5. docker build дважды подряд без изменений даёт разные digest'ы. Назовите две причины.

Разбор блока 1

1. Не различаются ничем существенным: тот же планировщик, та же память, тот же набор инструкций. Различие — в том, что ядро показывает процессу: namespaces ограничивают видимость, cgroups — объём ресурсов, capabilities и seccomp — набор доступных операций. Виртуализации нет.

2. Ядро одно на всех container'ах и на хосте; образ содержит только пользовательскую часть системы. Следствие: дистрибутив внутри может быть любым, но модули ядра, версии системных вызовов и поведение ядра — всегда хостовые. Отсюда же невозможность запустить образ с другим ядром без виртуальной машины.

3. Copy-up: файл копируется из нижнего слоя в записываемый целиком, дальше правится копия. Один изменённый байт в файле на 128 МиБ даёт слой на 128 МиБ. chmod тоже вызывает копирование — меняются метаданные, а копируется файл.

4. Три: digest списка манифестов (index, для многоплатформенных образов), digest манифеста конкретной платформы и digest конфигурации. Для закрепления годится тот, что показывает RepoDigests после push или pull — он адресует то, что лежит в registry.

5. Первая: временные метки. Слои содержат время создания файлов, и оно меняется от сборки к сборке. Вторая: неопределённость содержимого — apt-get install без фиксации версий, pip install без файла блокировки, загрузка по URL без проверки. Обе устраняются, но требуют усилий: воспроизводимая сборка — отдельная работа, а не свойство Dockerfile.


Блок 2. Жизненный цикл и Dockerfile (вопросы 6–10)

6. Почему container завершается вместе с процессом PID 1, а не продолжает работать?

7. Ядро обрабатывает сигналы для PID 1 иначе, чем для обычного процесса. В чём разница и к чему она приводит в container'е?

8. docker stop занимает ровно grace period. Назовите две различные причины и способ различить их одной командой.

9. Что делает EXPOSE и почему это одна из самых часто неверно понимаемых инструкций?

10. Аргументы docker run в одном случае заменяют команду, в другом добавляются к ней. От чего это зависит?

Разбор блока 2

6. Container — это и есть процесс PID 1 вместе с его namespace'ами, cgroup и файловой системой. Нет процесса — нет владельца у namespace'ов, и ядро их освобождает. «Оставить container работать» без процесса невозможно в принципе.

7. Для PID 1 ядро не применяет действия по умолчанию: если обработчик не установлен, сигнал игнорируется. Обычный процесс от SIGTERM умер бы; PID 1 без обработчика — нет. В container'е это означает, что приложение без обработчика SIGTERM доживёт до SIGKILL.

8. Первая: PID 1 — оболочка (shell-форма CMD), она не пересылает сигнал потомку. Вторая: обработчик отсутствует в самом приложении. Различает docker exec container ps -eo pid,ppid,cmd: в первом случае PID 1 — sh, во втором — само приложение.

9. Только документирует предполагаемый порт в метаданных образа. Не публикует его, не открывает, не создаёт ни одного правила. Неверное понимание живуче, потому что слово означает «выставить наружу», а инструкция ничего не выставляет.

10. От того, задан ли ENTRYPOINT. Аргументы docker run заменяют CMD; при заданном ENTRYPOINT они становятся его аргументами, а CMD служит значением по умолчанию. Отсюда идиома: ENTRYPOINT — команда, CMD — умолчания для неё.


Блок 3. Сборка и Python (вопросы 11–15)

11. Что именно делает слой кэша недействительным и почему один недействительный слой обесценивает все последующие?

12. Секрет скопирован в образ и удалён следующей инструкцией. Почему он извлекается и как передать его правильно?

13. Стадия test описана в Dockerfile, но сборка проходит, даже когда тесты сломаны. Почему и что с этим делать?

14. Почему PYTHONUNBUFFERED нужен именно в container'е и не нужен при запуске в терминале?

15. Почему USER задают числом, а не именем, и где именно проявляется разница?

Разбор блока 3

11. Недействительным слой делает изменение самой инструкции либо изменение содержимого копируемых ею файлов. Последующие обесцениваются потому, что слой — это состояние файловой системы после всех предыдущих: если предыдущее состояние другое, результат следующей инструкции тоже может быть другим, и переиспользовать его нельзя.

12. Слои только добавляются. Удаление создаёт в новом слое запись «этого файла здесь нет», а содержимое остаётся в нижнем слое и извлекается из выгруженного образа. Правильно — RUN --mount=type=secret: секрет доступен во время выполнения инструкции и в слой не пишется. Альтернатива — multi-stage, где слой с секретом остаётся в промежуточной стадии.

13. Сборка выполняет только те стадии, от которых зависит целевая. Стадия, на которую нет ссылок, пропускается, и сборка при этом успешна. Решение — вызывать docker build --target test отдельным шагом и убедиться, что сломанный тест даёт ненулевой код.

14. Python буферизует stdout блоками, когда он не является терминалом. В терминале действует построчная буферизация, поэтому проблема не видна. В container'е вывод идёт в канал, строки копятся и появляются пачкой — иногда только при завершении процесса.

15. Имя пользователя разрешается по /etc/passwd внутри образа, снаружи оно неизвестно. Kubernetes с runAsNonRoot: true обязан убедиться, что UID ненулевой; имя он разрешить не может и pod не запускает с ошибкой о нечисловом пользователе.


Блок 4. Хранилище и сеть (вопросы 16–20)

16. Пустой именованный том подключён к каталогу образа. Откуда он берёт владельца и права, и когда это происходит?

17. Почему монтирование каталога поверх пути образа скрывает то, что там было, и куда девается скрытое?

18. Что происходит с сетью при создании container'а — перечислите объекты, которые создаются.

19. Приложение отвечает на docker exec … curl localhost:8000, но не отвечает на curl localhost:8080 с хоста при -p 8080:8000. Что происходит?

20. Чем сеть по умолчанию bridge отличается от пользовательской и почему различие обычно замечают не сразу?

Разбор блока 4

16. Из каталога образа, поверх которого он подключён, — и только при первом подключении пустого тома. Дальше том хранит свои права независимо от образа: пересборка их не изменит, нужно удалить том. Отсюда типичный отказ в правах после смены USER в образе.

17. Монтирование заменяет содержимое точки монтирования на содержимое источника — это свойство самого механизма, а не Docker. Скрытое никуда не девается: файлы остаются в слоях образа и снова видны, если монтирование убрать.

18. Network namespace для container'а; пара виртуальных интерфейсов veth, один конец внутри namespace'а, другой подключён к мосту; адрес из подсети моста; записи в таблице маршрутизации; при публикации порта — правила трансляции адресов.

19. Приложение слушает 127.0.0.1 — петлевой интерфейс самого container'а. Опубликованный порт направляет трафик на адрес container'а в сети Docker, и до петли он не доходит. Изнутри соединение проходит, потому что оно и идёт по петле.

20. В пользовательской сети работает встроенное разрешение имён по имени container'а, в сети по умолчанию — нет. Замечают не сразу, потому что Compose создаёт пользовательскую сеть автоматически: симптом возникает только при ручном docker run.


Блок 5. Compose и эксплуатация (вопросы 21–25)

21. Что именно гарантирует depends_on в форме списка и почему связанная с ним ошибка проявляется через раз?

22. Зачем разделять liveness и readiness, если обе проверяют «работает ли сервис»?

23. Для чего нужна startup-проба, если readiness уже умеет отвечать «не готов»?

24. В чём смысловая разница между requests и limits и почему нельзя обойтись одним?

25. Почему конфигурацию передают окружением, а не кладут в образ, — при том что файл в образе надёжнее и не может потеряться?

Разбор блока 5

21. Гарантирует только запуск container'а, а не готовность процесса внутри. Проявляется через раз потому, что исход зависит от гонки: на прогретой машине база успевает инициализироваться, на холодной — нет. Плавающие ошибки дороже воспроизводимых именно этим.

22. Они отвечают на разные вопросы и вызывают разные действия. Liveness — «жив ли процесс», отказ означает перезапуск. Readiness — «готов ли обслуживать», отказ означает вывод из балансировки. Слитые вместе, они приводят к тому, что отказ зависимости перезапускает все экземпляры разом — а после перезапуска зависимость по-прежнему недоступна.

23. Чтобы не назначать большую начальную задержку остальным пробам. Пока startup не пройдена, liveness не выполняется вовсе; после — проверки идут с обычной частотой. Без неё приходится выбирать между «убиваем медленно стартующее приложение» и «замечаем зависание через минуту».

24. requests — обещание планировщику, влияет на размещение; limits — предел, влияет на поведение под нагрузкой. Только limits означает класс BestEffort и вытеснение в первую очередь; только requests — отсутствие защиты соседей от утечки.

25. Потому что образ должен быть один для всех сред. Конфигурация в образе означает отдельный образ на среду, а значит, в эксплуатацию едет не тот артефакт, который проверяли. Аргумент про надёжность верен для файла, но решается монтированием файла в container, а не встраиванием его в образ.


Блок 6. Безопасность (вопросы 26–30)

26. Почему root внутри container'а по умолчанию равен root на хосте, и что это меняет в модели угроз?

27. Монтирование /var/run/docker.sock часто считают безобидным. Почему это ошибка?

28. Профиль seccomp по умолчанию запрещает несколько десятков системных вызовов из более чем трёхсот. Почему не больше?

29. Что такое container escape и почему такие уязвимости появляются регулярно, а не «однажды и навсегда исправлены»?

30. Container изолирует приложение. Почему он при этом не защищает от вредоносной зависимости в requirements.txt?

Разбор блока 6

26. Docker не создаёт user namespace: преобразования идентификаторов нет, UID 0 внутри — это UID 0 снаружи. Разделяют их namespaces, capabilities и seccomp. В модели угроз это означает, что выход за изоляцию даёт сразу права root на хосте, без промежуточных шагов.

27. Через сокет обращаются к API демона, а демон работает от root. Имея доступ, можно создать container с любыми параметрами — например, смонтировать корень хоста на запись. То есть доступ к «просто файлу» эквивалентен правам root на машине.

28. Потому что приложение обязано работать: подавляющее большинство вызовов нужно обычным программам. Запрещаются заведомо опасные и бесполезные в container'е. Отсюда вывод, который важнее числа: seccomp сокращает площадь атаки, но не превращает container в песочницу для недоверенного кода.

29. Escape — выход процесса за пределы изоляции на хост. Уязвимости повторяются, потому что сложность сосредоточена в местах, где изоляция должна прерываться контролируемым образом: файловые дескрипторы, /proc, setns, пересечение границ namespace'ов. Известные случаи в runc относятся именно к этому. Ожидать «окончательного исправления» интерфейса такой сложности не следует.

30. Потому что зависимость выполняется внутри границы, с правами самого приложения. Container защищает хост от приложения, а не приложение от собственного кода. Против этого работают другие меры: фиксация версий, проверка происхождения, сканирование, минимальные права самого приложения.


Блок 7. Диагностика и доставка (вопросы 31–35)

31. Container исчез, в логах приложения последняя строка обычная. Как узнать причину и почему её нет в логах?

32. docker inspect показывает лимит памяти. Почему этого недостаточно, чтобы утверждать, что лимит действует?

33. docker system df показывает немного, а диск заполнен. Где искать?

34. Почему развёртывание по тегу приводит к тому, что в эксплуатации работает не тот код, — назовите три независимых места расхождения.

35. Что экспортирует cache-to при сборке и что при этом не экспортируется?

Разбор блока 7

31. docker inspect --format '{{.State.ExitCode}} {{.State.OOMKilled}}'; при 137 и true причина — OOM killer. В логах приложения её нет, потому что SIGKILL не перехватывается: процесс не получает возможности что-либо записать. Запись делает ядро — dmesg, docker events.

32. inspect показывает запрошенное при создании, а не применённое. Запрос мог не выполниться: отключён контроллер cgroup, несовместимое ядро, отсутствующая возможность. Проверять нужно показания ядра изнутри: cat /sys/fs/cgroup/memory.max.

33. Логи container'ов: /var/lib/docker/containers/*/*-json.log. В вывод system df они не входят, а при драйвере по умолчанию не ротируются вовсе. Это самая частая причина заполнения диска, которую ищут дольше прочих.

34. Первое: pull выполнен, но container не пересоздан — работает старый образ. Второе: тег перезаписан между pull и запуском. Третье: опубликовано не то, что собрано, — сборка пометила тегом образ до прохождения проверок или два конвейера победили друг друга. Все три устраняются развёртыванием по digest.

35. Экспортируются слои образа. Содержимое RUN --mount=type=cache не экспортируется — это другой механизм, живущий на машине сборки. Отсюда следствие: cache mount ускоряет локальную сборку и в конвейере не работает.


Блок 8. Kubernetes и границы (вопросы 36–40)

36. Kubernetes отказался от dockershim. Почему это не потребовало пересобирать образы?

37. Почему у depends_on нет аналога в Kubernetes и что применяют вместо?

38. Сервис хранит состояние в локальном каталоге. Что именно сломается при масштабировании и почему ReadWriteOnce не решает задачу?

39. Где контейнеризация стоит производительности, а где не стоит почти ничего? Объясните механизм.

40. Значительная часть свойств Docker доступна из systemd на тех же механизмах ядра. Что тогда остаётся уникальным для Docker?

Разбор блока 8

36. Образы соответствуют стандарту OCI, а dockershim был лишь прослойкой между kubelet и container runtime — одним звеном в цепочке из нескольких. Заменили звено; формат образов и сам runtime (containerd) остались теми же.

37. Pod'ы запускаются параллельно по устройству системы: порядок зависит от планировщика, доступности ресурсов и состояния узлов. Гарантировать его значило бы отказаться от параллельного запуска. Вместо порядка предлагается устойчивость: readiness-проба не пускает трафик, а повторы подключения в приложении решают задачу не только при старте, но и при отказе зависимости в работе.

38. Каждая реплика получает своё состояние: счётчики расходятся, загруженный файл доступен только через принявший его экземпляр, сессия теряется при попадании на другой. ReadWriteOnce не решает, а меняет симптом: том подключается к одному узлу, поэтому либо второй pod не стартует (Multi-Attach error), либо оба оказываются на одном узле и два процесса пишут в один файл без блокировок. Первый исход заметен сразу, второй тихий и опаснее; выбор между ними делает планировщик.

39. Почти ничего не стоит на вычислениях и памяти: это обычный процесс ядра, тот же планировщик, те же страницы. Стоит на вводе-выводе и сети: фильтр seccomp проверяет каждый системный вызов; OverlayFS ищет по стеку слоёв при чтении и копирует файл целиком при первой записи; сетевой пакет проходит veth, мост и правила трансляции. Правило оценки: чем больше доля вычислений, тем меньше цена.

40. Упаковка. systemd даёт ограничения ресурсов, изоляцию файловой системы и сети, снятие capabilities, фильтр вызовов — на буквально тех же cgroups, namespaces и seccomp. Чего он не даёт: зависимости, едущие вместе с приложением, одинаковость окружения на разных машинах и единообразие доставки. Отсюда граница применимости: если приложение — один бинарь или скрипт с зависимостями из репозитория дистрибутива, упаковка не нужна.


Подсчёт результата

ВерныхРезультат
36–40Отлично: понимание механизмов, а не рецептов
30–35Проходной результат
24–29Не пройдено; повторить разделы по темам ошибок
менее 24Не пройдено; вернуться к курсу

Отметьте не только неверные ответы, но и угаданные — те, где вы не смогли бы объяснить механизм. По ним стоит вернуться в первую очередь: неверный ответ виден, угаданный — нет.

Карта вопросов по разделам

БлокВопросыРазделы
11–502, 03, 14
26–1004, 05
311–1505, 06, 12, 15
416–2007, 08
521–2509, 11
626–3012, 19
731–3513, 14, 16, 17
836–4018, 19

Что этот тест не проверяет

Полезно назвать прямо. Тест проверяет понимание механизмов и не проверяет умения ими пользоваться: написать Dockerfile, найти причину отказа в незнакомой системе, принять решение при неполных данных.

Для этого есть другие инструменты: debugging challenges — расследование, практический экзамен — работа целиком. Сорок верных ответов здесь не означают, что вы сделаете вторую часть.


Навигация

Вернуться к системе проверки
Итоговый практический экзамен
Debugging challenges
Grading rubric
Главное оглавление

Markdown на GitHub ↗