Раздел 17. Практические задания
Раздел разбирает механизмы, часть которых доступна только с правами root. Задания построены так, чтобы основная часть выполнялась без привилегий — через API, через показания ядра изнутри container'а, через штатные команды.
Там, где привилегии всё же нужны, задание требует отметить шаг невыполненным, а не обойти его. Это то же правило, что в уроке 16.4: «не проверено» должно отличаться от «проверено и чисто».
Как проверять себя
| Формулировка | Что требуется |
|---|---|
| «показать» | Команда и её фактический вывод |
| «измерить» | Число, полученное командой |
| «сверить» | Два независимых источника, дающих один ответ |
| «отметить» | Явное указание, что шаг не выполнялся, и почему |
Задание 1. Клиент Engine API без CLI
Тип: обязательное
Материал: 17.1
Написать программу, работающую с Docker без установленного CLI.
Требования:
- Обратиться к API через Unix-сокет, получить версию и список container'ов.
- Создать и запустить container двумя запросами; показать
Pidмежду ними. - Прочитать логи, разобрав мультиплексированный поток по кадрам.
- Различить
stdoutиstderrпо первому байту заголовка. - Запустить программу в образе без Docker CLI и без сторонних пакетов.
- Сверить результат с выводом CLI.
Проверка
curl -s --unix-socket /var/run/docker.sock http://localhost/version | python3 -m json.tool | head -4
python3 client.py
docker ps -aq | wc -l
Критерий
Число container'ов совпадает с выводом CLI. Кадры логов разобраны: stdout и stderr различены. Программа работает в образе, где CLI отсутствует.
Ориентир: формат кадра
Восемь байт заголовка перед каждым фрагментом:
байт 0 поток: 1 = stdout, 2 = stderr
байты 1-3 зарезервированы
байты 4-7 длина фрагмента, big-endian
Распаковка: struct.unpack(">BxxxI", data[:8]).
Отрезать первые 8 байт и считать задачу решённой недостаточно: на втором кадре появится второй заголовок.
Задание 2. Цепочка выполнения
Тип: обязательное
Материал: 17.2
Разобрать путь от docker run до процесса приложения.
Требования:
- Найти PID процесса на host и построить цепочку родителей.
- Показать, что процессов
runcнет, и объяснить почему. - Объяснить, кто хранит код выхода, и подтвердить это container'ом, процесса которого уже нет.
- Сверить состояние по трём источникам:
docker inspect, ядро изнутри,config.json. - Собрать OCI bundle вручную и разобрать его структуру.
- Отметить шаги, не выполненные из-за отсутствия прав.
Проверка
pid=$(docker inspect ИМЯ --format '{{.State.Pid}}')
grep PPid /proc/$pid/status
pgrep -c runc; pgrep -c containerd-shim
docker inspect ИМЯ --format '{{.State.ExitCode}} {{.State.Pid}}'
Критерий
pgrep -c runc даёт ноль при работающих container'ах. Код выхода получен у container'а с Pid=0. Два доступных источника состояния согласуются.
Задание 3. Namespaces по inode
Тип: обязательное
Материал: 17.3
Определить, что именно изолирует Docker.
Требования:
- Сравнить все восемь namespaces container'а и host по значению
readlink. - Назвать, какие Docker не создаёт, и объяснить последствие каждого.
- Показать, что означает карта
0 0 4294967295, и подтвердить владельцем файла. - Показать, что
--network container:даёт тот же namespace, а не похожий. - Создать изолированное окружение через
unshareи перечислить, чего в нём нет. - Объяснить, почему
unshare --pidтребует--fork.
Проверка
pid=$(docker inspect ИМЯ --format '{{.State.Pid}}')
for n in mnt uts ipc pid net user cgroup time; do
printf '%-8s host=%s cont=%s\n' "$n" \
"$(readlink /proc/self/ns/$n)" "$(readlink /proc/$pid/ns/$n)"
done
docker exec ИМЯ cat /proc/self/uid_map
Критерий
Изолировано шесть namespaces; user и time совпадают с host. Совпадение inode у двух container'ов с общей сетью показано.
Ориентир для самопроверки
Сравнивать namespaces по поведению — по IP-адресу, по видимости процессов — недостаточно: это даёт вероятностный ответ. Совпадение строки net:[4026532574] означает тождество объекта ядра.
Признак хорошего решения по пункту 5: перечислено не менее пяти механизмов, которых нет в ручном окружении, — cgroups, pivot_root, seccomp, сеть, слои.
Задание 4. Чтение cgroup изнутри
Тип: обязательное
Материал: 17.4
Прочитать ограничения и статистику без прав root.
Требования:
- Найти cgroup container'а обоими способами: по пути на host и изнутри.
- Сопоставить не менее шести флагов
docker runс файлами cgroup. - Показать, что Docker не задаёт
memory.high, и объяснить последствие. - Прочитать
memory.eventsи объяснить сочетаниеmax > 0приoom_kill = 0. - Отличить рост кэша от настоящей нехватки, сравнив
memory.currentиanon. - Измерить throttling двумя показателями:
cpu.statиcpu.pressure.
Проверка
docker exec ИМЯ cat /proc/self/cgroup
docker exec ИМЯ sh -c 'cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/memory.high /sys/fs/cgroup/cpu.max'
docker exec ИМЯ cat /sys/fs/cgroup/memory.events
docker exec ИМЯ grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
Критерий
/proc/self/cgroup содержит 0::/. Соответствие шести флагов показано. Объяснено, что memory.high = max означает убийство вместо замедления.
Задание 5. Copy-up и слои
Тип: обязательное
Материал: 17.5
Измерить стоимость записи в файл образа.
Требования:
- Собрать образ с файлом на 128 МиБ.
- Измерить: запись байта в него,
chmodна нём, создание нового файла того же размера, создание маленького файла. - Показать размер writable layer после записи одного байта.
- Показать, что файл, удалённый инструкцией
RUN rm, остаётся в слое с содержимым. - Объяснить, почему
RUN rmотдельной инструкцией не уменьшает образ. - Определить, какое хранилище образов используется, двумя независимыми признаками.
Проверка
docker run --rm ОБРАЗ sh -c 'time (echo x >> /big.bin)'
docker ps -s
docker diff ИМЯ
docker save ОБРАЗ | tar -t | head
docker info --format '{{.Driver}}'
Критерий
Запись байта заметно дороже создания маленького файла. Writable layer вырос на размер файла. Содержимое удалённого файла прочитано из слоя.
Задание 6. Изолированное окружение вручную
Тип: дополнительное
Материал: 17.3, 17.5
Воспроизвести часть работы Docker без Docker.
Требования:
- Получить корневую файловую систему через
docker export. - Создать namespaces командой
unshareи показать, какие получились. - Собрать overlay из двух нижних слоёв и одного верхнего — или отметить отсутствие прав.
- Показать перекрытие файлов и порядок старшинства слоёв.
- Перечислить, что Docker делает сверх этого, — не менее шести механизмов.
- Объяснить, почему это не делает Docker «тонкой обёрткой».
Проверка
docker export $(docker create alpine:3.21 true) | tar -x -C rootfs
unshare --user --map-root-user --pid --fork --mount --uts id -u
sudo mount -t overlay overlay -o lowerdir=l2:l1,upperdir=u,workdir=w merged
Критерий
Окружение создано, namespaces перечислены. Список отсутствующих механизмов содержит не менее шести пунктов с пояснением каждого.
Задание 7. runc напрямую
Тип: дополнительное
Материал: 17.2
Запустить container без Docker и containerd.
Требования:
- Собрать OCI bundle:
rootfsиconfig.json. - Задать в спецификации: пользователя, пустые capabilities, лимиты памяти и CPU, набор namespaces.
- Сопоставить каждое поле с флагом
docker run. - Запустить через
runc run— или отметить, что инструмент недоступен. - Показать, что после запуска процесса
runcне остаётся. - Объяснить, что это означает для взаимозаменяемости сред выполнения.
Проверка
cd bundle && runc spec
python3 -c "import json; print(json.load(open('config.json'))['linux']['namespaces'])"
sudo runc run demo
pgrep -c runc
Критерий
Bundle собран и валиден как JSON. Соответствие полей флагам показано не менее чем для шести пунктов.
Ориентир: минимальный `config.json`
Обязательны: ociVersion, process (с args, cwd, user), root (с path), mounts для /proc.
Флаги docker run в полях:
| Флаг | Поле |
|---|---|
--user | process.user.uid, gid |
--cap-drop=ALL | process.capabilities.* — пустые списки |
--read-only | root.readonly |
--memory | linux.resources.memory.limit |
--cpus | linux.resources.cpu.quota и period |
--pids-limit | linux.resources.pids.limit |
runc spec создаёт заготовку, которую остаётся отредактировать.
Задание 8. Диагностика: лимит задан, но не работает
Тип: диагностическое
Материал: весь раздел
docker inspect показывает Memory: 268435456, приложение потребляет больше и не убивается. Найти причину.
Требования:
- Назвать пять механизмов, дающих такое расхождение.
- Для каждого предложить проверку.
- Воспроизвести хотя бы один.
- Объяснить, почему
docker inspectнедостаточно для проверки. - Предложить последовательность проверок от дешёвой к дорогой.
Критерий
Названы пять механизмов с проверкой. Один воспроизведён. Объяснено различие «запрошено» и «применено».
Ориентир: пять механизмов
| Механизм | Проверка |
|---|---|
Контроллер memory не включён в родительской cgroup | cat /sys/fs/cgroup/cgroup.subtree_control |
Считается page cache, а не только anon — потребление кажется большим | grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat |
Подкачка не ограничена: --memory без --memory-swap даёт вдвое | cat /sys/fs/cgroup/memory.swap.max |
Измеряется не тот процесс: ps на host показывает RSS, а не cgroup | cat /sys/fs/cgroup/memory.current |
| cgroup v1 на этой машине: другие файлы и другая семантика | stat -fc %T /sys/fs/cgroup |
Порядок проверок от дешёвой к дорогой:
docker exec ИМЯ cat /sys/fs/cgroup/memory.max— применился ли лимит вообщеmemory.stat:anonпротивfile— что именно занятоmemory.events— было ли давлениеmemory.swap.max— не уходит ли в подкачкуcgroup.subtree_controlна host — включён ли контроллер
Почему docker inspect недостаточно: он показывает запрошенное при создании. Показания ядра изнутри container'а отвечают на вопрос, применилось ли оно.
Задание 9. Сверка трёх источников ★
Тип: итоговое
Материал: весь раздел
Написать инструмент, сверяющий состояние container'а по трём независимым источникам.
Требования:
- Источник 1 — Engine API: что запрошено при создании.
- Источник 2 — показания ядра изнутри container'а:
/proc,/sys/fs/cgroup. - Источник 3 —
config.jsonиз bundle containerd, если доступен. - Сверить не менее восьми свойств: пользователь, capabilities, память, CPU, PID, корень только для чтения, namespaces, seccomp.
- Отметить расхождения и объяснить возможную причину каждого.
- Отличить «источник недоступен» от «значения различаются».
- Работать без прав root, используя первые два источника.
Проверка
Инструмент должен:
- давать один результат на согласованном container'е;
- находить расхождение на container'е, запущенном с изменёнными настройками;
- отличать недоступный источник от расхождения;
- возвращать разные коды: согласовано, расхождение, источник недоступен.
Критерий
Восемь свойств сверены. Расхождение воспроизведено намеренно и обнаружено. Недоступность третьего источника отмечена, а не засчитана как согласие.
Ориентир: почему три источника
| Источник | Отвечает на вопрос | Требует root |
|---|---|---|
| Engine API | Что запрошено | Нет |
| Ядро изнутри | Что применилось | Нет |
config.json | Что применил runc | Да |
Расхождение первого и второго означает: запрос не был выполнен. Причины — отключённый контроллер cgroup, несовместимая версия ядра, отсутствующая возможность.
Расхождение второго и третьего означало бы, что состояние изменилось после запуска, — редкий, но возможный случай.
Практическая ценность: инструмент отвечает на вопрос «действительно ли ограничения работают», на который docker inspect в одиночку ответить не может.
Сводная таблица
| № | Задание | Тип | Основной материал |
|---|---|---|---|
| 1 | Клиент Engine API без CLI | обяз. | 17.1 |
| 2 | Цепочка выполнения | обяз. | 17.2 |
| 3 | Namespaces по inode | обяз. | 17.3 |
| 4 | Чтение cgroup изнутри | обяз. | 17.4 |
| 5 | Copy-up и слои | обяз. | 17.5 |
| 6 | Изолированное окружение вручную | доп. | 17.3 |
| 7 | runc напрямую | доп. | 17.2 |
| 8 | Лимит задан, но не работает | диаг. | весь раздел |
| 9 | Сверка трёх источников | ★ | весь раздел |
Что должно получиться
После заданий 1–5 у вас есть практическое подтверждение пяти утверждений раздела:
- CLI — преобразователь аргументов в HTTP-запрос; всё делает daemon.
runcзавершается после настройки; родитель процесса — shim.- Docker создаёт шесть своих namespaces из восьми.
user— хостовый;timeна Docker 29.7.1 не хостовый, но одинаковый у всех container'ов. - Ограничения читаются изнутри container'а без прав root.
- Запись одного байта в файл образа копирует его целиком.
Задания 6–7 воспроизводят часть работы Docker вручную и показывают, чего в ручном варианте нет.
Задание 8 отрабатывает главное различение раздела: запрошено против применено. Задание 9 превращает это различение в инструмент.
Что этот раздел меняет в работе
До него docker inspect был единственным источником истины о container'е. После — понятно, что он показывает намерение, а показания ядра изнутри container'а показывают результат, и эти два ответа могут не совпадать.
Это же объясняет приёмы из предыдущих разделов: почему /sys/fs/cgroup читают изнутри (урок 6.13), почему nsenter -n работает для образов без утилит (урок 13.5), почему секрет извлекается из слоя после RUN rm (урок 12.6).
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий раздел: Docker и Kubernetes →
Главное оглавление