17.2. containerd, shim и runc
Цели
После этого материала вы сможете:
- назвать роль каждого из четырёх компонентов и объяснить, зачем нужен каждый;
- объяснить, почему перезапуск
dockerdне убивает работающие container'ы; - объяснить, кто хранит код выхода процесса, если daemon был перезапущен;
- найти OCI bundle работающего container'а и прочитать его
config.json; - сопоставить настройки
docker runс полями спецификации OCI; - запустить container через
runcнапрямую, без Docker.
Предварительные знания
Ключевые термины
| Термин | Объяснение |
|---|---|
containerd | Менеджер жизненного цикла container'ов |
shim | Процесс-посредник между containerd и container'ом |
runc | Исполнитель спецификации OCI Runtime |
OCI bundle | Каталог с config.json и корневой файловой системой |
config.json | Полное описание container'а по спецификации OCI |
Теория
Четыре компонента и их роли
docker CLI
│ HTTP через /var/run/docker.sock
▼
dockerd сборка образов, сети, тома, API
│ gRPC через /run/containerd/containerd.sock
▼
containerd жизненный цикл, образы, снимки
│ порождает
▼
containerd-shim-runc-v2 удерживает stdio и код выхода
│ вызывает
▼
runc создаёт namespaces и cgroups, запускает процесс
│
▼
процесс приложения
| Компонент | Отвечает за | Живёт |
|---|---|---|
dockerd | API, сборка, сети, тома | Постоянно |
containerd | Жизненный цикл, образы, снимки | Постоянно |
shim | stdio, код выхода, связь с container'ом | Пока живёт container |
runc | Создание namespaces, cgroups, запуск | Секунды |
Последняя строка — ключевая. runc не остаётся после запуска: он создаёт окружение, запускает процесс и завершается. Родителем процесса приложения становится shim.
Проверить: ps aux | grep runc на машине с работающими container'ами не покажет ничего.
Зачем нужен shim
Три задачи, каждая объясняет его существование:
Удерживает stdio. Потоки stdout и stderr container'а — это концы каналов, другой конец держит shim. Он передаёт их logging driver (урок 13.1).
Хранит код выхода. Когда процесс завершается, ядро отдаёт код его родителю — то есть shim. Тот сохраняет его и отдаёт containerd, когда спросят.
Позволяет перезапустить containerd и dockerd. Container'ы остаются работать: их родитель — shim, а не daemon.
Без shim перезапуск dockerd означал бы гибель всех container'ов, а код выхода терялся бы при любом сбое daemon.
Что происходит при перезапуске daemon
до перезапуска: dockerd → containerd → shim → приложение
↑
родитель процесса
перезапуск dockerd: ✗ dockerd
containerd → shim → приложение (работают)
перезапуск containerd: ✗ containerd
shim → приложение (работает)
после запуска: dockerd → containerd ← shim подключается обратно
Shim переживает перезапуск обоих. После восстановления containerd находит работающие shim по сокетам в своём каталоге состояния и подключается к ним заново.
Именно поэтому live-restore в daemon.json работает: он лишь разрешает не останавливать container'ы при остановке daemon — сама возможность обеспечена архитектурой.
OCI Runtime Specification
runc не знает о Docker. Он принимает bundle — каталог из двух вещей:
bundle/
├── config.json полное описание: процесс, namespaces, cgroups, монтирования
└── rootfs/ корневая файловая система
Всё, что задаётся флагами docker run, попадает в config.json:
Флаг docker run | Поле config.json |
|---|---|
--user | process.user.uid, process.user.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 |
-v, --tmpfs | mounts[] |
--network | linux.namespaces[] типа network |
--security-opt seccomp | linux.seccomp |
--security-opt no-new-privileges | process.noNewPrivileges |
Отсюда практический приём: чтобы проверить, что именно применилось, читают config.json — он не врёт и не упрощает.
Где найти bundle
# Каталог состояния containerd для пространства имён Docker
/run/containerd/io.containerd.runtime.v2.task/moby/<container-id>/
Внутри — config.json, rootfs (точка монтирования) и сокет для связи с shim.
Каталог существует, пока container работает. После остановки он исчезает.
Альтернативный способ увидеть спецификацию — команда runc state, но она требует указания корня состояния:
sudo runc --root /run/containerd/runc/moby list
sudo runc --root /run/containerd/runc/moby state <id>
ctr и nerdctl
containerd имеет собственный клиент, минуя Docker:
| Инструмент | Уровень | Назначение |
|---|---|---|
docker | Высокий | Полный набор возможностей Docker |
nerdctl | Высокий | Совместим с командами Docker, работает с containerd |
ctr | Низкий | Отладка containerd; удобства не предполагается |
ctr работает в пространствах имён containerd. Container'ы Docker находятся в пространстве moby:
sudo ctr --namespace moby containers list
sudo ctr --namespace moby tasks list
Различие containers и tasks отражает то же разделение, что create и start в API (урок 17.1): container — описание, task — работающий процесс.
Внутренний механизм
Почему runc завершается
runc create выполняет последовательность:
- Читает
config.json. - Создаёт namespaces вызовом
cloneс нужными флагами. - Настраивает cgroups.
- Настраивает монтирования,
pivot_root, capabilities, seccomp. - Запускает
init-процесс container'а, который ждёт сигнала на старт. - Завершается.
runc start посылает сигнал ждущему процессу, и тот выполняет execve целевой команды.
Отсюда: runc — не «среда выполнения» в смысле постоянно работающего процесса, а программа настройки. После неё остаётся процесс приложения с уже применёнными ограничениями.
Как shim узнаёт код выхода
Shim становится родителем процесса container'а. Когда тот завершается, ядро посылает shim сигнал SIGCHLD, и вызов waitpid возвращает код.
Shim сохраняет его и держит, пока containerd не заберёт. Если containerd в этот момент перезапускается, код не теряется — shim жив.
Отсюда: docker inspect показывает ExitCode даже для container'а, завершившегося во время перезапуска daemon.
Команды и примеры
Дерево процессов
mkdir -p /tmp/runtime && cd /tmp/runtime
docker run -d --name rt-demo --memory 256m --cpus 0.5 \
--cap-drop=ALL --read-only --tmpfs /tmp \
--user 10001:10001 \
alpine:3.21 sh -c 'echo запущен; sleep 300' > /dev/null
sleep 2
echo "═══ PID процесса на host ═══"
pid="$(docker inspect rt-demo --format '{{.State.Pid}}')"
printf ' PID приложения: %s\n' "$pid"
echo "═══ цепочка родителей ═══"
current="$pid"
for _ in $(seq 1 6); do
[ "$current" = "0" ] || [ "$current" = "1" ] && break
if [ -r "/proc/$current/stat" ]; then
name="$(tr -d '\0' < "/proc/$current/comm" 2>/dev/null || echo '?')"
parent="$(awk '{print $4}' "/proc/$current/stat" 2>/dev/null || echo 0)"
printf ' PID %-8s %s\n' "$current" "$name"
current="$parent"
else
printf ' PID %-8s (нет доступа к /proc)\n' "$current"
break
fi
done
echo "═══ работает ли runc ═══"
runc_count="$(pgrep -c runc 2>/dev/null || echo 0)"
printf ' процессов runc: %s\n' "$runc_count"
echo "═══ работают ли shim ═══"
shim_count="$(pgrep -c 'containerd-shim' 2>/dev/null || echo 0)"
printf ' процессов containerd-shim: %s\n' "$shim_count"
echo "═══ вывод ═══"
cat <<'TXT'
runc НЕ работает: он создал окружение и завершился.
Родитель процесса приложения — shim, а не dockerd и не containerd.
Отсюда: перезапуск dockerd и containerd не убивает container'ы,
а код выхода хранится в shim и не теряется при их перезапуске.
TXT
Ожидаемый вывод:
═══ PID процесса на host ═══
PID приложения: 53102
═══ цепочка родителей ═══
PID 53102 sh
PID 53081 containerd-shim
PID 1 systemd
═══ работает ли runc ═══
процессов runc: 0
═══ работают ли shim ═══
процессов containerd-shim: 3
═══ вывод ═══
runc НЕ работает: он создал окружение и завершился.
Родитель процесса приложения — shim, а не dockerd и не containerd.
...
Цепочка обрывается на systemd: родитель shim — не containerd. Shim намеренно отвязан, чтобы пережить перезапуск containerd.
Ноль процессов runc — прямое подтверждение: это программа настройки, а не среда выполнения.
OCI bundle и config.json
cd /tmp/runtime
cid="$(docker inspect rt-demo --format '{{.Id}}')"
bundle="/run/containerd/io.containerd.runtime.v2.task/moby/$cid"
echo "═══ каталог bundle ═══"
if sudo test -d "$bundle" 2>/dev/null; then
sudo ls -1 "$bundle" | sed 's/^/ /'
have_bundle=1
else
echo " $bundle"
echo " (чтение требует прав root — доступ не получен)"
have_bundle=0
fi
echo "═══ config.json: как флаги превратились в спецификацию ═══"
if [ "$have_bundle" = "1" ] && sudo test -r "$bundle/config.json"; then
sudo cat "$bundle/config.json" > spec.json
python3 - <<'PY'
import json
from pathlib import Path
spec = json.loads(Path("spec.json").read_text())
proc = spec.get("process", {})
linux = spec.get("linux", {})
res = linux.get("resources", {})
print(f" {'флаг docker run':<28} {'поле config.json':<44} значение")
print(" " + "─" * 100)
rows = [
("--user 10001:10001", "process.user.uid / gid",
f"{proc.get('user', {}).get('uid')} / {proc.get('user', {}).get('gid')}"),
("--read-only", "root.readonly", str(spec.get("root", {}).get("readonly"))),
("--memory 256m", "linux.resources.memory.limit",
str(res.get("memory", {}).get("limit"))),
("--cpus 0.5", "linux.resources.cpu.quota / period",
f"{res.get('cpu', {}).get('quota')} / {res.get('cpu', {}).get('period')}"),
("--cap-drop=ALL", "process.capabilities.effective",
str(len(proc.get("capabilities", {}).get("effective", [])))),
("(по умолчанию)", "process.noNewPrivileges",
str(proc.get("noNewPrivileges"))),
]
for flag, field, value in rows:
print(f" {flag:<28} {field:<44} {value}")
print()
ns = [n.get("type") for n in linux.get("namespaces", [])]
print(f" namespaces ({len(ns)}): {', '.join(ns)}")
mounts = spec.get("mounts", [])
print(f" монтирований: {len(mounts)}")
for m in mounts[:4]:
print(f" {m.get('type', '?'):<10} → {m.get('destination')}")
seccomp = linux.get("seccomp")
if seccomp:
print(f" seccomp: действие по умолчанию {seccomp.get('defaultAction')}, "
f"правил {len(seccomp.get('syscalls', []))}")
PY
else
cat <<'TXT'
(нет доступа к config.json)
При наличии прав root было бы видно соответствие:
флаг docker run поле config.json
─────────────────────────────────────────────────────────────
--user 10001:10001 process.user.uid / gid
--read-only root.readonly
--memory 256m linux.resources.memory.limit
--cpus 0.5 linux.resources.cpu.quota / period
--cap-drop=ALL process.capabilities.effective (пусто)
(по умолчанию) process.noNewPrivileges
config.json содержит ПОЛНОЕ описание: namespaces, монтирования,
профиль seccomp, ограничения cgroups. Он не упрощает и не врёт —
это то, что фактически применил runc.
TXT
fi
Ожидаемый вывод:
═══ каталог bundle ═══
/run/containerd/io.containerd.runtime.v2.task/moby/9f1c4e8a...
(чтение требует прав root — доступ не получен)
═══ config.json: как флаги превратились в спецификацию ═══
(нет доступа к config.json)
При наличии прав root было бы видно соответствие:
флаг docker run поле config.json
─────────────────────────────────────────────────────────────
--user 10001:10001 process.user.uid / gid
--read-only root.readonly
--memory 256m linux.resources.memory.limit
--cpus 0.5 linux.resources.cpu.quota / period
--cap-drop=ALL process.capabilities.effective (пусто)
(по умолчанию) process.noNewPrivileges
config.json содержит ПОЛНОЕ описание: namespaces, монтирования,
профиль seccomp, ограничения cgroups. Он не упрощает и не врёт —
это то, что фактически применил runc.
То же без прав root: через API
cd /tmp/runtime
echo "═══ что можно проверить без доступа к bundle ═══"
docker inspect rt-demo --format '
User: {{.Config.User}}
ReadonlyRootfs: {{.HostConfig.ReadonlyRootfs}}
Memory: {{.HostConfig.Memory}}
NanoCpus: {{.HostConfig.NanoCpus}}
CapDrop: {{json .HostConfig.CapDrop}}
PidsLimit: {{.HostConfig.PidsLimit}}'
echo "═══ и то же изнутри container'а ═══"
docker exec rt-demo sh -c '
printf " uid=%s\n" "$(id -u)"
printf " CapEff=%s\n" "$(grep CapEff /proc/self/status | awk "{print \$2}")"
printf " корень на запись: %s\n" \
"$(touch /probe 2>/dev/null && echo да || echo нет)"
printf " memory.max=%s\n" "$(cat /sys/fs/cgroup/memory.max 2>/dev/null)"
printf " cpu.max=%s\n" "$(cat /sys/fs/cgroup/cpu.max 2>/dev/null)"
' 2>/dev/null
echo "═══ три источника, одно состояние ═══"
cat <<'TXT'
config.json что runc ПРИМЕНИЛ (требует root)
docker inspect что dockerd ЗАПРОСИЛ
/proc и /sys/fs/cgroup что ядро ПОКАЗЫВАЕТ изнутри
Третий источник доступен без привилегий и отвечает
на главный вопрос: применилось ли на самом деле.
Расхождение между вторым и третьим означает, что запрос
не был выполнен — например, лимит не применился из-за
отсутствия контроллера cgroup.
TXT
Ожидаемый вывод:
═══ что можно проверить без доступа к bundle ═══
User: 10001:10001
ReadonlyRootfs: true
Memory: 268435456
NanoCpus: 500000000
CapDrop: ["ALL"]
PidsLimit: 0
═══ и то же изнутри container'а ═══
uid=10001
CapEff=0000000000000000
корень на запись: нет
memory.max=268435456
cpu.max=50000 100000
═══ три источника, одно состояние ═══
config.json что runc ПРИМЕНИЛ (требует root)
docker inspect что dockerd ЗАПРОСИЛ
/proc и /sys/fs/cgroup что ядро ПОКАЗЫВАЕТ изнутри
Третий источник доступен без привилегий и отвечает
на главный вопрос: применилось ли на самом деле.
...
CapEff=0000000000000000 — все capabilities отброшены. cpu.max=50000 100000 — это и есть --cpus 0.5: 50 мс квоты на период 100 мс (урок 13.3).
Совпадение всех трёх источников подтверждает, что запрошенное применилось.
Перезапуск daemon не убивает container'ы
cd /tmp/runtime
echo "═══ до перезапуска ═══"
before_pid="$(docker inspect rt-demo --format '{{.State.Pid}}')"
before_start="$(docker inspect rt-demo --format '{{.State.StartedAt}}')"
printf ' PID: %s\n' "$before_pid"
printf ' запущен: %s\n' "$before_start"
echo "═══ проверка настройки live-restore ═══"
docker info --format ' live-restore: {{.LiveRestoreEnabled}}' 2>/dev/null
echo "═══ что происходит при перезапуске ═══"
cat <<'TXT'
Перезапуск dockerd:
· dockerd останавливается
· containerd продолжает работать
· shim продолжает работать
· процесс приложения НЕ ЗАТРАГИВАЕТСЯ
· после запуска dockerd подключается к containerd заново
Перезапуск containerd:
· containerd останавливается
· shim продолжает работать (его родитель — не containerd)
· после запуска containerd находит shim по сокетам
в /run/containerd/io.containerd.runtime.v2.task/moby/
и подключается обратно
Настройка live-restore разрешает НЕ останавливать container'ы
при штатной остановке dockerd. Сама возможность пережить
перезапуск обеспечена архитектурой, а не этой настройкой.
Проверка на учебной машине:
sudo systemctl restart docker
docker inspect ИМЯ --format '{{.State.Pid}}'
→ PID тот же, StartedAt не изменился
TXT
echo "═══ фактическая проверка ═══"
if sudo -n true 2>/dev/null; then
sudo systemctl restart docker 2>/dev/null && sleep 5
after_pid="$(docker inspect rt-demo --format '{{.State.Pid}}' 2>/dev/null || echo '—')"
after_start="$(docker inspect rt-demo --format '{{.State.StartedAt}}' 2>/dev/null || echo '—')"
printf ' PID после перезапуска: %s (было %s)\n' "$after_pid" "$before_pid"
if [ "$after_pid" = "$before_pid" ]; then
echo " ✓ процесс тот же — container пережил перезапуск"
else
echo " PID изменился: container был перезапущен"
fi
else
echo " перезапуск daemon НЕ ВЫПОЛНЯЛСЯ: нужен sudo без пароля"
echo " утверждение проверяется командой выше на учебной машине"
fi
Ожидаемый вывод:
═══ до перезапуска ═══
PID: 53102
запущен: 2026-07-31T12:04:18.412Z
═══ проверка настройки live-restore ═══
live-restore: false
═══ что происходит при перезапуске ═══
Перезапуск dockerd:
· dockerd останавливается
· containerd продолжает работать
· shim продолжает работать
· процесс приложения НЕ ЗАТРАГИВАЕТСЯ
· после запуска dockerd подключается к containerd заново
...
═══ фактическая проверка ═══
перезапуск daemon НЕ ВЫПОЛНЯЛСЯ: нужен sudo без пароля
утверждение проверяется командой выше на учебной машине
Фактический перезапуск не выполнялся — и это сказано прямо, а не обойдено.
ctr: containerd без Docker
cd /tmp/runtime
echo "═══ пространства имён containerd ═══"
if command -v ctr > /dev/null 2>&1 && sudo -n true 2>/dev/null; then
sudo ctr namespaces list 2>/dev/null | sed 's/^/ /'
echo "═══ container'ы в пространстве moby ═══"
sudo ctr --namespace moby containers list 2>/dev/null | head -4 | sed 's/^/ /'
echo "═══ работающие задачи ═══"
sudo ctr --namespace moby tasks list 2>/dev/null | head -4 | sed 's/^/ /'
else
cat <<'TXT'
ctr недоступен или нет прав — команды НЕ ВЫПОЛНЯЛИСЬ.
Команды для справки:
sudo ctr namespaces list
sudo ctr --namespace moby containers list
sudo ctr --namespace moby tasks list
Container'ы Docker живут в пространстве имён moby.
TXT
fi
echo "═══ containers и tasks — разные сущности ═══"
cat <<'TXT'
containers описания: образ, спецификация, метки
tasks работающие процессы: PID, состояние
Это то же разделение, что POST /containers/create
и POST /containers/ID/start в Engine API ([урок 17.1]).
Container может существовать без задачи — состояние "created".
Задача без container'а существовать не может.
Три уровня клиентов:
docker полный набор возможностей Docker
nerdctl те же команды, но поверх containerd напрямую
ctr низкий уровень, для отладки containerd
TXT
Ожидаемый вывод:
═══ пространства имён containerd ═══
ctr недоступен или нет прав — команды НЕ ВЫПОЛНЯЛИСЬ.
Команды для справки:
sudo ctr namespaces list
sudo ctr --namespace moby containers list
sudo ctr --namespace moby tasks list
Container'ы Docker живут в пространстве имён moby.
═══ containers и tasks — разные сущности ═══
containers описания: образ, спецификация, метки
tasks работающие процессы: PID, состояние
Это то же разделение, что POST /containers/create
и POST /containers/ID/start в Engine API ([урок 17.1]).
...
Запуск через runc напрямую
cd /tmp/runtime
echo "═══ собираем bundle вручную ═══"
mkdir -p mybundle/rootfs
# Корневая файловая система: экспорт из образа
docker export "$(docker create --name rt-export alpine:3.21 true)" \
| tar -x -C mybundle/rootfs 2>/dev/null
docker rm rt-export > /dev/null 2>&1
printf ' rootfs: %s файлов верхнего уровня\n' "$(ls mybundle/rootfs | wc -l)"
echo "═══ config.json ═══"
if command -v runc > /dev/null 2>&1; then
(cd mybundle && runc spec 2>/dev/null) && echo " создан командой runc spec"
generated=1
else
cat > mybundle/config.json <<'JSON'
{
"ociVersion": "1.2.0",
"process": {
"terminal": false,
"user": {"uid": 0, "gid": 0},
"args": ["/bin/echo", "запущено напрямую через runc"],
"env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],
"cwd": "/",
"capabilities": {
"bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
"effective": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"],
"permitted": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"]
},
"noNewPrivileges": true
},
"root": {"path": "rootfs", "readonly": true},
"hostname": "runc-demo",
"mounts": [
{"destination": "/proc", "type": "proc", "source": "proc"},
{"destination": "/dev", "type": "tmpfs", "source": "tmpfs",
"options": ["nosuid", "strictatime", "mode=755", "size=65536k"]},
{"destination": "/sys", "type": "sysfs", "source": "sysfs",
"options": ["nosuid", "noexec", "nodev", "ro"]}
],
"linux": {
"namespaces": [
{"type": "pid"}, {"type": "ipc"}, {"type": "uts"}, {"type": "mount"}
],
"resources": {"memory": {"limit": 67108864}}
}
}
JSON
echo " создан вручную (runc недоступен)"
generated=0
fi
echo "═══ структура config.json ═══"
python3 - <<'PY'
import json
from pathlib import Path
spec = json.loads(Path("mybundle/config.json").read_text())
print(f" ociVersion: {spec.get('ociVersion')}")
print(f" args: {spec.get('process', {}).get('args')}")
print(f" root.path: {spec.get('root', {}).get('path')}")
print(f" root.readonly: {spec.get('root', {}).get('readonly')}")
ns = [n['type'] for n in spec.get('linux', {}).get('namespaces', [])]
print(f" namespaces: {', '.join(ns)}")
print(f" mounts: {len(spec.get('mounts', []))}")
caps = spec.get('process', {}).get('capabilities', {}).get('effective', [])
print(f" capabilities effective: {len(caps)}")
PY
echo "═══ запуск ═══"
if command -v runc > /dev/null 2>&1 && sudo -n true 2>/dev/null; then
(cd mybundle && sudo runc run runc-demo 2>&1) | sed 's/^/ /'
echo " container отработал и завершился"
else
cat <<'TXT'
runc недоступен или нет прав — запуск НЕ ВЫПОЛНЯЛСЯ.
Команда для справки:
cd mybundle && sudo runc run runc-demo
Что произошло бы:
runc читает config.json
создаёт namespaces вызовом clone
настраивает cgroups, монтирования, capabilities
выполняет execve указанной команды
ЗАВЕРШАЕТСЯ, оставив процесс приложения
Ни dockerd, ни containerd, ни shim при этом не участвуют.
TXT
fi
echo "═══ что показывает этот пример ═══"
cat <<'TXT'
runc не знает о Docker. Ему нужны только два объекта:
config.json описание по спецификации OCI
rootfs/ корневая файловая система
Всё, что делает docker run, сводится к формированию
этих двух объектов и вызову runc.
Отсюда взаимозаменяемость сред выполнения: crun, youki,
gVisor и Kata принимают тот же bundle и ту же спецификацию.
TXT
docker rm -f rt-demo > /dev/null 2>&1
cd /tmp && rm -rf /tmp/runtime
Ожидаемый вывод:
═══ собираем bundle вручную ═══
rootfs: 19 файлов верхнего уровня
═══ config.json ═══
создан вручную (runc недоступен)
═══ структура config.json ═══
ociVersion: 1.2.0
args: ['/bin/echo', 'запущено напрямую через runc']
root.path: rootfs
root.readonly: True
namespaces: pid, ipc, uts, mount
mounts: 3
capabilities effective: 3
═══ запуск ═══
runc недоступен или нет прав — запуск НЕ ВЫПОЛНЯЛСЯ.
Команда для справки:
cd mybundle && sudo runc run runc-demo
...
═══ что показывает этот пример ═══
runc не знает о Docker. Ему нужны только два объекта:
config.json описание по спецификации OCI
rootfs/ корневая файловая система
...
Bundle собран: корневая файловая система получена через docker export, описание — по спецификации OCI.
Запуск не выполнялся: runc не установлен как отдельная команда и требует прав. Об этом сказано прямо.
Практическое упражнение
Задание. Разберите цепочку выполнения от docker run до процесса.
Требования:
- Найти PID процесса container'а и построить цепочку родителей.
- Показать, что
runcне работает, и объяснить почему. - Показать соответствие флагов
docker runполям спецификации OCI по трём источникам. - Объяснить, кто хранит код выхода, и подтвердить это состоянием завершённого container'а.
- Собрать OCI bundle вручную и разобрать его структуру.
- Отметить, что именно не выполнялось из-за отсутствия прав или инструментов.
Подсказки
Подсказка 1
Родитель процесса — четвёртое поле в /proc/PID/stat.
Подсказка 2
Три источника: docker inspect (что запрошено), /sys/fs/cgroup изнутри (что применилось), config.json (что применил runc).
Подсказка 3
Корневая файловая система для bundle получается через docker export container'а.
Решение
Показать решение
mkdir -p /tmp/rtlab && cd /tmp/rtlab
cat > inspect_runtime.py <<'PY'
"""Разбор цепочки выполнения container'а.
Сопоставляет три источника состояния:
docker inspect что ЗАПРОСИЛ dockerd
/sys/fs/cgroup, /proc что ПОКАЗЫВАЕТ ядро
config.json что ПРИМЕНИЛ runc (требует root)
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
def run(*args: str) -> str:
result = subprocess.run(args, capture_output=True, text=True)
return result.stdout.strip()
def process_chain(pid: int, limit: int = 8) -> list[tuple[int, str]]:
"""Цепочка родителей: PID, имя. Родитель — 4-е поле /proc/PID/stat."""
chain: list[tuple[int, str]] = []
current = pid
for _ in range(limit):
if current in (0, 1):
comm = "init/systemd" if current == 1 else "kernel"
chain.append((current, comm))
break
stat_path = Path(f"/proc/{current}/stat")
comm_path = Path(f"/proc/{current}/comm")
if not stat_path.exists():
chain.append((current, "(нет доступа к /proc)"))
break
try:
comm = comm_path.read_text().strip()
fields = stat_path.read_text().rsplit(")", 1)[1].split()
parent = int(fields[1])
except (OSError, IndexError, ValueError):
chain.append((current, "(не прочитан)"))
break
chain.append((current, comm))
current = parent
return chain
def count_processes(pattern: str) -> int:
out = run("pgrep", "-c", pattern)
return int(out) if out.isdigit() else 0
def inspect_container(name: str) -> dict:
raw = run("docker", "inspect", name)
return json.loads(raw)[0] if raw else {}
def kernel_view(name: str) -> dict[str, str]:
"""Что видно изнутри container'а — без привилегий."""
script = (
'printf "uid=%s\\n" "$(id -u)"; '
'printf "cap_eff=%s\\n" "$(grep CapEff /proc/self/status | awk \'{print $2}\')"; '
'printf "memory_max=%s\\n" "$(cat /sys/fs/cgroup/memory.max 2>/dev/null)"; '
'printf "cpu_max=%s\\n" "$(cat /sys/fs/cgroup/cpu.max 2>/dev/null)"; '
'printf "pids_max=%s\\n" "$(cat /sys/fs/cgroup/pids.max 2>/dev/null)"; '
'touch /probe 2>/dev/null && printf "rootfs_writable=yes\\n" '
'|| printf "rootfs_writable=no\\n"'
)
out = run("docker", "exec", name, "sh", "-c", script)
result: dict[str, str] = {}
for line in out.splitlines():
if "=" in line:
key, _, value = line.partition("=")
result[key] = value
return result
def oci_spec(container_id: str) -> dict | None:
"""config.json из каталога состояния containerd. Требует root."""
path = Path("/run/containerd/io.containerd.runtime.v2.task/moby") \
/ container_id / "config.json"
probe = subprocess.run(["sudo", "-n", "cat", str(path)],
capture_output=True, text=True)
if probe.returncode != 0:
return None
try:
return json.loads(probe.stdout)
except json.JSONDecodeError:
return None
def main(name: str) -> int:
info = inspect_container(name)
if not info:
print(f" container {name} не найден")
return 2
pid = info["State"]["Pid"]
cid = info["Id"]
host = info.get("HostConfig", {})
print(" ── Цепочка процессов ──\n")
chain = process_chain(pid)
for level, (p, comm) in enumerate(chain):
indent = " " + " " * level
print(f"{indent}PID {p:<8} {comm}")
runc_n = count_processes("runc")
shim_n = count_processes("containerd-shim")
print()
print(f" процессов runc: {runc_n}")
print(f" процессов containerd-shim: {shim_n}")
print()
print(" runc завершился после настройки окружения:")
print(" он создаёт namespaces и cgroups, запускает процесс и уходит.")
print(" Родитель приложения — shim, поэтому перезапуск dockerd")
print(" и containerd не убивает container'ы.")
kernel = kernel_view(name)
spec = oci_spec(cid)
print("\n ── Три источника состояния ──\n")
print(f" {'свойство':<22} {'docker inspect':<22} {'ядро изнутри':<22} config.json")
print(" " + "─" * 88)
def spec_value(path: list[str]) -> str:
if spec is None:
return "нет доступа"
node: object = spec
for key in path:
if isinstance(node, dict) and key in node:
node = node[key]
else:
return "—"
return str(node)
rows = [
("пользователь", str(info["Config"].get("User") or "—"),
kernel.get("uid", "—"), spec_value(["process", "user", "uid"])),
("корень только чтение", str(host.get("ReadonlyRootfs")),
"нет" if kernel.get("rootfs_writable") == "yes" else "да",
spec_value(["root", "readonly"])),
("лимит памяти", str(host.get("Memory")),
kernel.get("memory_max", "—"),
spec_value(["linux", "resources", "memory", "limit"])),
("квота CPU", str(host.get("NanoCpus")),
kernel.get("cpu_max", "—"),
spec_value(["linux", "resources", "cpu", "quota"])),
("capabilities", str(host.get("CapDrop")),
kernel.get("cap_eff", "—"),
spec_value(["process", "capabilities", "effective"])[:18]),
]
for name_, requested, actual, applied in rows:
print(f" {name_:<22} {requested[:22]:<22} {actual[:22]:<22} {applied[:24]}")
print()
if spec is None:
print(" config.json НЕ ПРОЧИТАН: нужен sudo без пароля.")
print(" Два доступных источника при этом согласуются.")
else:
print(" Все три источника согласуются.")
print()
print(json.dumps({
"pid": pid,
"цепочка": [c[1] for c in chain],
"runc_процессов": runc_n,
"shim_процессов": shim_n,
"config_прочитан": spec is not None,
"cap_eff": kernel.get("cap_eff"),
"memory_max": kernel.get("memory_max"),
"cpu_max": kernel.get("cpu_max"),
}, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "rtlab-demo"))
PY
cat > make_bundle.sh <<'SH'
#!/usr/bin/env bash
# Сборка OCI bundle вручную: rootfs плюс config.json.
set -uo pipefail
BUNDLE="${1:-mybundle}"
IMAGE="${2:-alpine:3.21}"
mkdir -p "$BUNDLE/rootfs"
tmp="bundle-export-$$"
docker create --name "$tmp" "$IMAGE" true > /dev/null
docker export "$tmp" | tar -x -C "$BUNDLE/rootfs" 2>/dev/null
docker rm "$tmp" > /dev/null
cat > "$BUNDLE/config.json" <<'JSON'
{
"ociVersion": "1.2.0",
"process": {
"terminal": false,
"user": {"uid": 10001, "gid": 10001},
"args": ["/bin/sh", "-c", "id -u; cat /proc/self/status | grep CapEff"],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/",
"capabilities": {
"bounding": [],
"effective": [],
"permitted": []
},
"noNewPrivileges": true,
"rlimits": [{"type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024}]
},
"root": {"path": "rootfs", "readonly": true},
"hostname": "runc-bundle",
"mounts": [
{"destination": "/proc", "type": "proc", "source": "proc"},
{"destination": "/dev", "type": "tmpfs", "source": "tmpfs",
"options": ["nosuid", "strictatime", "mode=755", "size=65536k"]},
{"destination": "/sys", "type": "sysfs", "source": "sysfs",
"options": ["nosuid", "noexec", "nodev", "ro"]},
{"destination": "/tmp", "type": "tmpfs", "source": "tmpfs",
"options": ["nosuid", "nodev", "mode=1777", "size=16m"]}
],
"linux": {
"namespaces": [
{"type": "pid"}, {"type": "ipc"}, {"type": "uts"},
{"type": "mount"}, {"type": "network"}, {"type": "cgroup"}
],
"resources": {
"memory": {"limit": 67108864},
"cpu": {"quota": 50000, "period": 100000},
"pids": {"limit": 64}
},
"maskedPaths": ["/proc/kcore", "/proc/keys", "/sys/firmware"],
"readonlyPaths": ["/proc/bus", "/proc/sys", "/proc/sysrq-trigger"]
}
}
JSON
printf ' bundle: %s\n' "$BUNDLE"
printf ' rootfs: %s каталогов верхнего уровня\n' "$(ls "$BUNDLE/rootfs" | wc -l)"
printf ' config.json: %s байт\n' "$(wc -c < "$BUNDLE/config.json")"
SH
chmod +x make_bundle.sh
fail=0
ok() { printf ' ✓ %s\n' "$1"; }
bad() { printf ' ✗ %s\n' "$1"; fail=1; }
printf '\n═══ Подготовка ═══\n'
docker rm -f rtlab-demo > /dev/null 2>&1
docker run -d --name rtlab-demo \
--memory 256m --cpus 0.5 --pids-limit 64 \
--cap-drop=ALL --read-only --tmpfs /tmp \
--user 10001:10001 \
alpine:3.21 sh -c 'echo запущен; sleep 300' > /dev/null
sleep 2
printf ' container создан с ограничениями\n'
printf '\n═══ Требования 1-3: цепочка и три источника ═══\n'
python3 inspect_runtime.py rtlab-demo > runtime.log 2>&1
sed -n '1,/^ {/p' runtime.log | head -34
summary="$(tail -1 runtime.log)"
runc_n="$(echo "$summary" | python3 -c "import json,sys; print(json.load(sys.stdin)['runc_процессов'])")"
shim_n="$(echo "$summary" | python3 -c "import json,sys; print(json.load(sys.stdin)['shim_процессов'])")"
chain="$(echo "$summary" | python3 -c "
import json,sys; print(' → '.join(json.load(sys.stdin)['цепочка']))")"
cap_eff="$(echo "$summary" | python3 -c "import json,sys; print(json.load(sys.stdin)['cap_eff'])")"
cpu_max="$(echo "$summary" | python3 -c "import json,sys; print(json.load(sys.stdin)['cpu_max'])")"
cfg_read="$(echo "$summary" | python3 -c "import json,sys; print(json.load(sys.stdin)['config_прочитан'])")"
printf '\n цепочка: %s\n' "$chain"
printf ' процессов runc: %s, shim: %s\n' "$runc_n" "$shim_n"
[ "${runc_n:-1}" -eq 0 ] && [ "${shim_n:-0}" -ge 1 ] \
&& ok "runc не работает; родитель приложения — shim" \
|| bad "runc=$runc_n shim=$shim_n"
printf ' CapEff изнутри: %s\n' "$cap_eff"
printf ' cpu.max изнутри: %s (--cpus 0.5 → квота 50000 на период 100000)\n' "$cpu_max"
[ "$cap_eff" = "0000000000000000" ] \
&& ok "--cap-drop=ALL применился: ядро показывает нулевой набор" \
|| bad "CapEff=$cap_eff"
case "$cpu_max" in
"50000 100000") ok "--cpus 0.5 применился: квота 50 мс на период 100 мс" ;;
*) bad "cpu.max=$cpu_max" ;;
esac
printf ' config.json прочитан: %s\n' "$cfg_read"
[ "$cfg_read" = "True" ] \
&& ok "все три источника сверены" \
|| ok "config.json недоступен без sudo — отмечено, два источника сверены"
printf '\n═══ Требование 4: кто хранит код выхода ═══\n'
docker rm -f rtlab-exit > /dev/null 2>&1
docker run --name rtlab-exit alpine:3.21 sh -c 'exit 42' > /dev/null 2>&1
exit_code="$(docker inspect rtlab-exit --format '{{.State.ExitCode}}')"
finished="$(docker inspect rtlab-exit --format '{{.State.FinishedAt}}')"
pid_after="$(docker inspect rtlab-exit --format '{{.State.Pid}}')"
printf ' код выхода: %s\n' "$exit_code"
printf ' завершён: %s\n' "$finished"
printf ' PID: %s (процесса больше нет)\n' "$pid_after"
printf '\n Механизм: shim — родитель процесса container а.\n'
printf ' При завершении ядро отдаёт код РОДИТЕЛЮ через waitpid.\n'
printf ' Shim сохраняет код и передаёт containerd, когда тот спросит.\n'
printf '\n Поэтому код не теряется, даже если dockerd или containerd\n'
printf ' были перезапущены в момент завершения процесса.\n'
docker rm -f rtlab-exit > /dev/null 2>&1
[ "$exit_code" = "42" ] && [ "${pid_after:-1}" -eq 0 ] \
&& ok "код выхода сохранён после исчезновения процесса" \
|| bad "код=$exit_code pid=$pid_after"
printf '\n═══ Требование 5: OCI bundle вручную ═══\n'
./make_bundle.sh mybundle alpine:3.21
python3 - <<'PY'
import json
from pathlib import Path
spec = json.loads(Path("mybundle/config.json").read_text())
proc = spec["process"]
linux = spec["linux"]
res = linux["resources"]
print()
print(f" {'элемент спецификации':<32} значение")
print(" " + "─" * 68)
print(f" {'ociVersion':<32} {spec['ociVersion']}")
print(f" {'process.args':<32} {' '.join(proc['args'][:2])}...")
print(f" {'process.user.uid':<32} {proc['user']['uid']}")
print(f" {'process.noNewPrivileges':<32} {proc['noNewPrivileges']}")
print(f" {'process.capabilities.effective':<32} {len(proc['capabilities']['effective'])} шт.")
print(f" {'root.readonly':<32} {spec['root']['readonly']}")
print(f" {'linux.namespaces':<32} {', '.join(n['type'] for n in linux['namespaces'])}")
print(f" {'linux.resources.memory.limit':<32} {res['memory']['limit']}")
print(f" {'linux.resources.cpu.quota':<32} {res['cpu']['quota']} / {res['cpu']['period']}")
print(f" {'linux.resources.pids.limit':<32} {res['pids']['limit']}")
print(f" {'mounts':<32} {len(spec['mounts'])}")
print(f" {'maskedPaths':<32} {len(linux['maskedPaths'])}")
print()
print(" Соответствие флагам docker run:")
mapping = [
("--user 10001:10001", "process.user.uid / gid"),
("--cap-drop=ALL", "process.capabilities (пустые списки)"),
("--read-only", "root.readonly"),
("--memory 64m", "linux.resources.memory.limit"),
("--cpus 0.5", "linux.resources.cpu.quota / period"),
("--pids-limit 64", "linux.resources.pids.limit"),
("--tmpfs /tmp", "mounts[] с type=tmpfs"),
("(по умолчанию)", "process.noNewPrivileges, maskedPaths"),
]
for flag, field in mapping:
print(f" {flag:<24} → {field}")
PY
n_ns="$(python3 -c "
import json
from pathlib import Path
print(len(json.loads(Path('mybundle/config.json').read_text())['linux']['namespaces']))
")"
rootfs_dirs="$(ls mybundle/rootfs 2>/dev/null | wc -l)"
printf '\n namespaces в спецификации: %s, каталогов в rootfs: %s\n' "$n_ns" "$rootfs_dirs"
[ "${n_ns:-0}" -ge 5 ] && [ "${rootfs_dirs:-0}" -ge 10 ] \
&& ok "bundle собран: спецификация и корневая файловая система на месте" \
|| bad "namespaces=$n_ns rootfs=$rootfs_dirs"
printf '\n═══ Требование 6: что не выполнялось ═══\n'
python3 - <<'PY'
import shutil
import subprocess
def has_sudo() -> bool:
return subprocess.run(["sudo", "-n", "true"],
capture_output=True).returncode == 0
CHECKS = [
("чтение config.json из bundle containerd", has_sudo(),
"нужен sudo: каталог /run/containerd принадлежит root"),
("запуск через runc напрямую", shutil.which("runc") is not None and has_sudo(),
"нужен runc в PATH и права root"),
("ctr --namespace moby containers list", shutil.which("ctr") is not None and has_sudo(),
"нужен ctr и права root"),
("перезапуск dockerd с проверкой PID", has_sudo(),
"нужен sudo для systemctl restart docker"),
]
print(f" {'проверка':<44} {'выполнена':<12} причина")
print(" " + "─" * 96)
not_run = 0
for name, done, why in CHECKS:
if not done:
not_run += 1
print(f" {name:<44} {'да' if done else 'НЕТ':<12} {'' if done else why}")
print()
print(f" не выполнено: {not_run} из {len(CHECKS)}")
print()
print(" Эти проверки требуют прав root. Утверждения о них в уроке")
print(" приведены с командами для самостоятельного выполнения")
print(" на учебной машине, а не как результат измерения.")
PY
ok "невыполненные проверки перечислены с причиной"
printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo " все требования выполнены" || echo " ЕСТЬ ПРОВАЛЫ"
docker rm -f rtlab-demo > /dev/null 2>&1
cd /tmp && rm -rf /tmp/rtlab
exit "$fail"
Ожидаемый вывод:
═══ Подготовка ═══
container создан с ограничениями
═══ Требования 1-3: цепочка и три источника ═══
── Цепочка процессов ──
PID 53102 sh
PID 53081 containerd-shim
PID 1 init/systemd
процессов runc: 0
процессов containerd-shim: 3
runc завершился после настройки окружения:
он создаёт namespaces и cgroups, запускает процесс и уходит.
Родитель приложения — shim, поэтому перезапуск dockerd
и containerd не убивает container'ы.
── Три источника состояния ──
свойство docker inspect ядро изнутри config.json
────────────────────────────────────────────────────────────────────────────────────────
пользователь 10001:10001 10001 нет доступа
корень только чтение True да нет доступа
лимит памяти 268435456 268435456 нет доступа
квота CPU 500000000 50000 100000 нет доступа
capabilities ['ALL'] 0000000000000000 нет доступа
config.json НЕ ПРОЧИТАН: нужен sudo без пароля.
Два доступных источника при этом согласуются.
цепочка: sh → containerd-shim → init/systemd
процессов runc: 0, shim: 3
✓ runc не работает; родитель приложения — shim
CapEff изнутри: 0000000000000000
cpu.max изнутри: 50000 100000 (--cpus 0.5 → квота 50000 на период 100000)
✓ --cap-drop=ALL применился: ядро показывает нулевой набор
✓ --cpus 0.5 применился: квота 50 мс на период 100 мс
config.json прочитан: False
✓ config.json недоступен без sudo — отмечено, два источника сверены
═══ Требование 4: кто хранит код выхода ═══
код выхода: 42
завершён: 2026-07-31T12:08:41.203Z
PID: 0 (процесса больше нет)
Механизм: shim — родитель процесса container а.
При завершении ядро отдаёт код РОДИТЕЛЮ через waitpid.
Shim сохраняет код и передаёт containerd, когда тот спросит.
Поэтому код не теряется, даже если dockerd или containerd
были перезапущены в момент завершения процесса.
✓ код выхода сохранён после исчезновения процесса
═══ Требование 5: OCI bundle вручную ═══
bundle: mybundle
rootfs: 19 каталогов верхнего уровня
config.json: 1284 байт
элемент спецификации значение
────────────────────────────────────────────────────────────────────
ociVersion 1.2.0
process.args /bin/sh -c...
process.user.uid 10001
process.noNewPrivileges True
process.capabilities.effective 0 шт.
root.readonly True
linux.namespaces pid, ipc, uts, mount, network, cgroup
linux.resources.memory.limit 67108864
linux.resources.cpu.quota 50000 / 100000
linux.resources.pids.limit 64
mounts 4
maskedPaths 3
Соответствие флагам docker run:
--user 10001:10001 → process.user.uid / gid
--cap-drop=ALL → process.capabilities (пустые списки)
--read-only → root.readonly
--memory 64m → linux.resources.memory.limit
--cpus 0.5 → linux.resources.cpu.quota / period
--pids-limit 64 → linux.resources.pids.limit
--tmpfs /tmp → mounts[] с type=tmpfs
(по умолчанию) → process.noNewPrivileges, maskedPaths
namespaces в спецификации: 6, каталогов в rootfs: 19
✓ bundle собран: спецификация и корневая файловая система на месте
═══ Требование 6: что не выполнялось ═══
проверка выполнена причина
────────────────────────────────────────────────────────────────────────────────────────────────
чтение config.json из bundle containerd НЕТ нужен sudo: каталог /run/containerd принадлежит root
запуск через runc напрямую НЕТ нужен runc в PATH и права root
ctr --namespace moby containers list НЕТ нужен ctr и права root
перезапуск dockerd с проверкой PID НЕТ нужен sudo для systemctl restart docker
не выполнено: 4 из 4
✓ невыполненные проверки перечислены с причиной
═══ ИТОГ ═══
все требования выполнены
Все требования выполнены; четыре проверки из четырёх, требующих root, не выполнялись, и это перечислено явно.
Требование 3 даёт главный практический приём: cpu.max=50000 100000 изнутри container'а подтверждает, что --cpus 0.5 применился, а не просто был запрошен. Расхождение между docker inspect и показаниями ядра означало бы, что запрос не выполнен.
Три решения, определяющие качество.
Состояние сверяется по трём источникам, из которых два доступны без привилегий. config.json требует root и на большинстве машин недоступен. Но docker inspect и /sys/fs/cgroup изнутри container'а дают ответ на главный вопрос — применилось ли запрошенное. Урок, опирающийся только на config.json, был бы невоспроизводим для читателя без sudo.
Цепочка родителей строится разбором /proc/PID/stat, а не вызовом pstree. Готовая утилита есть не везде, а поле родителя в stat — часть интерфейса ядра. Заодно видно, что цепочка обрывается на systemd: родитель shim — не containerd, и это объясняет, почему shim переживает его перезапуск.
Код выхода проверяется на container'е, процесса которого уже нет. PID=0 и ExitCode=42 в одном выводе показывают, что код хранится не в процессе. Дальше остаётся назвать, кто его хранит, — и объяснение про waitpid у shim становится проверяемым утверждением, а не пересказом схемы.
Чего решение не делает. Четыре проверки требуют root и не выполнялись: чтение config.json, запуск через runc, работа с ctr, перезапуск daemon. Для каждой приведена команда — но результат не измерен, а описан. Bundle собран, однако не запущен: без runc и прав это невозможно, и утверждение «runc создаёт окружение и завершается» подтверждается косвенно — отсутствием процессов runc при работающих container'ах. Альтернативные среды выполнения (crun, youki, gVisor) названы как принимающие тот же bundle, но не проверялись.
Проверка результата
docker inspect ИМЯ --format '{{.State.Pid}}'
cat /proc/$(docker inspect ИМЯ --format '{{.State.Pid}}')/status | grep PPid
pgrep -c runc; pgrep -c containerd-shim
sudo ls /run/containerd/io.containerd.runtime.v2.task/moby/
docker exec ИМЯ cat /sys/fs/cgroup/cpu.max
pgrep -c runc должен давать ноль при работающих container'ах.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
Искать процесс runc среди работающих | Считают его средой выполнения | Он завершается после настройки |
Считать dockerd родителем процесса | Логично предположить | Родитель — shim |
| Ожидать гибели container'ов при перезапуске daemon | Кажется естественным | Shim переживает перезапуск |
Считать live-restore причиной живучести | Название наводит | Возможность обеспечена архитектурой |
Полагаться только на docker inspect | Проще | Он показывает запрошенное, а не применённое |
| Не проверять состояние изнутри container'а | Нужны привилегии для config.json | /sys/fs/cgroup доступен без них |
Путать containers и tasks в ctr | Похожие понятия | Описание против работающего процесса |
Ожидать, что runc знает о Docker | Он часть Docker | Он принимает только bundle по OCI |
Считать ctr заменой docker | Тоже клиент | Низкий уровень; удобства не предполагается |
Контрольные вопросы
На понимание:
- Почему процесс
runcне виден среди работающих? - Три задачи shim — назовите и объясните каждую.
- Почему перезапуск
dockerdне убивает container'ы? - Кто хранит код выхода и как он его получает?
- Что такое OCI bundle и из чего он состоит?
На применение:
- Как проверить, что
--cpus 0.5действительно применился? - Как найти
config.jsonработающего container'а? - Как собрать bundle и запустить container без Docker?
На диагностику:
- После перезапуска containerd
docker psпоказывает container'ы, ноdocker logsпуст. Гипотеза? docker inspectпоказывает лимит памяти, а приложение его не ощущает. Что проверить?
Краткое резюме
- Цепочка:
dockerd→containerd→ shim → процесс;runcвызывается и завершается. runc— программа настройки, а не среда выполнения: после неё остаётся процесс приложения.- Shim удерживает stdio, хранит код выхода и переживает перезапуск daemon.
- Родитель процесса container'а — shim, а не
dockerdи неcontainerd. - Код выхода ядро отдаёт родителю через
waitpid; shim сохраняет его до запроса. live-restoreразрешает не останавливать container'ы; сама живучесть обеспечена архитектурой.- OCI bundle — это
config.jsonплюсrootfs; большеruncничего не нужно. - Все флаги
docker runпревращаются в поляconfig.json. config.jsonлежит в/run/containerd/io.containerd.runtime.v2.task/moby/<id>/.- Состояние сверяют по трём источникам:
inspect, ядро изнутри,config.json. - Два первых источника доступны без привилегий и отвечают на вопрос «применилось ли».
ctrразличаетcontainersиtasks— то же, чтоcreateиstartв API.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| OCI Runtime Specification | https://github.com/opencontainers/runtime-spec/blob/main/spec.md | Bundle, config.json |
OCI: config.json | https://github.com/opencontainers/runtime-spec/blob/main/config.md | Поля спецификации |
| OCI: Linux-специфика | https://github.com/opencontainers/runtime-spec/blob/main/config-linux.md | Namespaces, cgroups, seccomp |
| runc | https://github.com/opencontainers/runc | runc spec, run, state |
| containerd | https://containerd.io/docs/ | Архитектура, пространства имён |
| containerd: shim | https://github.com/containerd/containerd/blob/main/core/runtime/v2/README.md | Роль и протокол shim |
Docker: live-restore | https://docs.docker.com/engine/daemon/live-restore/ | Поведение при перезапуске |
| Docker: архитектура | https://docs.docker.com/get-started/docker-overview/ | Компоненты и их связь |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Namespaces: углублённо
Главное оглавление