Итоговый теоретический тест
Сорок вопросов по всем разделам курса. Проверяется понимание механизмов, а не запоминание флагов: то, что отвечает --help, здесь не спрашивается.
Проходной результат: 30 из 40.
Время: около часа.
Вес в итоговом балле: 15 %.
Правила
- Ответ формулируется письменно до открытия разбора.
- Ответ «не знаю» лучше угаданного: угаданный верный ответ не попадает в список тем для повторения.
- Справочные материалы не запрещены — вопросы составлены так, что поиск по документации не даёт ответа быстрее понимания.
Разбор устроен по блокам из пяти вопросов: так проще не заглянуть вперёд.
Блок 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 | Не пройдено; вернуться к курсу |
Отметьте не только неверные ответы, но и угаданные — те, где вы не смогли бы объяснить механизм. По ним стоит вернуться в первую очередь: неверный ответ виден, угаданный — нет.
Карта вопросов по разделам
| Блок | Вопросы | Разделы |
|---|---|---|
| 1 | 1–5 | 02, 03, 14 |
| 2 | 6–10 | 04, 05 |
| 3 | 11–15 | 05, 06, 12, 15 |
| 4 | 16–20 | 07, 08 |
| 5 | 21–25 | 09, 11 |
| 6 | 26–30 | 12, 19 |
| 7 | 31–35 | 13, 14, 16, 17 |
| 8 | 36–40 | 18, 19 |
Что этот тест не проверяет
Полезно назвать прямо. Тест проверяет понимание механизмов и не проверяет умения ими пользоваться: написать Dockerfile, найти причину отказа в незнакомой системе, принять решение при неполных данных.
Для этого есть другие инструменты: debugging challenges — расследование, практический экзамен — работа целиком. Сорок верных ответов здесь не означают, что вы сделаете вторую часть.
Навигация
Вернуться к системе проверки
Итоговый практический экзамен
Debugging challenges
Grading rubric
Главное оглавление