# Середовище виконання контейнерів

## Що це таке

Середовище виконання контейнерів, англійською container runtime, — це програмне забезпечення між
образом контейнера на диску та ізольованим процесом у ядрі. Ця назва охоплює два рівні, і чимало
плутанини виникає через те, що люди мають на увазі різні: низькорівневе середовище, яке ще називають
OCI-runtime, і високорівневе, яке частіше звуть рушієм контейнерів. Людина здебільшого працює із
середовищами виконання контейнерів через рушій, а не напряму.

Низькорівневе середовище має одне вузьке завдання. Воно читає бандл, який специфікація середовища
виконання від Open Container Initiative визначає як каталог із файлом `config.json` і кореневою
файловою системою контейнера, просить у ядра простори імен, cgroups і монтування, описані в
конфігурації, і запускає процес усередині них. Специфікація називає операції, які мусить
підтримувати кожне таке середовище: create, start, kill, delete і запит стану, а командний рядок
залишає на розсуд реалізації. Версія 1.0 вийшла в липні 2017 року. runc, який підтримує сама OCI,
описує себе як низькорівневий інструмент, не призначений для кінцевого користувача. crun виконує ту
саму роботу, написаний мовою C, споживає менше пам'яті й названий у документації Podman типовим.
youki — ще одне, написане на Rust.

Рушій розташований вище. containerd описує себе як демон, що керує повним життєвим циклом
контейнерів на своєму хості: передаванням і зберіганням образів, виконанням контейнерів і наглядом
за ними, а також підключенням сховища та мережі. CRI-O виконує подібну роботу, але створений лише
для Kubernetes. Рушій завантажує й розпаковує образ, записує бандл, передає його низькорівневому
середовищу й продовжує стежити за процесом і після того, як те завершило роботу. Docker Engine
стоїть ще на щабель вище: за його документацією, він використовує containerd для життєвого циклу
контейнерів, а containerd типово запускає runc. Podman іде іншим шляхом і обходиться без постійного
демона. Кожна команда сама готує бандл, викликає crun або runc і залишає запущений контейнер під
наглядом невеликого процесу conmon.

Kubernetes додає до словника ще одне поняття. Kubelet на кожному вузлі спілкується з рушієм через
Container Runtime Interface, або CRI, — протокол на gRPC, який реалізують і containerd, і CRI-O.
Docker Engine старший за CRI, тож Kubernetes роками тримав для нього адаптер dockershim і прибрав
його у версії 1.24.

## Чому це важливо

Вибір середовища виконання насправді є вибором рівня, який змінюють, і ці два рівні дають різне.

Рушій визначає повсякденне життя контейнерів: хто завантажує й зберігає образи, хто перезапускає
контейнер після збою, куди йдуть його журнали, яким API користуються інші інструменти і кому
належить дерево процесів. Заміна Docker Engine на Podman на сервері чи на containerd на вузлі
Kubernetes змінює все це, а самі контейнери лишає як є, бо формат образу й бандл спираються на ті
самі стандарти OCI. Документація Kubernetes про вилучення dockershim каже прямо: образи, зібрані
через `docker build`, працюють з усіма реалізаціями CRI.

Низькорівневе середовище визначає, наскільки ізольований процес. runc і crun запускають звичайний
процес на ядрі хоста: це швидко й повністю сумісно, і всі контейнери на машині ділять одне ядро.
Середовище gVisor, runsc, ставить між контейнером і хостом прикладне ядро, написане на Go, і саме
відповідає на системні виклики контейнера; документація проєкту називає ціною цього нижчу сумісність
застосунків і більші витрати на кожен системний виклик. Kata Containers запускає кожне навантаження
в легкій віртуальній машині з власним ядром, використовує апаратну віртуалізацію як другий рубіж
захисту і потребує гіпервізора на кожному вузлі, де працює.

Kubernetes дозволяє робити другий вибір для окремого пода, а не для всього кластера. RuntimeClass
указує на обробник, налаштований у рушії, і под із полем `runtimeClassName` працює під цим
обробником. Так навантаження, якому не довіряють, може жити в gVisor чи Kata, а решта лишається на
runc. Документація описує це як баланс між продуктивністю та безпекою.

## Як це виглядає на практиці

У повсякденній мові назва тяжіє до рівня рушія, і робота на цьому сайті вживає її саме так.

Двадцять застосунків, упакованих у Docker і
[розподілених між двома хмарами й кількома середовищами виконання контейнерів](/uk/portfolio/deployed-and-maintained-20-docker-containerized-applications-troubleshooting-19/),
мали розгортатися послідовно й швидко діагностуватися в разі проблем. Docker їх пакував, усунення
несправностей ішло через Podman, а налагодження оркестрації — через Kubernetes. Така робота з
кількома рушіями тримається на описаному вище поділі на рівні: рушії різні, а стандарти OCI для
образів і середовищ виконання під ними спільні.

