Мечта: systemd как execution plane
Все мы знаем старый прикол — заказ пиццы через Kubernetes (это, кстати, реальный проект)… Смешно, потому что показывает, до какой степени создатели Kubernetes оторвались от простого «запустить процесс»: между декларативным состоянием в API-сервере и реальным процессом на хосте у него почти ещё одна целая ОС — CRI, containerd, runc. Но на любом Linux-хосте уже есть менеджер выполнения с cgroups, namespaces, зависимостями, журналом и D-Bus API — systemd… Вас ничего не смущает?
Откройте любую документацию Kubernetes — там будет написано, что это просто Control Plane для оркестрации контейнеров. Но при этом сами создатели теперь предпочитают об этом только говорить: реальная нода обросла отдельной ОС, целой прослойкой между декларативным состоянием и процессом и кучей ограничений, которых на бумаге никто не обещал.
Кто-то ещё помнит священную войну вокруг Docker и systemd в Kubernetes? В 1.20 объявили deprecation dockershim — и в интернете началась паника «Kubernetes бросает Docker», хотя на деле для образов и Dockerfile не менялось ничего, отваливалась только прямая интеграция kubelet → dockerd. Причина была прозаичной: dockershim — это шим над шимом. Docker сам внутри уже работает поверх containerd → runc, а kubelet вдобавок этого держал отдельный CRI-шим, транслирующий вызовы в Docker Engine API. Итоговый путь одного контейнера: kubelet → CRI → dockershim → dockerd → containerd → runc → процесс. Шесть абстракций, чтобы запустить один процесс.
Зачем
|
|
Stock kubelet + containerd |
Periapsis |
|---|---|---|
|
RSS демонов в простое |
~350 МБ |
~74 МБ (1 pawn) — ~130 МБ (30 pawns) |
|
оверхед на под |
~15–20 МБ (containerd) |
< 1 МБ (нативный юнит) |
|
Подов на хост |
110 (жёсткий потолок) |
тысячи (зависит от мощности) |
|
Логи |
CRI-текстовые файлы |
journald |
|
Обновление демона |
обычно zero-downtime |
zero-downtime |
Возьмём мой реальный пример: у меня VPS на 1 CPU и 1 ГБ RAM, kubelet + Cilium + Envoy Proxy, вроде стандартный набор, но на нём еле помещалось, и это — еще без какого-либо workload. Обычный kubelet, обычный CNI, обычный ingress. Просто сама инфра уже съедала почти весь гигабайт до того, как в кластере появлялась хоть одна реальная нагрузка. Для сравнения: на похожей по классу машине (1 vCPU, 2 GB RAM, VPS) у меня весь control plane вместе с рабочей нагрузкой — apiserver, controller-manager, scheduler, kube-proxy, CoreDNS и один nginx — уместились в ~523 МБ, с запасом (далее в разделе «Цифры»).
На мощном железе эту же проблему обычно решают с другой стороны: не убирают прослойку, а наращивают ещё одну. KubeVirt запускает полноценные VM (через QEMU/libvirt) как поды — удобно управлять виртуалками через kubectl, но каждый под тянет за собой настоящее ядро, полноценный boot и сотни МБ RAM только на саму VM, вне зависимости от того, что внутри реально работает. Kata Containers идёт мягче: каждый контейнер получает свой облегчённый microVM ради изоляции сильнее, чем namespaces + cgroups, но тоже не бесплатно — обычно ~130-170 МБ и заметно более медленный старт на контейнер. Оба честно решают проблему изоляции, но ценой ещё одного слоя между декларативным состоянием и процессом.
А что если у вас мощное железо для внутренней инфраструктуры? Или скромный homelab? Вам не нужна VM-изоляция от чужих тенантов, вам просто нужно нарезать одну машину на столько нод, сколько реально нужно — без CRI с одной стороны и без гипервизора с другой. Ровно об этом весь этот пост.
Весь этот набор компромиссов — следствие одного молчаливого ограничения: kubelet считает, что одна нода = одна машина. Нигде в документации Kubernetes это прямо не написано, но зашито в архитектуру kubelet.
Архитектура
Pawns, not nodes
$ kubectl get nodes -o wideNAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIMEengifire Ready primary 2d19h perigeos://6c446973 192.168.50.2 <none> Arch Linux 7.0.14-zen1-1-zen (amd64) systemd://262~develengifire-pawn-01 Ready pawn 2d19h perigeos://6c446973 192.168.50.2 <none> Arch Linux 7.0.14-zen1-1-zen (amd64) systemd://262~develengifire-scale-1 Ready pawn 21h perigeos://6c446973 192.168.50.2 <none> Arch Linux 7.0.14-zen1-1-zen (amd64) systemd://262~develengix99 Ready primary 2d20h perigeos://7f68f512 192.168.100.200 <none> Arch Linux 7.0.14-x64v3-xanmod1 (amd64) systemd://262~develengix99-e2e-1 Ready pawn 2d20h perigeos://7f68f512 192.168.100.200 <none> Arch Linux 7.0.14-x64v3-xanmod1 (amd64) systemd://262~develengix99-e2e-2 Ready pawn 2d20h perigeos://7f68f512 192.168.100.200 <none> Arch Linux 7.0.14-x64v3-xanmod1 (amd64) systemd://262~develengix99-scale-1 Ready pawn 21h perigeos://7f68f512 192.168.100.200 <none> Arch Linux 7.0.14-x64v3-xanmod1 (amd64) systemd://262~develengix99-trail-1 Ready pawn 2d20h perigeos://7f68f512 192.168.100.200 <none> Arch Linux 7.0.14-x64v3-xanmod1 (amd64) systemd://262~develengix99-trail-2 Ready pawn 2d20h perigeos://7f68f512 192.168.100.200 <none> Arch Linux 7.0.14-x64v3-xanmod1 (amd64) systemd://262~devel
Один процесс perigeos поднимает N «виртуальных» нод (Pawn) — у каждой свой TLS-сертификат (через CSR API кластера), свой kubelet HTTP endpoint (exec/attach/logs/port-forward), свой pod CIDR, свой cgroup slice и свои лимиты. У каждой Pawn свой бласт-радиус: паника одной Pawn не утащит за собой ни хост-демон, ни соседние Pawns.
Планировщик видит обычные ноды и балансирует, как обычно:
# с тестов$ apsis pawnsNAME ROLE PORT PODScompute-00 pawn 12261 56compute-01 pawn 12262 55...compute-29 pawn 12290 56
30 pawns, ~55 подов на каждой.
Node != host
В отличие от ванильного kubelet, где поднимать потолок выше 110 не рекомендует сам апстрим, у каждой Pawn это конфигурируемый параметр, а не флаг с оговорками — сейчас, например, стоит 256. Потолок количества нод просто перестаёт быть потолком одной машины, и identity (далее Pawn) на одной машине можно завести хоть сто. Я запустил 33 Pawns на двух физических хостах и это не виртуализация и не трюк с вложенными контейнерами (как у kind/k3d, где нода это под в поде): это честных 33 Node объекта, у каждого свой сертификат и свой kubelet-эндпоинт.
Под = машина systemd
$ sudo machinectl | headMACHINE CLASS SERVICE OS VERSION ADDRESSESpod-0278b268-569a-43d0-8d01-791c77e90e7e-nginx container systemd-nspawn alpine 3.23.4 10.0.65.53…pod-0a272159-351a-4fdf-9738-6010f1f5ad47-nginx container systemd-nspawn alpine 3.23.4 10.0.127.141…pod-0b773d9b-e74b-41c0-b301-3169144f43de-nginx container systemd-nspawn alpine 3.23.4 10.0.55.182…pod-0fb1dabd-f7c3-461b-8052-413f70dbb2ad-nginx container systemd-nspawn alpine 3.23.4 10.0.50.176…pod-1342a134-e0d1-422f-913d-7533d3aca554-nginx container systemd-nspawn alpine 3.23.4 10.0.58.116…pod-1ad3491d-5779-4284-afb5-00ab0b480e60-nginx container systemd-nspawn alpine 3.23.4 10.0.115.134…pod-1cccc4de-6133-4b3e-8062-6579a219e67f-nginx container systemd-nspawn alpine 3.23.4 10.0.116.107…pod-2000e594-376b-45ef-a574-971c29fc1dc2-nginx container systemd-nspawn alpine 3.23.4 10.0.23.127…pod-204b0f04-871c-44e0-bcfb-6cc2c215be98-operator container systemd-nspawn periapsis 0 -
Каждый контейнер — это nspawn-машина из обычных OCI-образов (pull, дедупликация слоёв, P2P-раздача блобов между хостами по TLS). Изоляция ядерная: namespaces + cgroups, тот же механизм, что у runc, но без демона рантайма или shim. Журналы контейнеров едут сразу в journald.
Zero-downtime обновления демона
При рестарте perigeos умирает только сам демон, поды остаются жить. Новый демон стартует, находит их и продолжает с ними работу. Обновление агента не требует drain. После ребута хоста происходит обратное: демон замечает исчезнувшие машины и перезапускает их.
Справедливости ради: containerd v2 умеет то же самое — shim-процессы переживают рестарт демона независимо от него. Разница в том, что perigeos для этого не нужен отдельный shim-процесс на контейнер: супервизор пода — тот же systemd, который управляет любым другим юнитом на хосте, так что «пережить рестарт демона» здесь не отдельный механизм, а естественное следствие того, что под — это обычный юнит.
Цифры
Железо скромное: Xeon E5-2690 v4 (48 ГБ) и мини ПК на Intel N150 (16 ГБ), между ними обычный Ethernet (0.15 мс RTT).
Плотность: 1772 пода, 33 ноды
$ apsis status # engix99 (Xeon, 30 pawns)Hostname: engix99Uptime: 1h47mPawns: 30Pods: 1,660Memory: 29 / 48 GBCPU cores: 28Machines: 1,660RSS: 365 MiBLXC veths: 1,665Netns: 1,665$ apsis status # engifire (N150, 2 pawn)Hostname: engifireUptime: 1h45mPawns: 2Pods: 112Memory: 2.7 / 16 GBCPU cores: 4Machines: 112RSS: 89 MiBNetns: 112
365 МБ RSS на 1660 подов — это ~220 КБ на под. Для масштаба: один containerd-shim стоит ~15-20 МБ на контейнер, то есть на те же 1660 подов одни только shim’ы весили бы 25-33 ГБ — больше половины всей памяти этого сервера. Здесь этих процессов просто нет.
Под нагрузкой: k6, 1000 VU, 3m30s, ClusterIP -> nginx-whoami на 33 нодах
THRESHOLDS errors rate=0.00% < 0.01 PASS http_req_duration p(95)=4.05ms < 500 PASS p(99)=10.04ms < 1000 PASS TOTAL RESULTS checks_total........: 7,507,074 35,740/s checks_succeeded....: 100.00% checks_failed.......: 0.00% HTTP http_reqs...........: 2,502,358 11,913/s http_req_duration...: avg=998 us med=257 us p(90)=1.48 ms p(95)=4.05 ms p(99)=10 ms max=860 ms http_req_failed.....: 0.00% NETWORK data_received.......: 4.4 GB 21 MB/s data_sent...........: 170 MB 810 kB/s running (3m30.0s), 0/1000 VUs, 2,502,358 complete and 0 interrupted iterations
Ноль ошибок. 2.5 миллиона запросов. Медиана 257 мкс при ~12k rps — и это через ClusterIP по реальному datapath (eBPF, VXLAN между хостами), а не loopback-синтетика.
Свежие замеры (09.07.26)
$ sudo apsis statusHostname: engix99Version: 61a39f25Uptime: 9m26sPawns: 6Pods: 11Kernel: 7.1.3-x64v3-xanmod1Arch: linux/amd64Go: go1.26.5-X:nodwarf5Memory: 23998 / 48004 MiBCPU cores: 28Load avg: 1.44 1.85 1.86PSI cpu: 0.0%PSI memory: 0.0%Machines: 8Disk dirs: 106Units: 8RSS: 84 MiBLXC veths: 4Netns: 3Pawns:NAME ROLE PORT IP PODS CPU(ms) MEM(MiB)engix99 primary 12260 5 9348467 1457engix99-e2e-1 pawn 12261 2 170 0engix99-e2e-2 pawn 12262 0 0 0engix99-trail-1 pawn 12263 2 14721 51engix99-trail-2 pawn 12264 2 10335 77engix99-scale-1 pawn 12265 0 0 0
Апрельский 33-нодовый стенд я не пересобирал: рабочий кластер сейчас — 6 нод на тех же двух хостах. Так что это не повтор плотностного бенчмарка, а свежий замер текущей версии.
-
RSS демона в простое: 71.6 МБ (5 pawn) / 67.7 МБ (2 pawn); после 5-минутного load-теста — +4.5 и +2.7 МБ соответственно, никаких утечек памяти.
-
Мое предположение: ~55-60 МБ базы и рантайм плюс ~5-18 МБ на pawn, отсюда и диапазон «70-130 МБ» в таблице из раздела «Зачем».
-
21 под на 7 нодах, 2910 rps в течение 5 минут: p90 996 мкс, p95 1.36 мс, 0% ошибок, трафик дошёл до всех 21 реплик на всех 7 нодах.
Дешёвый VPS
Дешёвый VPS (1 vCPU, 1.9 ГБ RAM). Полный self-hosted control plane (apiserver + controller-manager + scheduler) плюс kube-proxy, CoreDNS и рабочий под — весь стек целиком:
perigeos, сам демон: ~102 МБ (~119 МБ пик)всё, что perigeos управляет: ~421 МБитого perigeos + нагрузка: ~523 МБ, ~7% одного ядрахост целиком (free -h): ~794 МБ / 1.9 ГБ (~42%)
Кластер отвечал на реальные запросы снаружи через NodePort (curl по внешнему IP), DNS резолвился через CoreDNS — обычный kubectl get pods -A показывает всё 1/1 Running.
И совсем маленький хост: Raspberry Pi Zero 2 W
Плата за ~15 долларов: 467 МиБ RAM, arm64, стоковое ядро Raspberry Pi OS. Тот же бинарь perigeos, нода регистрируется в кластере, обычный alpine под создаётся, тянет образ, работает — с обычными событиями в kubectl describe:
$ sudo apsis statusHostname: engipiPawns: 1Pods: 1Kernel: 6.1.21-v8+Arch: linux/arm64Memory: 177 / 467 MiBRSS: 59 MiB$ free -h total used free shared buff/cache availableMem: 467Mi 172Mi 87Mi 240Ki 265Mi 294Mi
59 МиБ RSS демона; весь хост вместе с рабочим подом — 172 МиБ из 467, свободно ещё почти 300. kubelet + containerd на эту плату просто не поместились бы вообще. Сноска: под здесь на hostNetwork — Constellation CNI требует ядро 6.6+, а на Pi стоковое 6.1.
Безопасность и что нода умеет сверх kubelet
Плотность — не единственное, что выпадает из отказа от CRI. Дальше — вещи, которые либо про безопасность, либо у стокового kubelet просто отсутствуют.
SecurityContext, но fail-closed
Поддержан полный SecurityContext: runAsUser/runAsGroup/fsGroup/supplementalGroups, runAsNonRoot, allowPrivilegeEscalation → NoNewPrivileges, capabilities add/drop, seccompProfile, sysctls, readOnlyRootFilesystem, privileged. Принцип один: поле, которое нода не может выполнить честно, отклоняется на этапе сборки конфига — Warning Event FailedCreateContainerConfig прямо в kubectl describe. Это не камень в огород kubelet — он такие поля тоже отклоняет. Это страховка против главного соблазна любой реимплементации ноды: молча запустить под без запрошенного ограничения, потому что так проще. Здесь этот путь запрещён как класс: например, seccompProfile: Localhost через systemd не загрузить — значит, под с ним не стартует вообще, а не стартует без профиля.
# runAsUser:1000 runAsGroup:1000 fsGroup:2000 supplementalGroups:[3000,4000], с emptyDir$ kubectl exec demo -- id # exec работает как в kubeletuid=1000 gid=1000 groups=2000,3000,4000$ kubectl exec demo -- stat -c '%U:%G %A' /dataroot:2000 drwxrwsr-x # fsGroup владеет томом, setgid выставлен
Root в поде — не root на хосте
userns-маппинг здесь по умолчанию для непривилегированных подов: UID 0 контейнера отображается в непривилегированный высокий UID хоста, так что эскейп из контейнера будет на хосте как nobody. В стоковом kubelet user namespaces до сих пор opt-in.
Containment, который видно
-
resources.limits — это настоящие cgroup-v2 caps (memory.max / cpu.max / pids.max), которые обеспечивает systemd, а не эмулирует рантайм: контейнер, вышедший за лимит, убивается, хост при этом не страдает.
-
Форк-бомбы: per-pod pids.max (
periapsis.io/max-pids, дефолт 4096 вместо унаследованных от systemd ~18874). Живьём: под с форк бомбой и max-pids=64 умер обfork() EAGAIN. -
OOM-детекция: рантайм ловит это и честно ставит OOMKilled. Eviction при node pressure ранжируется как у kubelet: QoS → priority → usage.
Устойчивость, которой у kubelet нет
-
Отказ API-сервера: поды продолжают работать, демон не падает, а когда KAS возвращается — нода сама приходит в Ready (~44 с, без рестарта демона).
-
Взрывной радиус: каждый pawn живёт за panic-firewall — паника одного pawn не роняет ни хост-демон, ни соседние pawn.
P2P-раздача образов
OCI-слои живут в content-addressable store и раздаются между pawn и хостами peer-to-peer: слой, скачанный однажды, дальше расходится по кластеру сам. Вплоть до вырожденного случая — нода тянет приватный (403 от registry) образ целиком от соседа, манифест и слои, без единого запроса в registry:
$ kubectl get eventsNormal ResolvingManifest pod/wasix-engifire Resolving image manifest: ghcr.io/.../wasix-info:latestNormal PulledFromPeer pod/wasix-engifire Layer a03d287ccca9 pulled from peer 192.168.100.200:12261 (h3, tier 0)Normal Started pod/wasix-engifire Container server started
Пиры выбираются по близости (subnet → local → remote), транспорт — HTTP/3 с fallback на TCP на каждого пира. А apsis ingest загружает образ в библиотеку ноды прямо из tar — и он тут же раздаётся P2P, так что образ попадает в кластер вообще без registry.
И два фикса уехали в сам systemd
Часть багов в самом systemd:
-
nspawn: fix EPERM when using —private-users with —network-namespace-path (смержен) — nspawn падал с EPERM, если контейнеру одновременно нужны user namespace (
--private-users) и присоединение к уже существующему сетевому неймспейсу (--network-namespace-path): inner child сначала оказывался в новом userns, аsetns()в чужой netns оттуда ядро уже не пускало. Для Periapsis это не экзотика, а буквально каждый под: userns-маппинг из раздела выше плюс join в netns, который уже настроил CNI. -
nspawn: fix .mstack bind mounts being masked by —volatile=overlay (на ревью) — bind-маунты из
.mstack-каталога молча исчезали при--volatile=overlay: overlayfs игнорирует активные mount points внутри lowerdir, так что mounts, собранные на этапе assembly, маскировались свежим оверлеем.
Это заодно и ответ на «почему systemd, там же нет всего для контейнеров»: там есть почти всё, а чего нет — туда можно принести ну или пофиксить.
Трейд-офф
Я сознательно (и не только) выбрал OS-изоляцию (общее ядро) вместо аппаратной или VM. Это значит: Periapsis не для враждебных мультитенантных нагрузок. Для доверенной внутренней инфраструктуры, CI/CD, edge и density-first сценариев это правильный размен: изоляция та же, что у обычных контейнеров (namespaces + cgroups + seccomp/caps от nspawn), а налог на microVM отсутствует.
Есть и третья точка на шкале изоляция/плотность — WASM-поды (о них в бонусе): для нагрузки, которой нужен sandbox сильнее namespaces, но без microVM, WASIp3 компонент работает в песочнице wasmtime с deny-by-default профилем. Непроаннотированный под не получает вообще ничего — ни host-интерфейсов, ни сети, ни env, ни файловой системы; каждое разрешение выдаётся явно, а невыданный импорт — это ошибка инстанцирования (подробнее в архитектуре WASI или в последующих статьях). Плюс лимит памяти на уровне рантайма: guest упирается в чистый wasm-trap до того, как за него возьмётся OOM-killer.
Второй пункт: экосистема считает, что нода выглядит как containerd. Стандартный Helm-чарт Cilium не встаёт как есть (Сам Cilium работает без твиков, это init-контейнеры чарта, лезущие хосту в /proc/sys), но у меня все-равно форк. Это постоянная цена за жизнь вне CRI.
С чем сравнивают
Коротко о том, чем это не является — от соседних слоёв стека к главному сравнению, ради которого весь пост.
vs k3s
k3s — компактный control plane (один бинарник, kine вместо etcd), но нода у него та же, что у ванильного Kubernetes: kubelet + containerd + тот же потолок в 110 подов. k3s решает проблему установки кластера, Periapsis решает проблему ноды — но теперь умеет и первое: perigeos сам поднимает свой control plane (apiserver + controller-manager + scheduler) холодным стартом из статических манифестов kubeadm — сам формат манифестов остаётся, убрана необходимость держать k3s/kubeadm запущенным как постоянный оркестратор; bootstrap и adopt происходят автоматически. Это не только homelab: недавно поднял её с нуля на чистом VPS (1 vCPU, 2 ГБ RAM) — вместе с kube-proxy, CoreDNS и рабочей нагрузкой control plane уместился в ~523 МБ, всё поднялось end-to-end и отвечало на реальные запросы снаружи через NodePort. По пути нашлись и сразу были починены пара edge-кейсов — именно так этот проект и взрослеет. Так что граница между «нода» и «control plane» тут не жёсткая: можно взять k3s поверх perigeos, как и раньше, а можно не брать вовсе.
vs LXC/Incus
Incus — системный контейнерный менеджер сам по себе, без Kubernetes сверху: свой CLI, свой API, своя модель профилей и сетей. Periapsis использует похожий изоляционный примитив (namespaces + cgroups через systemd-nspawn, тот же класс, что LXC), но остаётся нодой Kubernetes: тот же API-сервер, планировщик, Deployments, Services, NetworkPolicy, весь существующий инструментарий и экосистема YAML, которую все уже знают. Разница не в изоляции, а в том, что стоит сверху.
vs minikube / kind / k3d
Однонодовый (или под-в-поде, как kind/k3d) кластер на ноутбуке годится, чтобы проверить, что манифест применяется. Он не годится, чтобы проверить то, ради чего вообще нужен Kubernetes: как ведёт себя (anti-)affinity, topology spread, Service/NetworkPolicy, CNI между нодами, PodDisruptionBudget при cordon/drain реальной ноды — да и просто так, кто-то говорил что Kubernetes это только про Enterprise. Один хост Periapsis регистрируется как 30+ независимых pawn, это настоящий многонодовый кластер для тестов, а не имитация одной ноды под разными именами.
vs Knative
У Periapsis есть нативный scale-to-zero, целых два тира:
-
Frozen (
periapsis.io/freeze-after) — тот же процесс, та же память, просто снят с CPU. Прогретый JIT, пулы соединений, кэши, загруженные веса модели — всё остаётся тёплым; разморозка — это снятие freeze-флага, процесс продолжает с той же инструкции. RAM это не экономит (честно: freeze не отдаёт память), экономит CPU и время пробуждения. -
Idled (
periapsis.io/idle-after) — процесс останавливается по-настоящему, но под не удаляется: он виден в API-сервере, netns и IP сохранены, образ на месте. Пробуждение — новый процесс в уже существующем окружении.
Общая точка с Knative Serving та же: под, до которого никто не достучался, схлопывается. Но технически это устроено иначе: у Knative Deployment Revision’а уходит на 0 реплик, ReplicaSet удаляет под целиком, а следующий запрос — это полноценный новый под: новый UID, новый IP, планирование и CNI IPAM с нуля.
С моей стороны цифра есть: 10 чистых циклов STZ (замер 06.07.26) — пробуждение из Idled стабильно 1.4-1.6 с, и это тот же самый под: UID, IP и netns сохранены. Пробуждение из Frozen отдельно не замерял — там по устройству нечего мерить, кроме записи в cgroupfs. Цифру Knative рядом сознательно не ставлю: своего воспроизводимого замера у меня нет, а чужие cold-start бенчмарки слишком зависят от конфигурации, чтобы честно сравнивать. По устройству это в любом случае разные операции — у Knative “пробуждение” собирает новый под с нуля, т.е. “заморозка” у них — это удаление.
vs runwasi
runwasi — containerd v2 shim: запускает Wasm-модуль вместо обычного OCI-контейнера, но остаётся внутри CRI/containerd/shim-архитектуры — тот же дополнительный процесс на workload, тот же слой, который весь этот пост как раз выкидывает, просто вместо runc — Wasm-рантайм.
У Periapsis это runtimeClassName: trail — WASIp2/p3 компонент исполняется хостовым рантаймом напрямую, присоединённым к netns пода: без shim, без лишнего процесса на workload. (Ранние отдельные классы wasmtime/wasmedge/wasmer консолидированы в один trail.) Тот же тезис, что и в “Зачем” (нет прослойки между декларативным состоянием и процессом на хосте), только применённый к Wasm, а не к OCI-контейнерам.
vs kubelet
Это, собственно, главное сравнение — ради него весь пост. Все сравнения выше про соседние слои стека (control plane, изоляция без Kubernetes, локальный дев-кластер, scale-to-zero, Wasm-рантайм). Это — про саму ноду.
Путь одного контейнера на стандартной ноде: kubelet → CRI → containerd → containerd-shim → runc → процесс. У каждой прослойки свой процесс, своё состояние, свой рестарт и свои баги. Путь того же контейнера здесь: perigeos → D-Bus → systemd → процесс. Причём perigeos в этой цепочке не супервизор, а декларатор: systemd сам следит за процессами.
Из одного этого сокращения выпадает всё остальное — и почти ничего из этого не пришлось делать, оно случилось само:
-
Логи не собирает агент по текстовым файлам: они уже в journald, потому что под — это юнит.
-
Zero-downtime обновление демона — не механизм, а следствие: поды не дети perigeos, они дети systemd, и рестарт демона их просто не касается.
-
Видимость не требует crictl —
apsis,machinectl,journalctlиsystemctlпоказывают всё что нужно. -
“Нода != хост”: возвращаю оригинальную архитектуру Kubernetes, 30 pawn на одном хосте — это просто 30 Pawns с собственными сертификатами.
И чтобы было совсем честно: kubelet — не ошибка. Он проектировался, когда «нода» значило «железная машина», Docker был единственным рантаймом, а systemd ещё не умел и половины того, что умеет сейчас. Он сделал контейнеры скучной, надёжной технологией — и заслужил памятник.
Просто памятнику пришло время мены.
Бонус: WASM-поды без nspawn
Тот же тезис — убрать прослойку между декларативным состоянием и процессом — работает не только для контейнеров.
Periapsis, в общем-то, всё равно, что и через что запускать: для реконсайлера под — это «то, что должно работать», а чем оно окажется — nspawn-машиной из OCI-образа, хост-процессом или wasm-рантаймом в netns пода, да хоть VM — решает runtimeClassName. Ни одна из выкинутых прослоек не возвращается ни в одном из вариантов.
Под с runtimeClassName: trail — это не OCI-образ с wasm-рантаймом внутри и не containerd-shim (как runwasi), а WASIp2/p3 компонент, исполняемый хостовым рантаймом прямо в netns пода. Pod IP, Services, NetworkPolicy, логи в journald — всё как у обычного пода, только вместо rootfs — один .wasm артефакт, а вместо capability-набора Linux — deny-by-default профиль из “Трейд-оффа”.
Wasm HTTP-сервер держал под k6 ~14 тысяч rps с нулём ошибок (медиана 5.4 мс, 200 VU) — замер от 21.06.26, ещё на раннем WASIX-классе рантайма; нынешний trail — это уже компонентная модель (рекомендую к прочтению) WASIp2/p3, включая настоящие async-экспорты WASI 0.3.
А дальше начинается материал, который в этот пост уже не влезает: компоненты, связываемые через обычный WIT-интерфейс в одном процессе (вызов между «подами» за сотни наносекунд вместо микросекунд kernel IPC), cooperative checkpoint/restore — компонент сериализует своё состояние сам, рантайм переносит его через рестарт — и живой переезд stateful-компонента между хостами, включая x86 → arm64: .wasm не привязан к архитектуре, а чекпоинт — это байты приложения, не нативный стек. Об этом можно делать отдельный пост.
Что можно потрогать
Публичный репозиторий: https://github.com/apsis-io/showcase — бенчмарки с сырыми транскриптами, обзор возможностей и модели pawn, public issue tracker.
Про исходники. Periapsis пока не опубликован; лицензия — BUSL-1.1 (та же модель, что у ряда известных инфраструктурных проектов). По-человечески это значит: свободно для любого внутреннего использования — прод, дев, homelab, research, внутренняя инфраструктура компании; нельзя — продавать это как hosted/managed сервис (SaaS/XaaS), коммерчески бандлить в edge/IoT-продукты и брать деньги за удалённое управление чужой инфрой. Консалтинг на инфраструктуре самого заказчика — можно. Важный нюанс, чтобы никто не боялся зря: запускать на Periapsis свой коммерческий продукт или SaaS (как Fly.io) — можно, это и есть внутреннее использование вашей инфраструктуры; запрещено продавать как услугу сам Periapsis — его хостинг, оркестрацию, управление. И это не навсегда: каждый релиз автоматически переходит под GPL-3.0-or-later не позднее чем через 4 года после выхода. Полный текст лицензии опубликован в showcase-репозитории.
Почему так: я соло-разработчик, и это осознанный размен — настолько открыто, насколько это не превращает проект в бесплатный R&D для чужого managed-сервиса.
Коммерческая лицензия за пределами этих условий и энтерпрайз-вопросы — в личку Хабра или контакты из репозитория.
FAQ (подключаю libastral.so)
-
«Это же велосипед, есть k3s». k3s — компактный control plane (один бинарник, kine вместо etcd). Нода у него та же самая: kubelet + containerd + потолок в 110 подов. Мы решаем разные проблемы и спокойно совместимы — апрельский стенд из «Цифр» работал именно под k3s.
-
«Чем это лучше LXC/Incus?» Тем, что это нода Kubernetes: API-сервер, планировщик, Deployments, Services, NetworkPolicy — весь инструментарий и вся экосистема YAML, которую команда уже знает. Изоляционный примитив похожий, разница — во всём, что сверху.
-
«systemd-nspawn — это несерьёзно». Механизм изоляции тот же, что у runc/docker/containerd: namespaces + cgroups + seccomp/caps. Что правда — в проде nspawn обкатан меньше. Этот риск я знаю и считаю оплаченным: взамен исчезает целый слой со своими процессами, своим состоянием и своими багами.
-
«Сломалась тачка — сломались сразу 100 нод». Нарезка на pawn не создаёт новый failure domain: хост как был единой точкой отказа, так и остался — что с одной нодой, что с тридцатью (см. “Node != host” в архитектуре). Падает хост — падает всё, что на нём крутилось, при любой архитектуре; вопрос в масштабе, а не в наличии проблемы. HA как строилась на нескольких физических хостах (anti-affinity, topology spread), так и строится.
-
«Где исходники?» См. «Что можно потрогать»: showcase-репозиторий с бенчмарками и документацией уже открыт, транскрипты воспроизводимы. Полный текст лицензии там же.
-
«А безопасность мультитенантности?» Первый абзац «Честного трейд-оффа»: Periapsis сознательно не для враждебных тенантов. Общее ядро — значит, недоверенный код, умеющий в kernel-эксплойты (а их нынче развелось), должен жить в microVM-рантайме, а не здесь. Для sandbox сильнее namespaces без microVM есть WASM-путь с deny-by-default (см. бонус).
ссылка на оригинал статьи https://habr.com/ru/articles/1058526/