2.6. Container runtime и OCI
Цели
После этого материала вы сможете:
- описать цепочку компонентов от
docker runдо запущенного процесса; - объяснить роль каждого: CLI,
dockerd,containerd, shim,runc; - объяснить, зачем нужен shim и что даёт его существование;
- объяснить, что стандартизируют OCI Image Specification и OCI Runtime Specification;
- найти OCI bundle работающего container и прочитать его
config.json; - объяснить, что означало «Kubernetes отказался от Docker» и почему образы это не затронуло.
Предварительные знания
- 2.1. Процессы и containers;
- 2.3. Linux namespaces, 2.4. Cgroups, 2.5. Capabilities;
- 1.3. Docker daemon — архитектура daemon–client.
Ключевые термины
| Термин | Объяснение |
|---|---|
container runtime | Программа, создающая и запускающая containers |
high-level runtime | Управляет образами, сетью, хранением, жизненным циклом. Пример: containerd |
low-level runtime | Непосредственно создаёт container из готового bundle. Пример: runc |
OCI | Open Container Initiative — организация, разрабатывающая открытые стандарты для containers |
OCI bundle | Каталог с корневой файловой системой и файлом config.json — вход для low-level runtime |
shim | Процесс-посредник между containerd и container, переживающий перезапуск родителя |
CRI | Container Runtime Interface — интерфейс, через который Kubernetes общается с runtime |
Теория
Зачем нужна цепочка из четырёх компонентов
Первый вопрос, который возникает при виде схемы Docker: почему нельзя проще? Ответ — историей и разделением ответственности.
docker (CLI)
│ HTTP через /var/run/docker.sock
▼
dockerd образы, сети, volumes, API, сборка
│ gRPC через /run/containerd/containerd.sock
▼
containerd жизненный цикл containers, распространение образов
│ порождает по одному на container
▼
containerd-shim-runc-v2 удерживает stdio и exit code, переживает перезапуск
│ вызывает однократно
▼
runc создаёт namespaces/cgroups, выполняет execve и завершается
│
▼
процесс приложения
Ответственность распределена так:
| Компонент | Отвечает за |
|---|---|
| CLI | Разбор команд, формирование HTTP-запросов, форматирование вывода |
| dockerd | Сборка образов, управление сетями и volumes, Docker API, оркестрация всего остального |
| containerd | Загрузка и хранение образов, жизненный цикл containers, снапшоттеры |
| shim | Быть родителем процесса container, удерживать его потоки ввода-вывода и код возврата |
| runc | Один вызов: создать namespaces, cgroup, применить capabilities и seccomp, выполнить execve() |
Разделение не декоративное. Оно даёт заменяемость: containerd работает и без Docker (его использует Kubernetes напрямую), runc заменяется на другие реализации (crun, gVisor, Kata Containers) без изменения остальной цепочки.
Зачем нужен shim
Самый неочевидный компонент. Его существование решает три задачи.
1. Containers переживают перезапуск daemon. runc завершается сразу после execve() — он не остаётся родителем. Если бы родителем был containerd, то его перезапуск сделал бы все процессы containers сиротами. Shim — отдельный процесс на каждый container, и он не зависит от жизни containerd.
Именно это делает возможным live-restore из урока 1.3.
2. Удержание stdio. Потоки ввода-вывода container должны существовать, даже когда никто к ним не подключён. Shim держит их открытыми, буферизует и отдаёт по запросу.
3. Сбор exit code. Когда процесс container завершается, кто-то должен вызвать wait() и получить код возврата. Это обязанность родителя — то есть shim. Он сообщает код containerd, и тот сохраняет его в состоянии container.
Проверить структуру можно на реальном дереве процессов — см. раздел «Команды и примеры».
Что стандартизирует OCI
Open Container Initiative создана в 2015 году при участии Docker. Она разрабатывает три спецификации.
OCI Image Specification описывает формат образа: manifest, config, слои, index для multi-platform образов. Благодаря ей образ, собранный Docker, запускается в Podman, containerd, CRI-O и Kubernetes без конвертации.
OCI Runtime Specification описывает формат OCI bundle и жизненный цикл container. Bundle — это каталог из двух вещей:
bundle/
├── config.json полное описание: namespaces, cgroups, capabilities, mounts, процесс
└── rootfs/ корневая файловая система
Low-level runtime получает такой каталог и создаёт по нему container. Именно эта спецификация делает runc заменяемым.
OCI Distribution Specification описывает протокол обмена образами с registry — то, как работают docker push и docker pull. Разбирается в разделе 14.
Практический смысл всех трёх: инструменты сборки, хранения и запуска развязаны. Можно собрать образ в CI через BuildKit, положить в GitLab Registry, а запустить в Kubernetes на containerd — ни один шаг не знает о существовании остальных.
История «Kubernetes отказался от Docker»
Эту новость 2020 года часто понимают неверно.
Kubernetes общается с container runtime через интерфейс CRI. Docker появился раньше CRI и его не реализует. Чтобы Kubernetes мог работать с Docker, существовал компонент-переходник dockershim внутри самого Kubernetes.
Поддержка dockershim требовала усилий ради одного частного случая, притом что Docker внутри всё равно вызывал containerd, который CRI реализует напрямую. В версии 1.24 dockershim удалили: Kubernetes стал обращаться к containerd напрямую, минуя dockerd.
Что это означало на практике:
| Утверждение | Верно? |
|---|---|
| Образы Docker перестали работать в Kubernetes | Нет. Образы соответствуют OCI Image Specification, формат не менялся |
| Нужно пересобирать образы | Нет |
| Docker больше нельзя использовать для разработки | Нет. Сборка и локальный запуск не затронуты |
| Из цепочки на узлах Kubernetes убрали один слой | Да. Это единственное фактическое изменение |
Вывод, полезный за пределами этой истории: Docker — это инструмент разработчика поверх стандартизированного стека. Стандарт (OCI) обеспечивает переносимость независимо от того, какой инструмент запускает container.
Внутренний механизм
Полная последовательность docker run
- CLI разбирает команду и формирует
POST /containers/create. - Запрос идёт в
/var/run/docker.sock. dockerdпроверяет наличие образа; при необходимости обращается к registry.dockerdсоздаёт запись о container и готовит конфигурацию.- CLI отправляет
POST /containers/<id>/start. dockerdчерез gRPC проситcontainerdсоздать задачу.containerdчерез снапшоттер готовит корневую файловую систему: слои образа плюс writable layer.containerdформирует OCI bundle:config.jsonи смонтированныйrootfs.containerdпорождаетcontainerd-shim-runc-v2.- Shim вызывает
runc create, передавая путь к bundle. runcсоздаёт namespaces, cgroup, применяет capabilities, seccomp, AppArmor.- Shim вызывает
runc start; выполняетсяexecve()— процесс становится приложением. runcзавершается. Родителем процесса остаётся shim.- Приложение работает. Shim удерживает stdio и ждёт завершения.
- При завершении shim собирает exit code и сообщает
containerd, тот —dockerd.
Шаг 13 — ключевой для понимания: runc не остаётся в памяти. Он делает свою работу и уходит. В ps вы его не увидите.
Что внутри config.json
Это полное описание container в декларативном виде. Основные разделы:
| Раздел | Содержимое |
|---|---|
process | Команда, аргументы, переменные окружения, рабочий каталог, UID/GID, capabilities |
root | Путь к rootfs и признак read-only |
mounts | Все монтирования: /proc, /sys, /dev, volumes, bind mounts |
linux.namespaces | Список создаваемых namespaces |
linux.resources | Лимиты cgroup |
linux.seccomp | Профиль seccomp: разрешённые системные вызовы |
linux.maskedPaths | Пути, скрытые от container (например, /proc/kcore) |
linux.readonlyPaths | Пути, доступные только для чтения |
Всё, что изучалось в уроках 2.3–2.5, сходится в этом файле: namespaces, cgroups, capabilities, seccomp — в одном декларативном описании.
Команды и примеры
Дерево процессов
docker run -d --name rt-demo alpine sleep 3600
CPID="$(docker inspect rt-demo --format '{{.State.Pid}}')"
pstree -sp "$CPID"
systemd(1)───containerd-shim(188082)───sleep(188104)
Обратите внимание: родитель — shim, не dockerd и не containerd. Их в цепочке нет.
Посмотрим полную командную строку shim:
PPID="$(awk '{print $4}' /proc/"$CPID"/stat)"
tr '\0' ' ' < /proc/"$PPID"/cmdline; echo
/usr/bin/containerd-shim-runc-v2 -namespace moby -id c8f4a1b2d3e5... -address /run/containerd/containerd.sock
В аргументах видны: пространство имён containerd (moby — историческое имя проекта Docker), ID container и адрес сокета containerd.
Убедимся, что runc не работает:
pgrep -a runc || echo "runc не запущен — он завершился после создания container"
runc не запущен — он завершился после создания container
Все компоненты:
pgrep -a 'dockerd|containerd' | grep -v shim
1842 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
1653 /usr/bin/containerd
Два постоянных процесса плюс по одному shim на container.
Проверка независимости от daemon
docker info --format 'LiveRestore: {{.LiveRestoreEnabled}}'
Если включён live-restore:
sudo systemctl restart docker
sleep 3
docker ps --filter name=rt-demo --format '{{.Names}} {{.Status}}'
ps -o pid,ppid,comm -p "$CPID"
rt-demo Up 2 minutes
PID PPID COMMAND
188104 188082 sleep
Container продолжил работу, PID не изменился. Механизм: его родителем является shim, а shim перезапуск dockerd не затрагивает.
Обращение к containerd напрямую
Утилита ctr — низкоуровневый клиент containerd, поставляется вместе с ним:
sudo ctr --namespace moby containers list | head -3
CONTAINER IMAGE RUNTIME
c8f4a1b2d3e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1 - io.containerd.runc.v2
sudo ctr --namespace moby tasks list | head -3
TASK PID STATUS
c8f4a1b2d3e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1 188104 RUNNING
Виден тот же PID, что мы получили через docker inspect. Это подтверждает: containerd управляет тем же процессом, о котором знает Docker.
Пространство имён moby изолирует объекты Docker от других пользователей containerd — например, от Kubernetes, который использует пространство k8s.io.
Просмотр OCI bundle
CID="$(docker inspect rt-demo --format '{{.Id}}')"
sudo ctr --namespace moby containers info "$CID" | head -40
Полная спецификация в формате config.json доступна так:
sudo ctr --namespace moby containers info "$CID" \
| python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin)['Spec'], indent=2))" \
| head -60
Извлечём конкретные разделы. Namespaces:
sudo ctr --namespace moby containers info "$CID" \
| python3 -c "
import json, sys
spec = json.load(sys.stdin)['Spec']
for ns in spec['linux']['namespaces']:
print(ns['type'])
"
mount
network
uts
pid
ipc
cgroup
Ровно шесть namespaces, которые мы наблюдали в уроке 2.3 — теперь видно, откуда они берутся: из декларативной спецификации.
Capabilities:
sudo ctr --namespace moby containers info "$CID" \
| python3 -c "
import json, sys
spec = json.load(sys.stdin)['Spec']
caps = spec['process']['capabilities']['bounding']
print(f'Всего: {len(caps)}')
for c in caps: print(' ', c)
"
Всего: 14
CAP_CHOWN
CAP_DAC_OVERRIDE
CAP_FSETID
CAP_FOWNER
CAP_MKNOD
CAP_NET_RAW
CAP_SETGID
CAP_SETUID
CAP_SETFCAP
CAP_SETPCAP
CAP_NET_BIND_SERVICE
CAP_SYS_CHROOT
CAP_KILL
CAP_AUDIT_WRITE
Те самые 14 capabilities из урока 2.5, теперь — в виде исходной конфигурации, а не наблюдаемого следствия.
Скрытые пути:
sudo ctr --namespace moby containers info "$CID" \
| python3 -c "
import json, sys
spec = json.load(sys.stdin)['Spec']
for p in spec['linux']['maskedPaths'][:8]: print(p)
"
/proc/asound
/proc/acpi
/proc/interrupts
/proc/kcore
/proc/keys
/proc/latency_stats
/proc/timer_list
/proc/scsi
Эти пути скрыты, чтобы container не получил сведения об оборудовании и памяти host. /proc/kcore особенно важен: это образ физической памяти системы.
Уборка
docker rm -f rt-demo
Практическое упражнение
Задание. Проследите путь от команды до процесса и оформите отчёт.
Для запущенного container определите:
- PID процесса приложения на host.
- PID и полную командную строку его родителя (shim).
- PID
dockerdиcontainerd. - Присутствует ли
runcв списке процессов и почему. - Список namespaces из OCI-спецификации.
- Число capabilities в bounding set из спецификации.
- Совпадает ли PID, известный containerd, с PID, известным Docker.
Подсказки
Подсказка 1
PPID процесса лежит в четвёртом поле /proc/<pid>/stat:
awk '{print $4}' /proc/<pid>/stat
Подсказка 2
ctr требует указания пространства имён. Для Docker это moby:
sudo ctr --namespace moby tasks list
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/usr/bin/env bash
# runtime-trace.sh — цепочка компонентов от Docker до процесса.
set -euo pipefail
NAME="rt-trace-$$"
trap 'docker rm -f "$NAME" >/dev/null 2>&1 || true' EXIT
docker run -d --name "$NAME" alpine sleep 300 > /dev/null
CID="$(docker inspect "$NAME" --format '{{.Id}}')"
CPID="$(docker inspect "$NAME" --format '{{.State.Pid}}')"
echo "=== 1. Процесс приложения ==="
printf ' PID: %s\n' "$CPID"
printf ' Команда: %s\n' "$(tr '\0' ' ' < "/proc/$CPID/cmdline")"
echo
echo "=== 2. Родитель (shim) ==="
SPID="$(awk '{print $4}' "/proc/$CPID/stat")"
printf ' PID: %s\n' "$SPID"
printf ' Команда: %s\n' "$(sudo tr '\0' ' ' < "/proc/$SPID/cmdline")"
echo
echo "=== 3. Постоянные процессы ==="
printf ' dockerd: PID %s\n' "$(pgrep -x dockerd | head -1)"
printf ' containerd: PID %s\n' "$(pgrep -x containerd | head -1)"
echo
echo "=== 4. runc ==="
if pgrep -x runc > /dev/null; then
echo " запущен (необычно — обычно завершается сразу)"
else
echo " не запущен: выполнил execve() и завершился, оставив процесс shim"
fi
echo
echo "=== 5. Namespaces из OCI-спецификации ==="
sudo ctr --namespace moby containers info "$CID" 2>/dev/null \
| python3 -c "
import json,sys
spec=json.load(sys.stdin)['Spec']
for ns in spec['linux']['namespaces']: print(' ', ns['type'])
" || echo " (ctr недоступен)"
echo
echo "=== 6. Capabilities из OCI-спецификации ==="
sudo ctr --namespace moby containers info "$CID" 2>/dev/null \
| python3 -c "
import json,sys
spec=json.load(sys.stdin)['Spec']
print(' bounding set:', len(spec['process']['capabilities']['bounding']), 'шт.')
" || echo " (ctr недоступен)"
echo
echo "=== 7. Сверка PID ==="
CTR_PID="$(sudo ctr --namespace moby tasks list 2>/dev/null | awk -v id="$CID" '$1==id {print $2}')"
printf ' Docker: %s\n' "$CPID"
printf ' containerd: %s\n' "${CTR_PID:-недоступно}"
if [ "${CTR_PID:-}" = "$CPID" ]; then
echo " [+] совпадают — это один процесс"
fi
echo
echo "Цепочка: docker CLI → dockerd → containerd → shim → (runc) → sleep"
Ожидаемый вывод:
=== 1. Процесс приложения ===
PID: 188104
Команда: sleep 300
=== 2. Родитель (shim) ===
PID: 188082
Команда: /usr/bin/containerd-shim-runc-v2 -namespace moby -id c8f4a1... -address /run/containerd/containerd.sock
=== 3. Постоянные процессы ===
dockerd: PID 1842
containerd: PID 1653
=== 4. runc ===
не запущен: выполнил execve() и завершился, оставив процесс shim
=== 5. Namespaces из OCI-спецификации ===
mount
network
uts
pid
ipc
cgroup
=== 6. Capabilities из OCI-спецификации ===
bounding set: 14 шт.
=== 7. Сверка PID ===
Docker: 188104
containerd: 188082
[+] совпадают — это один процесс
Цепочка: docker CLI → dockerd → containerd → shim → (runc) → sleep
Проверка результата
docker run -d --name check alpine sleep 60
CPID=$(docker inspect check --format '{{.State.Pid}}')
pstree -sp "$CPID"
docker rm -f check
В выводе должен присутствовать containerd-shim как родитель процесса. Если вы это видите и можете объяснить, почему там нет dockerd и runc, — материал усвоен.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
Ожидание увидеть runc в ps | Кажется, что раз он запускает container, то и работает вместе с ним | runc завершается после execve(); родителем остаётся shim |
Мнение, что dockerd — родитель процессов containers | Логично по названию | Родитель — shim; это и делает возможным live-restore |
| «Kubernetes отказался от Docker, образы надо пересобирать» | Смешаны формат образа и runtime | Формат стандартизирован OCI; изменилась только цепочка на узлах |
Использование ctr без указания namespace | По умолчанию используется default, а Docker работает в moby | Всегда указывать --namespace moby |
Управление containers Docker через ctr | Docker хранит собственное состояние; изменения мимо него рассогласуют его | ctr — только для чтения и диагностики |
| Мнение, что containerd — часть Docker | Исторически так и было; сейчас это независимый проект | containerd используется и без Docker: Kubernetes обращается к нему напрямую |
Контрольные вопросы
На понимание:
- Почему в цепочке четыре компонента, а не один?
- Зачем нужен shim и что сломается без него?
- Почему
runcне виден в списке процессов? - Что стандартизирует OCI Runtime Specification и что это даёт на практике?
- Почему отказ Kubernetes от
dockershimне потребовал пересборки образов?
На применение:
- Как найти родителя процесса container и убедиться, что это shim?
- Как посмотреть OCI-спецификацию работающего container?
- Как проверить, что containerd знает о том же процессе, что и Docker?
На диагностику:
- После
systemctl restart dockerвсе containers остановились. Какая настройка это объясняет? - Нужно узнать, какой профиль seccomp применён к работающему container. Где это посмотреть?
Краткое резюме
- Цепочка: CLI →
dockerd→containerd→ shim →runc→ процесс приложения. - Ответственность разделена: сборка и API у
dockerd, жизненный цикл уcontainerd, создание container уrunc. - Shim — отдельный процесс на каждый container; он удерживает stdio, собирает exit code и переживает перезапуск daemon.
runcзавершается сразу послеexecve()и не остаётся в памяти.- OCI стандартизирует формат образа, формат bundle и протокол обмена с registry.
- OCI bundle — это
config.jsonплюсrootfs. - В
config.jsonсходятся namespaces, cgroups, capabilities, seccomp и маскированные пути. - Стандартизация развязывает инструменты сборки, хранения и запуска.
- Kubernetes обращается к containerd напрямую через CRI;
dockershimудалён в версии 1.24. - Docker — инструмент разработчика поверх стандартизированного стека; переносимость обеспечивает OCI, а не Docker.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker Engine overview | https://docs.docker.com/engine/ | Архитектура daemon–client, роль dockerd |
| OCI Runtime Specification | https://github.com/opencontainers/runtime-spec | Формат OCI bundle, структура config.json, жизненный цикл container |
| OCI Image Specification | https://github.com/opencontainers/image-spec | Формат образа: manifest, config, слои, index |
| OCI Distribution Specification | https://github.com/opencontainers/distribution-spec | Протокол обмена образами с registry |
| containerd documentation | https://containerd.io/docs/ | Роль containerd, пространства имён, снапшоттеры |
| runc | https://github.com/opencontainers/runc | Реализация OCI Runtime Specification |
| Don't Panic: Kubernetes and Docker | https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/ | Удаление dockershim, сохранение совместимости образов |
| Kubernetes: Container Runtimes | https://kubernetes.io/docs/setup/production-environment/container-runtimes/ | CRI и поддерживаемые runtime |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Терминология Docker
Главное оглавление