На новішому хості вибір зроблено на рівні рушія.
[Контейнерна площина працює на Podman і Quadlet замість Docker](/uk/portfolio/deployed-a-container-plane-on-podman-and-quadlet-122/),
бо типовий рушій публікує порти контейнерів вище за правила брандмауера, які встановлює роль
посилення захисту, і додав би другого наглядача поруч із systemd. Вибір змінив те, хто запускає
кожен контейнер і хто за ним потім стежить: контейнери стали службами, які запускаються так, як
запускаються служби, під наглядачем, якому хост уже довіряв.

Те саме міркування переходить на
[хост із 512 МБ, на якому працює вся компанія](/uk/portfolio/ran-the-whole-company-on-one-512mb-host-109/),
де пам'ять вирішувала кожну суперечку. Його контейнерна площина — це модулі Quadlet під systemd, а
не демон із власним наглядачем; такий вибір зроблено на машині, де придатними до вжитку є близько
464 МБ пам'яті.

## Де виникають проблеми

Проблеми із середовищами виконання зазвичай з'являються на стиках між рівнями, де кожна сторона
вважає, що інша налаштована так, як вона очікує.

**Інструменти на вузлі сліпнуть після оновлення кластера.** Починаючи з Kubernetes 1.24, kubelet
спілкується з containerd або CRI-O напряму, тож контейнери, які планує кластер, оминають Docker
Engine. `docker ps` на вузлі показує лише те, що запустив сам Docker, а агенти моніторингу, стартові
скрипти й привілейовані поди, які викликають Docker CLI або монтують `docker.sock`, втрачають огляд.
Навантаження працюють далі. Переводити на `crictl` або на власний API рушія треба інструменти
навколо них.

**Kubelet і середовище виконання керують cgroups по-різному.** Обидва мусять використовувати той
самий драйвер cgroup, `cgroupfs` або `systemd`, а kubelet типово бере `cgroupfs`. На хості, де
systemd є системою ініціалізації, це дає два менеджери cgroup із двома різними поглядами на ті самі
ресурси, і документація Kubernetes попереджає, що такі вузли можуть ставати нестабільними, коли
ресурсів бракує. Виправлення — драйвер systemd з обох боків; саме його рекомендують і для cgroup v2.
Для containerd 2.x з runc бік середовища виконання має такий вигляд:

```toml
# /etc/containerd/config.toml, containerd 2.x
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
  SystemdCgroup = true
```

На боці kubelet це `cgroupDriver: systemd` у його конфігурації, а containerd після цього треба
перезапустити. containerd 1.x пише назву розділу інакше, і сторінка Kubernetes наводить обидві
форми. Зміна драйвера на вузлі, який уже приєднано до кластера, — делікатна операція: поди, створені
з одним драйвером, можуть отримати помилки під час повторного створення з іншим, тому документація
радить замінити вузол або перевстановити його з новою конфігурацією.

**Под, перенесений до ізольованого середовища, зупиняється на старті.** RuntimeClass з обробником,
якого рушій вузла не має, одразу переводить под у фазу Failed, а причину видно в події. Далі
навантаження має пасувати до пісочниці. gVisor відповідає на системні виклики у власному ядрі, тож
програми, що спираються на рідше вживані можливості ядра, можуть поводитися інакше, а документація
називає навантаження з великою кількістю системних викликів поганим варіантом. Kata потребує
апаратної віртуалізації на вузлі, а на хмарній віртуальній машині це означає вкладену віртуалізацію.
Селектор вузлів `scheduling` у RuntimeClass тримає такі поди на вузлах, які здатні їх запустити.

**Образ, імпортований вручну, лишається невидимим для kubelet.** containerd зберігає назви образів і
метадані окремо для кожного простору імен. Його клієнт `ctr` працює в просторі `default`, якщо не
вказати інший, а втулок CRI, з яким говорить kubelet, — у `k8s.io`. Тож образ, завантажений
звичайним `ctr images import`, опиняється там, куди kubelet не дивиться, і под знову йде до реєстру
або зупиняється з помилкою завантаження образу. Імпорт через `ctr -n k8s.io images import` і
перевірка через `crictl images`, яка бачить те саме, що й kubelet, закривають цю щілину.

## Що почитати далі

- [OCI Runtime Specification](https://github.com/opencontainers/runtime-spec) — стандарт, який
  реалізує кожне низькорівневе середовище: бандл, `config.json` і операції життєвого циклу.
- [runc](https://github.com/opencontainers/runc) — власне середовище виконання OCI з поясненнями, як
  ним керують інструменти вищого рівня.
- [Container Runtimes](https://kubernetes.io/docs/setup/production-environment/container-runtimes/)
  — драйвери cgroup, версії CRI та налаштування containerd і CRI-O, яких очікує Kubernetes.
- [Container Runtime Interface (CRI)](https://kubernetes.io/docs/concepts/containers/cri/) —
  протокол gRPC між kubelet і рушієм.
- [Runtime Class](https://kubernetes.io/docs/concepts/containers/runtime-class/) — вибір іншого
  обробника для окремого пода і як тримати такі поди на вузлах, що його підтримують.
- [Alternative container runtimes](https://docs.docker.com/engine/daemon/alternative-runtimes/) — як
  Docker Engine передає контейнери до gVisor, Kata Containers, youki чи Wasmtime.

<https://engineer.company/uk/glossary/container-runtime/>
